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
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
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
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
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
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
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
Post a Comment