Core Java

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 provided Visitable
  • DbVisitor that stores it in database, 
  • ShoppingCart that adds it to a shopping cart 

etc.

Downsides of visitor pattern

  1. Return type of the visit() methods must be defined at design time. In fact in most cases these methods are void.
  2. Implementations of theaccept() method are identical in all classes. Obviously we prefer to avoid code duplication. 
  3. Every time the new model class is added each visitor must be updated, so the maintenance becomes hard. 
  4. It is impossible to have optional implementations for certain model class in certainvisitor. 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.

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

MapVisitorhas a package-private constructor. Initialization ofMapVisitordone 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. 

  1. visit() methods ofVisitordescribed here can return values and therefore may be implemented as pure functions [3] that help to combine Visitor pattern with functional programming paradigm.
  2. Breaking monolithic Visitor interface into separate blocks makes it more flexible and simplifies the code maintenance. 
  3. MapVisitorcan be configured using builder at runtime, so it may change its behavior depending on information known only at runtime and unavailable during development. 
  4. Visitors with different return type can be applied to the same Visitable classes.
  5. Default implementation of methods done in interfaces removes a lot of boiler plate code usual for typical Visitor implementation. 

References

  1. Wikipedia
  2. DZone
  3. Definition of pure function.

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.

Alexander Radzin

Alex is an experienced software engineer coding java and JVM based languages and technologies since 2000. Besides his work at the office he develops his own open source project at github and contributes code to other projects. Actively participated in StackOverflow community during several years.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button