This is the second part in my series on Obsolete Design Patterns, the general premise of the series is to go over design patterns that are obsoleted by (generally FP) language features or idioms that are much more common place now compared to 30 years ago.
Visitor Pattern (Java 8+) Link to heading
The goal of this pattern is to separate algorithms from the classes that the algorithm uses. One of the main selling points of this pattern is that it adheres to the OCP, which will allow further extension by clients without modifying the source code (important even outside of adherence to other principles like SRP, e.g if the soure code is in a library).
The example will showcase a Shape
abstraction with multiple variants - Dot, Circle, and Rectangle. The objective is to allow users of the Shape
abstraction to safely add algorithms that work on the different shapes without having to modify the Shape
abstraction or the shapes themselves.
A note on the code before I show it, ideally the Shape
would be sealed
, because we don’t want clients to have the ability to add their own Shape
implementations (as they don’t also have the ability to add new functions to the ShapeVisitor
) but that keyword doesn’t exist until Java 17 (out of preview). Later in this post I do write a more modern Java example that will use sealed
for this reason.
Here’s a Shape
hierarchy using the visitor pattern:
|
|
What this gets us is the ability for clients to implement algorithms over Shape
without modifying the source code. You can add new aglorithms by implementing ShapeVisitor
and then you will be able to call .accept
on any Shape
, pass in your Visitor, and it’ll work on any of them.
If you then wanted to implement an algorithm, say to calculate the area of a shape, you could do that like so:
|
|
Without the visitor pattern, you’d have to resort to runtime checking/casting of types (on older versions of Java):
|
|
While this is a bit longer and more verbose, the biggest issue is that it’s not safe. If the source code were to change (e.g the library author adds another shape), your code would fail at runtime, where as it would fail to compile in the visitor pattern, guiding you to implement the new logic needed immediately.
What the functional paradigm has to offer (Scala 3) Link to heading
One of the largest requirements of the visitor pattern is that new subtypes cannot be added on demand by clients who don’t have access to the source code of the original hierarchy. This is because the Visitor
needs to implement a function for each case for the pattern to work. This is why like I mentioned earlier we would want the Shape
interface to be sealed. There’s actually a much more concise way to define these short, sealed “hierarchies” - ADTs.
Recall that the main advantages of using the visitor pattern are:
- creating new algorithms without modifying the original implementation (adherence to SRP/OCP)
- safe at compile time against the addition of new
Shape
s in the original implementation
ADTs achieve this in a much more straightforward way. Here’s an example of that in Scala:
|
|
and then users of your Shape
can safely create algorithms without modifying the original source code with plain functions and pattern matching:
|
|
The advantages of this are pretty clear from the code:
- we don’t need to define a
Visitor
interface - we don’t need to have an
accept
method on theShape
(and all subclasses) - implementing and calling new algorithms is as easy as defining and calling functions
there are other advantages to the code too, but they mostly revolve around the more modern syntax of Scala over Java.
Java 21 implementation Link to heading
|
|
and the calculateArea
translation:
|
|
The language I use (Java, C#, C++, Kotlin) has enums, can’t I just use those to make it like the nice/short Scala from earlier? Link to heading
No, those languages don’t support enum ADTs. There are some languages that use the enum syntax for allowing ADT definitions (Scala, Swift, Rust), and others that have alternative first-class non-enum syntax for ADTs (Haskell, Idris, F#, OCaml). The biggest issue is you can’t associate values with individual cases of enums in languages with the simple form of enums, so you have to resort to using a sealed interface/trait and records/structs that implement it, like the above Java 21. If the language also supports pattern matching (like Java now does) it’s a fine approximation, but it’s definitely more verbose.