The Visitor Pattern Needs Fixed. Here's How.

The Visitor Pattern is a powerful idiom of object-oriented programming. It has the unique power to break through the type system and recover hidden type, all while using the type system and double-dispatch to accomplish this feat. Through this superpower, it is able to give the impression of adding new behavior to closed classes.

The visitor pattern is very powerful, but it is also quite burdensome. Classes to visit must implement an #accept method that takes a visitor, and then immediately calls that visitor with itself (generally, or some data on itself). This double-dispatch is what allows the type to be recovered, since a type's this will always be its own type. The problem then, is that there is a repetition on all of the different types to visit. Each subtype must implement #accept. It also treads the line of an SRP violation. A class must be concerned with what it does, plus allowing a visitor to visit it.

This really has never sat that well with me, so I began to think about how the visitor pattern could be improved through better programming principles. On one end, we have the maxim of favoring composition over inheritance. If the visitor pattern is used to visit subtypes, then there is often inheritance used in some fashion to accomplish this. Could we instead wrap the object to be visited? If we want a set of Foo types to be visited, could we implement a VisitableFoo that implements Foo and the visitor pattern. It would take a Foo and delegate to it for all the other calls, and implement the visitor pattern for when a visitor is used. The upside is that now the Foo responsibilities are separated from the visitor responsibilities. The downside is that either there will now need to be an equivalent VisitableFoo hierarchy to the Foo hierarchy, or we will need to write explicit type-checking code into VisitableFoo against its wrapped type.

At the other end, we have functional-style programming. Ruby blocks are an exemplary way to inject behavior into an otherwise closed piece of code. By using higher-order methods and functions, new behavior can be injected like any other parameter. This functional style is becoming so popular that it is starting to permeate even into more rigid languages, like Java. Higher-order functions are a great way to inject behavior, but they do not differentiate based on type. They can be used to make a straightforward interface for a visitor, but do no obviate the need for double-dispatch on subtype. Functional-style programming also brings the idea of monads into more common parlance in the programming community. Monads are interesting in that they can create seams at which behavior can be injected, all through generic composition. An optional monad can wrap any type of object and respond properly based on its null-ness.

These ideas all started to coalesce in my mind together. What if we take one bad thing, explicit type-checking, and encapsulate it in a way that makes a much simpler, more agreeable interface for the visitor pattern? What if we also make it generic enough that it could work on any arbitrary inheritance chain? This is how my ideas for the Acceptor came to be. There is currently both a C# and a Java implementation.

Rather than having to implement the visitor pattern on the objects we want to visit, let's wrap those objects into something more generic that just decides if the inner object is the right type for the visitor. While there is some explicit type-checking code, it is hidden away in the Acceptor object, and all the outside needs to know is that it has an #accept method that takes a function with a parameter of a specific type. If the inner object is of the right type, the function gets called with it; if it is not the right type, nothing gets called. Now any object can be visited, and we can get the type we want. This takes the responsibility off of the visited type, but it also takes the control from it as well. The visitor, not the visitee, decides the condition of visitation.

This leads to a very clean separation of concerns, and objects no longer need to worry about whether to implement the visitor pattern. Through basic composition, the behavior can simply be added generically.

If you want to check out the C# version, go to: https://github.com/cbojar/acceptor-csharp

If you want to check out the Java version, go to: https://github.com/cbojar/acceptor-java

Comments

Popular posts from this blog

The Timeline of Errors

Magic Numbers, Semantics, and Compiler Errors

Assuming Debugging