Visitor
- it is a behavioral design pattern that lets you separate algorithms from the objects on which they operate
Problem
- when a team develops an app which works with geographic information structured as 1 colossal graph
- each node of the graph may represent a complex entity
- such as a city, and granular things like industries, sightseeing areas, etc
- the nodes are connected with others if there's a road between real objects that they represent
- under the hood, each node type is represented by its own class
- while each specific node is an object
- each node of the graph may represent a complex entity
- at some point, a new task is given to implement exporting the graph into XML format
- seems straightforward
- plan to add an export method to each node class
- and then leverage recursion to go over each node of the graph, executing the export method
- solution was simple and elegant
- thanks to polymorphism, code weren't coupled that called the export method to concrete classes of node
- however, the system architect refused to allow altering of existing node classes
- since code was already in production, its not good to take risk breaking the app due to a potential bug in the changes
- it also does not make sense to have XML export code within node classes
- primary job of these classes was to work with geodata
- XML export behavior would look alien there
- primary job of these classes was to work with geodata
- another reason for refusal is after this feature is implemented
- someone from the marketing department would ask to provide the ability to export into a different format
- or request some other weird stuff
- this would force you to change those classes again
- someone from the marketing department would ask to provide the ability to export into a different format
Solution
the visitor pattern suggests that the new behavior should be placed into a separate class call visitor
- the original object that had to perform the behavior is now passed to 1 of the visitor's methods as an argument
- providing the method access to all necessary data contained within the object
- the original object that had to perform the behavior is now passed to 1 of the visitor's methods as an argument
the behavior can also be executed over objects of different classes
- the visitor class may define set of methods, each of which could take arguments of different types
class ExportVisitor implements Visitor is
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
// ... - however, calling of these methods will be an issue since they have different signatures, thus polymorphism can't be used
foreach (Node node in graph) {
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
// ...
} - why not use method overloading?
- give all methods the same name even if they support different sets of parameters
- however, even if java & C# supports it, it won't be of help
- since the exact class of a node object is unknown in advance
- the overloading mechanism won't be able to determine the correct method to execute
- it would default to the method that takes an object of the base Node class
- the visitor class may define set of methods, each of which could take arguments of different types
the Visitor pattern addresses this problem by using the
Double Dispatch
methodthis helps to execute the proper method on an object without cumbersome conditionals
instead of letting the client select a proper version of the method to call
we delegate the choice to objects we're passing to the visitor as an argument
since the objects know their own classes
they would be able to pick a proper method on the visitor less awkwardly
the accept a visitor and tell it what visiting method should be executed
// Client code
foreach (Node node in graph)
node.accept(exportVisitor)
// City
class City is
method accept(Visitor v) is
v.doForCity(this)
// ...
// Industry
class Industry is
method accept(Visitor v) is
v.doForIndustry(this)
// ...
although the node classes still has to be changed
- at least the change is trivial
- and lets us add further behaviors without altering the code again
by extracting a common interface for all visitors
- all existing nodes can work with any visitor introduced into the app
- when required to introduce a new behavior related to nodes, just need to implement a new visitor class
Analogy
- imagine a seasoned insurance agent who's eager to get new customers
- he can visit every building in a neighborhood
- and try to sell insurance to everyone he meets
- depending on the type of organization that occupies the building
- he can offer specialized insurance policies
- residential building, sells medical insurance
- bank, sells theft insurance
- coffee shop, sells fire and flood insurance
- he can offer specialized insurance policies
- he can visit every building in a neighborhood
Structure
The Visitor interface declares a set of visiting methods that can take concrete elements of an object structure as arguments.
- These methods may have the same names if the program is written in a language that supports overloading, but the type of their parameters must be different.
Each Concrete Visitor implements several versions of the same behaviors, tailored for different concrete element classes.
The Element interface declares a method for “accepting” visitors.
- This method should have one parameter declared with the type of the visitor interface.
Each Concrete Element must implement the acceptance method.
- The purpose of this method is to redirect the call to the proper visitor’s method corresponding to the current element class.
- Be aware that even if a base element class implements this method, all subclasses must still override this method in their own classes and call the appropriate method on the visitor object.
The Client usually represents a collection or some other complex object (for example, a Composite tree).
- Usually, clients aren’t aware of all the concrete element classes because they work with objects from that collection via some abstract interface.
When to use
- Use the Visitor when you need to perform an operation on all elements of a complex object structure (for example, an object tree)
- The Visitor pattern lets you execute an operation over a set of objects with different classes by having a visitor object implement several variants of the same operation, which correspond to all target classes
- Use the Visitor to clean up the business logic of auxiliary behaviors
- The pattern lets you make the primary classes of your app more focused on their main jobs by extracting all other behaviors into a set of visitor classes
- Use the pattern when a behavior makes sense only in some classes of a class hierarchy, but not in others
- You can extract this behavior into a separate visitor class and implement only those visiting methods that accept objects of relevant classes, leaving the rest empty
How to implement
- Declare the visitor interface with a set of “visiting” methods, one per each concrete element class that exists in the program
- Declare the element interface
- If you’re working with an existing element class hierarchy, add the abstract “acceptance” method to the base class of the hierarchy
- This method should accept a visitor object as an argument
- Implement the acceptance methods in all concrete element classes
- These methods must simply redirect the call to a visiting method on the incoming visitor object which matches the class of the current element
- The element classes should only work with visitors via the visitor interface
- Visitors, however, must be aware of all concrete element classes, referenced as parameter types of the visiting methods
- For each behavior that can’t be implemented inside the element hierarchy, create a new concrete visitor class and implement all of the visiting methods
- You might encounter a situation where the visitor will need access to some private members of the element class
- In this case, you can either make these fields or methods public, violating the element’s encapsulation, or nest the visitor class in the element class
- The latter is only possible if you’re lucky to work with a programming language that supports nested classes
- The client must create visitor objects and pass them into elements via “acceptance” methods
Pros & Cons
Pros
- Open/Closed Principle
- You can introduce a new behavior that can work with objects of different classes without changing these classes
- Single Responsibility Principle
- You can move multiple versions of the same behavior into the same class
- A visitor object can accumulate some useful information while working with various objects
- This might be handy when you want to traverse some complex object structure, such as an object tree, and apply the visitor to each object of this structure
Cons
- You need to update all visitors each time a class gets added to or removed from the element hierarchy
- Visitors might lack the necessary access to the private fields and methods of the elements that they’re supposed to work with
Example
// The component interface declares an 'accept' method that
// takes the base visitor interface as an argument.
interface Shape {
move(x, y);
draw();
accept(v: Visitor);
}
// Each concrete component class must implement the 'accept' method
class Dot implements Shape {
accept(v: Visitor) {
v.visitDot(this);
}
move(x, y) {}
draw() {}
}
class Circle implements Shape {
accept(v: Visitor) {
v.visitCircle(this);
}
move(x, y) {}
draw() {}
}
class Rectangle implements Shape {
accept(v: Visitor) {
v.visitRectangle(this);
}
move(x, y) {}
draw() {}
}
// The Visitor interface declares a set of visiting methods that
// correspond to component classes.
interface Visitor {
visitDot(d: Dot);
visitCircle(c: Circle);
visitRectangle(r: Rectangle);
}
class JSONExportVisitor implements Visitor {
visitDot(d: Dot) {
// Export the dot's ID and coordinates.
}
visitCircle(d: Circle) {
// Export the circle's ID, center coordinates and radius.
}
visitRectangle(d: Rectangle) {
// Export the rectangle's ID, left-top coordinates, width and height.
}
}
// The client code can run visitor operations over any set of
// elements without figuring out their concrete classes. The
// 'accept' operation directs a call to the appropriate operation
// in the visitor object.
const allShapes = [new Dot(), new Circle(), new Rectangle()];
const exportVisitor = new JSONExportVisitor();
allShapes.forEach((shape) => shape.accept(exportVisitor));