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.
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 provided Visitable,
- 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 are void.
- 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 method visit(Milk) but can implement visit(Software). Possible solution is to throw UnsupportedOperationException 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.
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:
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 interface MonoVisitor as following:
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 methods visit(). 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 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.
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); } }
- 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<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:
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:
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 on github.
Full source code of examples used here is available on github.
Conclusions
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.
- visit() methods of Visitor 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.
No comments:
Post a Comment