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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
abstract class Shape {
  int x;
  int y;

  Shape moveTo(int newX, int newY)
  <A> A accept(ShapeVisitor<A> v)
}

interface ShapeVisitor<A> {
  A visitDot(Dot d);
  A visitCircle(Circle c);
  A visitRectangle(Rectangle r);
}

class Dot implements Shape {
  Dot(int x, int y) { 
    this.x = x; 
    this.y = y; 
  }

  void moveTo(int newX, int newY) {
    return new Dot(newX, newY);
  }

  <A> A accept(ShapeVisitor<A> v) { 
    return v.visitDot(this);
  }
}

class Circle implements Shape {
  int radius;

  Circle(int x, int y, int radius) { 
    this.x = x; 
    this.y = y; 
    this.radius = radius; 
  }

  void moveTo(int newX, int newY) { 
    return new Circle(newX, newY, radius);
  }

  <A> A accept(ShapeVisitor<A> v) { 
    return v.visitCircle(this);
  }
}

class Rectangle implements Shape {
  int width;
  int height;

  Rectangle(int x, int y, int width, int height) { 
    this.x = x; 
    this.y = y; 
    this.width = width; 
    this.height = height; 
  }

  void moveTo(int newX, int newY) { 
    return new Rectangle(newX, newY, width, height);
  }

  <A> A accept(ShapeVisitor<A> v) { 
    return v.visitRectangle(this);
  }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class AreaCalculatorVisitor implements ShapeVisitor<Integer> {
  Integer visitDot(Dot d) {
    return 0;
  }

  Integer visitCircle(Circle c) {
    return Math.PI * Math.pow(c.radius, 2);
  }

  Integer visitRectangle(Rectangle r) {
    return r.width * r.height;
  }
}

// usage
Integer area = someShape.accept(new AreaCalculatorVisitor());
List<Integer> areas = someShapes.stream()
  .map(s -> s.accept(new ArreaCalculatorVisitor()))
  .collect(Collectors.toList());

Without the visitor pattern, you’d have to resort to runtime checking/casting of types (on older versions of Java):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class AreaCalculatorInteractor {
  static Integer perform(Shape s) {
    if (s instanceof Dot) {
      return 0;
    } else if (s instanceof Circle) {
      Circle c = (Circle) s;
      return Math.PI * Math.pow(c.radius, 2);
    } else if (s instanceof Rectangle) {
      Rectangle r = (Rectangle) s;
      return r.width * r.height;
    } else {
      throw new IllegalArgumentException("Unsupported shape type");
    }
  }
}

// usage
Integer area = AreaCalculatorInteractor.perform(someShape);
List<Integer> areas = someShapes.stream()
  .map(AreaCalculatorInteractor::perform)
  .collect(Collectors.toList());

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 Shapes in the original implementation

ADTs achieve this in a much more straightforward way. Here’s an example of that in Scala:

1
2
3
4
5
6
7
8
9
enum Shape:
  case Dot(x: Int, y: Int)
  case Circle(x: Int, y: Int, radius: Int)
  case Rectangle(x: Int, y: Int, width: Int, height: Int)

  def moveTo(newX: Int, newY: Int) = this match
    case d: Dot => Dot(newX, newY)
    case c: Circle => Circle(newX, newY, c.radius)
    case r: Rectangle => Rectangle(newX, newY, r.width, r.height)

and then users of your Shape can safely create algorithms without modifying the original source code with plain functions and pattern matching:

1
2
3
4
5
6
7
8
val calculateArea: Shape => Int =
  case _: Shape.Dot => 0
  case c: Shape.Circle => math.Pi * math.pow(c.radius, 2)
  case r: Shape.Rectangle => r.width * r.height

// usage
val area = calculateArea(someShape)
val areas = someShapes.map(calculateArea)

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 the Shape (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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
sealed interface Shape permits Dot, Circle, Rectangle {
  Shape moveTo(int newX, int newY);
}

record Dot(int x, int y) implements Shape {
  Shape moveTo(int newX, int newY) {
    return new Dot(newX, newY);
  }
}

record Circle(int x, int y) implements Shape {
  Shape moveTo(int newX, int newY) {
    return new Circle(newX, newY, radius);
  }
}

record Rectangle(int x, int y, int width, int height) implements Shape {
  Shape moveTo(int newX, int newY) { 
    return new Rectangle(newX, newY, width, height);
  }
}

and the calculateArea translation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ShapeUtils {
  static int calculateArea(Shape shape) {
    return switch (shape) {
      case Shape.Dot d -> 0;
      case Shape.Circle c -> Math.PI * Math.Pow(c.radius, 2);
      case Shape.Rectangle r -> r.width * r.height;
    }
  }
}

// usage
var area = ShapeUtils.calculateArea(someShape);
var areas = someShapes.stream().map(ShapeUtils::calculateArea).toList();

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.