New life of old Visitor design pattern
Introduction
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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | public interface Visitable { public void accept(Visitor visitor); } public class Book implements Visitable { ....... @Override public void accept(Visitor visitor) {visitor.visit( this )}; ....... } public class Cd implements Visitable { ....... @Override public void accept(Visitor visitor) {visitor.visit( this )}; ....... } interface Visitor { public void visit(Book book); public void visit(Magazine magazine); public void visit(Cd cd); } |
Now we can implement various visitors
, e.g.
PrintVisitor
that prints providedVisitable
DbVisitor
that stores it in database,ShoppingCart
that adds it to a shopping cart
etc.
Downsides of visitor pattern
- Return type of the
visit()
methods must be defined at design time. In fact in most cases these methods arevoid
. - 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
visitor
must 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,EmailSendingVisitor
cannot implement methodvisit(Milk)
but can implementvisit(Software)
. Possible solution is to throwUnsupportedOperationException
but the caller cannot know in advance that this exception will be thrown before it calls the method.
Improvements to classical Visitor pattern
Return value
First, let’s add return value to the Visitor
interface. General definition can be done using generics.
01 02 03 04 05 06 07 08 09 10 | public interface Visitable { public <R> R accept(Visitor<R> visitor); } interface Visitor<R> { public R visit(Book book); public R visit(Magazine magazine); public R visit(Cd cd); } |
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 Visitor
pattern).
Default implementation
Next, let’s thank Java 8 for its ability to hold default implementations inside the interface:
1 2 3 4 5 | public interface Visitable<R> { default R accept(Visitor<R> visitor) { return visitor.visit( this ); } } |
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.
MonoVisitor
Let’s try to apply further improvements. First, let’s define interfaceMonoVisitor
as following:
1 2 3 | public interface MonoVisitor<T, R> { R visit(T t); } |
The name Visitor
was changed to MonoVisitor
to avoid name clash and possible confusion. By the book visitor
defines many overloaded methodsvisit()
. Each of them accepts argument of different type for each Visitable
. Therefore, 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
Since 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 newvisitor
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.
01 02 03 04 05 06 07 08 09 10 11 12 13 | public class MapVisitor<R> implements Function<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> { private final Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors; MapVisitor(Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors) { this .visitors = visitors; } @Override public MonoVisitor apply(Class clazz) { return visitors.get(clazz); } } |
The MapVisitor
- Implements
Function
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
MapVisitor
has a package-private constructor. Initialization ofMapVisitor
done using special builder is very simple and flexible:
1 2 3 4 | MapVisitor<Void> printVisitor = MapVisitor.builder(Void. class ) .with(Book. class , book -> {System.out.println(book.getTitle()); return null ;}) .with(Magazine. class , magazine -> {System.out.println(magazine.getName()); return null ;}) .build(); |
MapVisitor usage is similar to one of the traditional Visitor
:
1 2 | someBook.accept(printVisitor); someMagazine.accept(printVisitor); |
Our 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 java.util.Predicate
:
01 02 03 04 05 06 07 08 09 10 | public class MapVisitor<R> implements Function<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>>, Predicate<Class<? extends Visitable>> { private final Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors; ............... @Override public boolean test(Class<? extends Visitable> clazz) { return visitors.containsKey(clazz); } } |
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.
Conclusions
This article demonstrates several improvements that make the good oldVisitor
pattern more flexible and powerful. The suggested implementation avoids some boiler plate code necessary for implementation of classicVistor
pattern. Here is the brief list of improvements explained above.
visit()
methods ofVisitor
described here can return values and therefore may be implemented as pure functions [3] that help to combine Visitor pattern with functional programming paradigm.- Breaking monolithic
Visitor
interface into separate blocks makes it more flexible and simplifies the code maintenance. MapVisitor
can 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
Visitable
classes. - Default implementation of methods done in interfaces removes a lot of boiler plate code usual for typical
Visitor
implementation.
References
Published on Java Code Geeks with permission by Alexander Radzin, partner at our JCG program. See the original article here: New life of old Visitor design pattern Opinions expressed by Java Code Geeks contributors are their own. |