Last week we started look at the Visitor pattern. This week we’ll continue where we left off last week, and write some more code for the Visitor pattern. And in so doing, we’ll end our exploration of design patterns.
Revision of the Visitor Pattern
Last week we said that the Visitor pattern allows us to separate algorithms from the objects on which they operate. By decoupling the algorithm from the data it works on, we can easily add future required behaviours to the class hierarchy without changing the classes.
The steps to define a new method include creating an interface for visitors, and adding accept()
methods in the class hierarchy that a visitor will call. The accept()
methods dispatch their calls back to the visitor using a double-dispatch mechanism. This allows the execution of a visit()
method that applies to the specific class.
We use the Visitor pattern when we want to execute an operation on all of the elements of a complex structure, e.g, an object tree in a composite class hierarchy.
Visitor Pattern: Design Example
In our first person shooter game we’ve already created mechanised fighting units that we can command to move and fight. (See the previous posts on the Bridge, Command and Proxy patterns).
We already have a FightingUnit
interface with methods for fighting and moving:
public interface FightingUnit { public void fight(); public void turn(int degrees); public void advance(); public void retreat(); }
We’ve already implemented the FightingUnit
interface with concrete classes such as MechanisedFightingVehicle
s and MechanisedFightingRobot
s.
What happens if we need to add new functionality to all these implemented classes? We would have to change the FightingUnit
interface and all the concrete classes. And every time we need to add extra functionality, we’d have to modify the same classes over and over again. Lots of additional coding and testing!
Visitor Pattern: Implementation
This would be a good place to use the Visitor pattern. We would still have to modify the existing FightingUnit
interface and the previously written concrete classes, but we’d only do that once. After that, we would be able to add any number of extra operation by simply creating new visitors, as opposed to changing our existing code.
Remember that the Visitor pattern participants are the following:
Element
: an interface that contains anaccept()
method that takes aVisitor
as an argument.ConcreteElement
: implements theaccept()
method defined inElement
.Visitor
: an interface that declares avisit()
method for each class ofConcreteElement
.ConcreteVisitor
: the concrete classes that appropriately implement each method declared by theVisitor
interface.- The client code creates visitor objects and passes each of these objects to the
accept()
calls.
Let’s use the existing FightingUnit
interface as the Element
participant. We’ll add the accept(Visitor v)
method to it:
public interface FightingUnit { public void fight(); public void turn(int degrees); public void advance(); public void retreat(); public void accept(Visitor v); }
Next we will implement the accept()
method in our MechanisedFightingVehicle
and MechanisedFightingRobot
classes. These would be the ConcreteElement
implementations. Only one of the classes is shown; the other has the identical implementation of the accept()
method.
public class MechanisedFightingRobot implements FightingUnit { public void fight() { System.out.println("Robot fighting!"); } public void turn(int degrees) { System.out.println("Robot turning through " + degrees + " degrees"); } public void advance() { System.out.println("Robot advancing..."); } public void retreat() { System.out.println("Robot retreating..."); } @Override public String toString() { return getClass().getName(); }
public void accept(Visitor v) { v.visit(this); } }
Next we define the Visitor
interface with a visit()
method for each concrete class type:
public interface Visitor { void visit(MechanisedFightingVehicle vehicle); void visit(MechanisedFightingRobot robot); }
Now we can create any number of concrete implementations of the Visitor
interface that we need. Let’s implement a LogVisitor
or a PersistenceVisitor
for example.
public class LogVisitor implements Visitor { public void visit(MechanisedFightingVehicle vehicle) { System.out.println(vehicle + " is logged."); // some appropriate code to log a vehicle; } public void visit(MechanisedFightingRobot robot) { System.out.println(robot + " is logged."); // some appropriate code to log a robot; } }
Finally we create the client code that creates visitor objects and passes each object to the accept()
methods.
public class VisitorClient { public static void main(String args[]) { FightingUnit units[] = { new MechanisedFightingVehicle(),
new MechanisedFightingRobot() }; Visitor logVisitor = new LogVisitor(); Visitor persistenceVisitor = new PersistenceVisitor(); for (FightingUnit unit : units) unit.accept(logVisitor); for (FightingUnit unit : units) unit.accept(persistenceVisitor); } }
We haven’t shown it here, but MechanisedFightingVehicle
s and MechanisedFightingRobot
s fit into a Composite pattern (see the relevant post on the Composite pattern). Some MechanisedFightingUnit
s would contain other MechanisedFightingUnit
s. A MechanisedFightingVehicle
would contain MechanisedFightingRobot
s. We could have a variety of container classes such as MechanisedFightingVehicle
, MechanisedFightingVehicleTransporter
, MechanisedFightingBattleship
, etc. The Visitor pattern works well with the Composite pattern.
JSE Examples of the Visitor Pattern
The Visitor pattern is recognizable by two different abstract/interface types which have methods that each take the other abstract/interface type. One calls the method of the other, and the other executes the desired strategy on it.
javax.lang.model.element.AnnotationValue
andAnnotationValueVisitor
.javax.lang.model.element.Element
andElementVisitor
.javax.lang.model.type.TypeMirror
andTypeVisitor
.java.nio.file.FileVisitor
andSimpleFileVisitor
.
Pros and Cons of the Visitor Pattern
The benefits of using the Visitor pattern include:
- We can add functionality to class libraries for which we either do not have the source code or cannot change the source code.
- We can get data from a disparate collection of unrelated classes, and use it to present the results of a global calculation to a client program.
- We can group related operations into a single class rather than having to change or derive classes to add these operations.
- We can collaborate with the Composite pattern.
The Visitor pattern works well when an operation is relevant to some of the classes, but not the others. We define the operation into a separate visitor class and implement only those visiting methods that accept the relevant object types. The rest of the methods will be empty stub methods.
The Visitor pattern is not good in situations where the visited classes aren’t stable. Every time a new composite hierarchy class is added, every visitor class must be modified.
Another disadvantage is that visitor classes might not have the necessary access to private members of the objects that they have to work with.
Conclusion
The Visitor pattern is often thought of as unnecessarily complex. This is due to the fact that a visitor can visit either a collection of different objects, a composite created by using the Composite pattern, or an inheritance tree.
We have to think about the problem carefully, and have a clear understanding of the problem before using Visitor, otherwise it can make our code unnecessarily complex. But in the right situation, the Visitor pattern can provide an elegant solution to complex problems.
I’m always interested in your opinion, so please leave a comment. Your feedback helps me write tips that help you.