Visitor [1, 2] is a widely known classical design pattern. There are a lot of resources that explain it in details. Without digging into the implementation I will briefly remind the idea of the pattern, will explain its benefits and downsides and will suggest some improvements that can be easily applied to it using Java programming language.
Classical Visitor[Visitor] Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure. (Gang of Four book)
The pattern is based on interface typically called.
Visitable that has to be implemented by model class and a set of
Visitors that implement method (algorithm) for each relevant model class.
Now we can implement various
PrintVisitorthat prints provided
DbVisitorthat stores it in database,
ShoppingCartthat adds it to a shopping cart
Downsides of visitor pattern
- Return type of the
visit()methods must be defined at design time. In fact in most cases these methods are
- Implementations of the
accept()method are identical in all classes. Obviously we prefer to avoid code duplication.
- Every time the new model class is added each
visitormust be updated, so the maintenance becomes hard.
- It is impossible to have optional implementations for certain model class in certain
visitor. For example, software can be sent to a buyer by email while milk cannot be sent. However, both can be delivered using traditional post. So,
EmailSendingVisitorcannot implement method
visit(Milk)but can implement
visit(Software). Possible solution is to throw
UnsupportedOperationExceptionbut the caller cannot know in advance that this exception will be thrown before it calls the method.
Improvements to classical Visitor pattern
First, let’s add return value to the
Visitor interface. General definition can be done using generics.
Well, this was easy. Now we can apply to our Book any kind of
Visitor that returns value. For example,
DbVisitor may return number of changed records in DB (Integer) and
ToJson visitor may return JSON representation of our object as String. (Probably the example is not too organic, in real life we typically use other techniques for serializing object to JSON, but it is good enough as theoretically possible usage of
Next, let’s thank Java 8 for its ability to hold default implementations inside the interface:
Now class that implements
Visitable does not have to implement
>visit() itself: the default implementation is good enough in most cases.
The improvements suggested above fix downsides #1 and #2.
Let’s try to apply further improvements. First, let’s define interface
MonoVisitor as following:
Visitor was changed to
MonoVisitor to avoid name clash and possible confusion. By the book
visitor defines many overloaded methods
visit(). Each of them accepts argument of different type for each
Visitor by definition cannot be generic. It has to be defined and maintained on project level.
MonoVisitor defines one single method only. The type safety is guaranteed by generics. Single class cannot implement the same interface several times even with different generic parameters. This means that we will have to hold several separate implementations of
MonoVisitor even if they are grouped into one class.
Function reference instead of Visitor
MonoVisitor has only one business method we have to create implementation per model class. However, we do not want to create separate top level classes but prefer to group them in one class. This new
visitor holds Map between various Visitable classes and implementations of
java.util.Function and dispatches call of
visit() method to particular implementation.
So, let’s have a look at MapVisitor.
in order to retrieve particular implementation (full generics are omitted here for readability; have a look at the code snippet for detailed definition)
- Receives mapping between class and implementation in map
- Retrieves particular implementation suitable for given class
MapVisitorhas a package-private constructor. Initialization of
MapVisitordone using special builder is very simple and flexible:
MapVisitor usage is similar to one of the traditional
MapVisitor has one more benefit. All methods declared in interface of a traditional visitor must be implemented. However, often some methods cannot be implemented.
For example, we want to implement application that demonstrates various actions that animals can do. The user can choose an animal and then make it do something by selecting specific action from the menu.
Here is the list of animals:
Duck, Penguin, Wale, Ostrich
And this is the list of actions:
Walk, Fly, Swim.
We decided to have visitor per action:
WalkVisitor, FlyVisitor, SwimVisitor. Duck can do all three actions, Penguin cannot fly, Wale can only swim and
Ostrich can only walk. So, we decided to throw exception if a user tries to cause Wale to walk or
Ostrich to fly. But such behavior is not user friendly. Indeed, a user will get error message only when he presses the action button. We would probably prefer to disable irrelevant buttons.
MapVisitor allows this without additional data structure or code duplication. We even do not have to define new or extend any other interface. Instead we prefer to use standard interface
Now we can call function
test() in order to define whether action button for selected animal has to be enabled or shown.
Full source code of examples used here is available ongithub.
This article demonstrates several improvements that make the good old
Visitor pattern more flexible and powerful. The suggested implementation avoids some boiler plate code necessary for implementation of classic
Vistor pattern. Here is the brief list of improvements explained above.
Visitordescribed here can return values and therefore may be implemented as pure functions  that help to combine Visitor pattern with functional programming paradigm.
- Breaking monolithic
Visitorinterface into separate blocks makes it more flexible and simplifies the code maintenance.
MapVisitorcan be configured using builder at runtime, so it may change its behavior depending on information known only at runtime and unavailable during development.
- Visitors with different return type can be applied to the same
- Default implementation of methods done in interfaces removes a lot of boiler plate code usual for typical