Visitor pattern
The Visitor pattern is a behavioral design pattern in object-oriented software design that represents an operation to be performed on elements of an object structure, allowing new operations to be defined without altering the classes of those elements.[1][2] It achieves this by separating the algorithm from the objects it operates on, using a visitor class to encapsulate the behavior and enabling the addition of new functionality through extension rather than modification.[3]
First formalized in the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—collectively known as the Gang of Four (GoF)—the Visitor pattern is one of 23 foundational design patterns classified under the behavioral category, which focuses on communication and responsibility assignment among objects.[4][5] The pattern leverages double dispatch, a mechanism where the specific operation executed is determined by the runtime types of both the visited element and the visitor itself, facilitating polymorphic behavior across class hierarchies.[3]
In practice, the Visitor pattern involves core participants: an abstract Visitor interface declaring a visit operation for each concrete element type; concrete Visitor implementations that define the actual logic for each element; an abstract Element with an accept method that takes a visitor and dispatches to the appropriate visit; concrete elements that implement accept to call visitor.visit(this); and an ObjectStructure (often a collection or composite) that maintains elements and iterates over them to apply the visitor.[2][1] This structure is particularly suited for scenarios involving stable element hierarchies with evolving operations, such as traversing abstract syntax trees in compilers, performing calculations on geometric shapes, or auditing employee records in HR systems.[3]
The pattern's key advantages include compliance with the open-closed principle, enabling new behaviors to be added without modifying existing classes; centralization of related operations in visitor subclasses; and the ability to accumulate side effects or state across multiple elements during traversal.[2][3] It promotes separation of concerns by keeping object representations clean and focused, while allowing algorithms to evolve independently.[3] However, drawbacks arise when element hierarchies change frequently, as each new element type requires updates to all visitor implementations, potentially leading to maintenance burdens and tight coupling.[3] Additionally, it can increase overall code complexity and may break encapsulation if visitors require access to private element details.[6]
The Visitor pattern often collaborates with structural patterns like Composite for handling recursive object structures and Iterator for traversal, making it a powerful tool in domains requiring extensible processing of heterogeneous object collections, though modern language features like pattern matching in some programming languages offer alternative approaches with reduced boilerplate.[1][7]
Introduction
Motivation and Problems Addressed
In object-oriented systems, a frequent design challenge occurs when an existing class hierarchy must accommodate new operations, such as traversals or analyses, without disrupting the established structure. Adding these operations directly to the classes often requires modifying each subclass in the hierarchy to implement the new method, which scatters the logic across multiple files and increases maintenance complexity. This practice contravenes the open-closed principle, articulated by Bertrand Meyer, which posits that software modules should be open for extension through inheritance or interfaces but closed for modification to existing code.[2][3]
Such issues are particularly evident in scenarios involving heterogeneous object collections, like processing an abstract syntax tree (AST) in a compiler or interpreter. Here, the tree comprises diverse node types—such as expressions, statements, or literals—each represented by subclasses of a base Node class. Introducing a new operation, for instance, code generation or semantic analysis, would necessitate altering every node subclass to handle the specific logic, even if most nodes share similar behaviors, thereby risking unintended side effects and violating encapsulation. The Visitor pattern motivates a solution by enabling these operations to operate on the structure without invasive changes to the node classes themselves.[8][9]
The pattern's historical context stems from the "Gang of Four" book, Design Patterns: Elements of Reusable Object-Oriented Software (Gamma et al., 1994), where it was formalized to address the limitations of single dispatch in languages like C++ for achieving polymorphic operations across class hierarchies. Central to this is the need for double dispatch, a mechanism that selects the appropriate behavior based on the dynamic types of both the visited object and the visitor, allowing algorithms to be extended independently of the element structure. This approach was motivated by real-world needs in domains like graphical user interfaces and compilers, where evolving requirements demand flexible behavioral extensions.[10][11]
Core Solution Overview
The Visitor pattern enables the definition of new operations over a collection of objects, known as an element structure, without altering the classes of those elements. This is accomplished by externalizing the operations into dedicated visitor classes, which encapsulate the behavior specific to each element type. The core mechanism relies on double dispatch: an element's accept method invokes the appropriate visit method on the visitor, with the dispatch determined by both the element's concrete type and the visitor's type, allowing type-specific processing without embedding the logic within the elements themselves.[2]
Conceptually, the pattern establishes two distinct hierarchies: the element hierarchy, which models the varying types of objects in the structure and their intrinsic behaviors, and the visitor hierarchy, which models the operations applicable to those elements. When traversing the structure, each element accepts a visitor, triggering the visitor to execute tailored actions based on the element's type—for instance, a shape-drawing visitor might render circles differently from rectangles. This decoupling ensures that changes to operations do not propagate to the element classes, preserving their stability even as new behaviors are introduced.[1][10]
By segregating operational logic from object representation, the Visitor pattern upholds the single responsibility principle, confining each class to a single, well-defined role: elements focus solely on their data and structure, while visitors handle the algorithms acting upon them. This approach facilitates extensibility, as adding a new operation involves creating a new visitor subclass rather than modifying multiple element classes, thereby reducing coupling and enhancing maintainability in systems with stable structures but evolving requirements.[2]
Definition and Intent
The Visitor pattern, as defined in the seminal work by the Gang of Four, has the formal intent: "Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates."[12]
This intent emphasizes separating the algorithm or operation from the object structure it traverses, allowing new behaviors to be added externally while preserving the integrity of the existing classes. In essence, the pattern encapsulates operations that may vary independently of the elements they act upon, promoting the open-closed principle by keeping element classes closed to modification yet open to extension through visitors.[12]
A key enabler of this pattern is the mechanism of double dispatch, where the operation's execution is determined not only by the visitor's type but also by the specific type of the element being visited. This dynamic resolution of behavior allows for type-specific implementations without altering the element hierarchy, effectively simulating multiple dispatch in languages that support only single dispatch.[12]
The pattern is particularly applicable under prerequisites where the classes representing elements in the structure have a stable interface and are unlikely to change, but the operations to be performed on them evolve frequently or come from external sources. This scenario avoids the proliferation of subclasses in the element hierarchy for each new operation, instead centralizing algorithmic variations in visitor implementations.
Applicability Conditions
The Visitor pattern is applicable when the structure of an object hierarchy remains stable, but the operations performed on its elements frequently evolve or require extension without altering the existing classes.[2][1] This scenario arises in systems where the core elements, such as nodes in a composite structure like a parse tree, do not change often, allowing new behaviors to be added through separate visitor implementations rather than subclassing the elements themselves.[2]
It is particularly suitable for situations involving many distinct operations across a set of related classes with varying interfaces, where each operation needs to behave differently based on the concrete type of element without embedding the logic directly into the element classes.[1] For instance, indicators include the need to traverse and process a heterogeneous collection—such as employees in an organizational chart with roles like engineer or manager—applying type-specific logic like salary calculations or reporting, while keeping the element hierarchy focused on its primary responsibilities.[1] This aligns with the pattern's intent to separate algorithms from the objects they operate on, enabling cleaner extension of behaviors.[2]
The pattern should be avoided if the element classes in the hierarchy change frequently, as adding or modifying an element would necessitate updating all existing visitor classes to handle the new type, leading to maintenance overhead.[2][1] Similarly, it is not ideal for shallow or simple hierarchies where the object structure lacks complexity, such as a flat list of uniform objects, since the overhead of defining accept methods and multiple visitor variants outweighs the benefits compared to simpler alternatives like direct polymorphism.[2] Additionally, if visitors require access to private members of elements but the public interface is insufficient, the pattern may violate encapsulation and prove impractical.[1]
Benefits and Limitations
Advantages
The Visitor pattern adheres to the open-closed principle by enabling the addition of new operations to an object structure without modifying the existing classes of the elements involved, thus promoting extensibility while preserving the stability of core structures.[13] This separation allows developers to introduce behaviors for diverse element types through dedicated visitor implementations, avoiding recompilation or alteration of the element hierarchy itself.
A primary benefit lies in centralizing related operations within a single visitor class, which organizes algorithms cohesively and reduces scattering of functionality across multiple element classes. This structure simplifies the traversal of complex object hierarchies, such as composites, by encapsulating traversal logic and type-specific actions in the visitor, making it easier to apply uniform or varied processing across elements.[14] Furthermore, visitors can accumulate state during visits—such as aggregating results or maintaining context across elements—which facilitates the implementation of intricate algorithms without embedding such logic directly into the elements.[14]
By isolating operations in visitor classes, the pattern enhances code maintainability and design simplicity, as evidenced by studies showing that Visitor-augmented designs are easier to comprehend compared to those without the pattern.[15] This isolation also improves testability, allowing individual visitors to be unit-tested independently of the underlying structure, thereby reducing coupling and simplifying verification of algorithmic behaviors.[16]
Disadvantages and Trade-offs
One significant drawback of the Visitor pattern is the introduction of a parallel hierarchy of visitor classes, which can lead to a proliferation of classes as new operations require additional concrete visitors.[17] Adding new concrete element classes to the object structure is particularly challenging, as it necessitates defining a new abstract operation in the Visitor interface and implementing it in every existing ConcreteVisitor class, thereby increasing maintenance overhead.[17]
The pattern creates tight coupling between the element hierarchy and the visitor hierarchy that can hinder extensibility if the element types change frequently. To enable visitors to perform their operations effectively, concrete elements may need to expose internal state through public methods, compromising encapsulation and potentially introducing security or consistency issues.[17]
In terms of trade-offs, the Visitor pattern imposes higher initial complexity due to the need for multiple classes and interfaces, but it offers long-term extensibility for adding operations without modifying existing elements, adhering to the open-closed principle—though this benefit diminishes if element changes are common. Performance considerations arise from the double-dispatch mechanism, which involves twofold method resolution (first on the element's accept method, then on the visitor's visit method), potentially introducing overhead in large object structures through additional indirections and runtime checks.[18]
Structural Components
Class Relationships
The Visitor pattern structures its architecture around five primary abstract roles, each contributing to the separation of algorithms from the objects they operate on. The Visitor role defines an abstract interface that declares a visit operation for each type of concrete element in the object structure, allowing the visitor to process different element types without modifying the elements themselves.[12] ConcreteVisitor classes implement this interface, providing specific logic tailored to individual element types, thereby encapsulating type-specific behaviors in dedicated subclasses.[12]
The Element role, in contrast, represents the objects being visited and declares an abstract accept method that takes a Visitor as a parameter, enabling elements to collaborate with visitors without embedding the operational logic. ConcreteElement classes implement this accept method by invoking the appropriate visit method on the passed visitor, passing themselves as arguments to dispatch the correct behavior.[12] Additionally, the ObjectStructure role manages a collection or composite of Element instances and provides a method to apply a Visitor to all contained elements, maintaining the integrity of the structure while facilitating traversal.[12]
These roles interconnect through directed relationships that promote loose coupling and polymorphism. Elements reference visitors via their accept methods, which receive a Visitor instance and redirect control to it, while visitors reference elements through parameterized visit methods that handle specific ConcreteElement types.[12] This bidirectional referencing enables double dispatch, a mechanism where the runtime types of both the element and the visitor determine the executed method: the accept call dispatches based on the element's type, and the subsequent visit call dispatches based on the visitor's type, allowing dynamic selection of operations without altering the element hierarchy.[12]
UML Diagrams
The UML class diagram for the Visitor pattern depicts a structure that separates algorithms from the object hierarchies they operate on, enabling double dispatch for type-specific behavior. At its core, it features an abstract Visitor interface or class containing abstract methods such as visitConcreteElementA() and visitConcreteElementB(), one for each type of concrete element in the target hierarchy. Concrete Visitor subclasses, like ConcreteVisitor1 and ConcreteVisitor2, implement these methods to define specific operations tailored to each element type. Complementing this, an abstract Element class or interface declares an accept(Visitor v) method, which elements use to receive visitors. Concrete Element subclasses, such as ConcreteElementA and ConcreteElementB, implement accept by invoking the corresponding visit method on the passed visitor, passing themselves as an argument (e.g., v.visitConcreteElementA(this)), thus enabling polymorphic dispatch. Arrows in the diagram illustrate inheritance from Visitor to concrete visitors and from Element to concrete elements, with dependency associations showing the bidirectional interaction between visitors and elements via the visit and accept operations.[2]
A typical UML sequence diagram for the Visitor pattern illustrates the runtime dynamics of applying an operation across an object structure. The client first instantiates a ConcreteVisitor and obtains a reference to an Element or ObjectStructure containing elements. The client then invokes accept(visitor) on the top-level element or structure. This triggers a chain: the Element (or each child element in the structure) calls visitor.visitConcreteElementX(this) within its accept implementation, dispatching control to the appropriate method in the ConcreteVisitor based on the element's concrete type. The visitor performs its operation on the element, potentially returning a result, before control flows back to the client. This double-dispatch mechanism—first via accept polymorphism on the element, then via visit polymorphism on the visitor—ensures type-safe, extensible behavior without modifying element classes.[1]
Variations in UML representations account for specific use cases, such as composite structures. An optional ObjectStructure class may appear in the class diagram as a container holding a collection of Element instances, with its own accept(Visitor v) method that iterates over the collection and calls accept(v) on each child, facilitating traversal of hierarchical or aggregate objects. Additionally, diagrams often highlight support for multiple concrete visitors by showing parallel inheritance hierarchies, where each visitor type provides a distinct set of operations on the shared element hierarchy, promoting modularity for evolving algorithms.[2][1]
Implementation Mechanics
Visitor Role and Methods
The Visitor role in the Visitor design pattern encapsulates operations to be performed on elements of an object structure, allowing these algorithms to be defined separately from the elements themselves. This separation enables the addition of new operations without altering the classes of the elements on which they operate.[2]
The Visitor interface, typically implemented as an abstract class or interface, declares one or more visit methods, each corresponding to a specific concrete element type in the structure. For instance, in a drawing application with elements like circles and dots, the interface might include visitCircle([Circle](/page/Circle) circle) and visitDot([Dot](/page/Dot) dot), where each method receives an instance of the respective element as a parameter. These method signatures ensure type-safe dispatching to the appropriate logic based on the element's class.[2]
Concrete Visitors are classes that implement the Visitor interface, providing tailored implementations for each visit method to execute element-specific operations. These implementations often involve processing the element's data—such as computing properties or transforming representations—and may maintain internal state, like accumulated results from traversing multiple elements. For example, a concrete visitor for XML export would serialize a circle's radius and position into XML tags within its visitCircle method, while another for SVG export might generate vector graphics code.[2]
By centralizing algorithms within Visitors, the pattern promotes the open-closed principle, where element classes remain closed to modification but open to extension through new Visitors. This approach is particularly useful when the element hierarchy is stable but the set of required operations evolves frequently.[2]
Element Role and Acceptance
In the Visitor design pattern, the Element interface defines a core method named accept that accepts a Visitor object as its parameter, serving as the entry point for the pattern's double dispatch mechanism. This declaration ensures that all elements expose a consistent way to receive and delegate to external operations without embedding the logic of those operations within the elements themselves.
Concrete Element classes implement the accept method by calling the corresponding visit method on the passed Visitor instance, forwarding a reference to themselves (e.g., v.visit(this) in languages like Java or C++). This implementation achieves double dispatch: the first dispatch occurs via the accept call based on the element's type, and the second via the visit call based on the visitor's type, allowing type-specific behavior to be executed precisely.[2]
The Element's role is to maintain a stable, extensible interface that shields its internal structure from visitor details, enabling clients to apply new operations dynamically across an object hierarchy without modifying the elements. By centralizing access through accept, elements facilitate uniform traversal and operation application while preserving the open-closed principle for extensions.
Client Interaction
In the Visitor pattern, the client assumes primary responsibility for instantiating concrete visitor objects tailored to specific operations and for integrating them with the target object structure. This involves first acquiring access to the structure—typically a collection of individual elements or a composite hierarchy—and then systematically invoking the accept method on each element, passing the visitor as an argument. This process ensures that the visitor's relevant visit method is dispatched for the element's concrete type, executing the desired behavior without altering the element classes themselves.[2]
A typical client workflow begins with creating a concrete visitor, followed by obtaining the object structure, such as through a reference to a root composite or an iterator over elements. The client then iterates over the structure, calling accept([visitor](/page/Visitor)) on each element in sequence; for non-composite elements, this directly triggers the corresponding visit operation, while for composites, it propagates recursively to children via their own accept calls. This recursive flow allows the client to apply the visitor across the entire hierarchy with minimal explicit traversal logic, as the structure itself handles the recursion.[1][2]
Clients benefit from the pattern's flexibility in supporting multiple visitors, enabling the reuse of the same object structure for diverse operations. By instantiating different concrete visitors—such as one for computation and another for reporting—and applying them sequentially or conditionally, the client can perform varied traversals without restructuring the elements or duplicating code across classes. This separation keeps client code focused on orchestration rather than implementation details.[1]
Practical Applications
Traversing Composite Structures
The Visitor pattern finds significant application in traversing composite structures, such as those modeled by the Composite design pattern, where an object structure maintains a collection of heterogeneous elements forming a hierarchy. In this context, the ObjectStructure—often a container class like a tree root or graph—holds references to Element instances and provides a method to apply a Visitor recursively. When the client invokes the traversal, the ObjectStructure iterates over its components and calls the accept method on each, which dispatches to the corresponding visit method in the Visitor. This mechanism ensures that operations propagate through the entire hierarchy without embedding navigation logic directly into the Element classes.[2]
A prominent example of this application occurs in compiler design, where Abstract Syntax Trees (ASTs) represent hierarchical code structures as composite objects. Visitors enable operations like computing subtree sizes or validating node semantics by recursively traversing the tree. For instance, a SizeVisitor implements a visit method for each node type (e.g., binary operations or literals), where it increments a counter for the current node and then invokes accept on its children to accumulate their sizes, yielding the total tree depth or node count. Similarly, a ValidationVisitor can check type compatibility across nodes, reporting errors during the downward pass. This recursive accept-invocation pattern, rooted in double dispatch, allows multiple analysis passes without altering the AST node implementations.[19][20]
The primary benefits of using Visitor for iteration in composites lie in its separation of concerns: traversal and processing logic remain centralized in the Visitor subclasses, preventing pollution of the Element hierarchy with algorithm-specific code. This centralization facilitates the addition of new traversal strategies—such as pre-order, post-order, or selective depth-limited walks—while preserving the integrity of the structural classes, which adhere solely to their domain responsibilities. As articulated in the foundational design patterns literature, this approach supports accumulating state across the structure, like partial results in computations, enhancing maintainability in complex hierarchies. Clients interact by instantiating a specific Visitor and passing it to the ObjectStructure for application, leveraging the pattern's extensibility for diverse processing needs.[21]
Extending Operations Dynamically
The Visitor pattern facilitates the addition of new operations to established class hierarchies without requiring modifications to the existing classes, a key advantage when dealing with legacy codebases or third-party libraries where source access is unavailable or undesirable. This separation of concerns adheres to the open-closed principle, allowing behaviors to evolve independently of the structure they operate on.[2]
In integration scenarios with third-party libraries, the pattern shines by enabling developers to create visitor implementations that define operations tailored to the library's element classes. For example, if a library provides a fixed set of domain objects like accounts or transactions in a financial system, a visitor can be introduced to perform audits, export data to new formats, or apply compliance checks externally, bypassing the need to alter the library's codebase. This approach ensures compatibility and reduces maintenance overhead, as updates to the library do not invalidate the visitor logic.[2]
A common application occurs in graphical user interface (GUI) frameworks, where hierarchies of components—such as buttons, panels, and text fields—represent the element structure. Developers can extend these without source changes by implementing visitors for operations like theme-based rendering or form validation; for instance, a rendering visitor might apply different styles to each component type during traversal, while a validation visitor checks constraints specific to inputs or layouts. This extensibility supports evolving UI requirements in stable frameworks.[6]
The pattern's design also accommodates dynamic extension by allowing new visitor implementations to be added, promoting pluggable behaviors in modular systems. Multiple visitor classes can be instantiated and applied selectively based on context, such as user configuration or environmental needs.[2]
Code Examples
Java Implementation
The Java implementation of the Visitor pattern utilizes interfaces to define the roles of elements and visitors, enabling the separation of algorithms from the object structure while supporting extensibility through new visitor classes. This example employs a hierarchy of geometric shapes to compute aggregate properties like total area and perimeter, demonstrating how the pattern facilitates double dispatch in a statically typed language like Java. The code is structured around the Shape interface for elements, the Visitor interface for operations, concrete shape classes that implement acceptance, specialized visitor classes for calculations, and client code that applies visitors to a collection of shapes.
Shape Interface
The [Shape](/page/Shape) interface declares the accept method, which each concrete element must implement to receive a visitor.
java
public interface Shape {
void accept(Visitor visitor);
}
public interface Shape {
void accept(Visitor visitor);
}
Visitor Interface
The Visitor interface defines a visit method for each concrete element type, allowing visitors to perform type-specific operations.
java
public interface Visitor {
void visitCircle(Circle circle);
void visitRectangle(Rectangle rectangle);
}
public interface Visitor {
void visitCircle(Circle circle);
void visitRectangle(Rectangle rectangle);
}
Concrete Element Classes
Concrete shapes like Circle and Rectangle implement the [Shape](/page/Shape) interface, storing their geometric properties and delegating to the appropriate visit method via accept.
java
public class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
@Override
public void accept(Visitor visitor) {
visitor.visitCircle(this);
}
}
public class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
@Override
public void accept(Visitor visitor) {
visitor.visitCircle(this);
}
}
java
public class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public void accept(Visitor visitor) {
visitor.visitRectangle(this);
}
}
public class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public void accept(Visitor visitor) {
visitor.visitRectangle(this);
}
}
Concrete Visitor Classes
Visitors like AreaVisitor and PerimeterVisitor implement the Visitor interface, accumulating results across elements by overriding the visit methods with type-specific computations using Java's Math library for precision.
java
public class AreaVisitor implements Visitor {
private double totalArea = 0.0;
@Override
public void visitCircle(Circle circle) {
totalArea += Math.PI * circle.getRadius() * circle.getRadius();
}
@Override
public void visitRectangle(Rectangle rectangle) {
totalArea += rectangle.getWidth() * rectangle.getHeight();
}
public double getTotalArea() {
return totalArea;
}
}
public class AreaVisitor implements Visitor {
private double totalArea = 0.0;
@Override
public void visitCircle(Circle circle) {
totalArea += Math.PI * circle.getRadius() * circle.getRadius();
}
@Override
public void visitRectangle(Rectangle rectangle) {
totalArea += rectangle.getWidth() * rectangle.getHeight();
}
public double getTotalArea() {
return totalArea;
}
}
java
public class PerimeterVisitor implements Visitor {
private double totalPerimeter = 0.0;
@Override
public void visitCircle(Circle circle) {
totalPerimeter += 2 * Math.PI * circle.getRadius();
}
@Override
public void visitRectangle(Rectangle rectangle) {
totalPerimeter += 2 * (rectangle.getWidth() + rectangle.getHeight());
}
public double getTotalPerimeter() {
return totalPerimeter;
}
}
public class PerimeterVisitor implements Visitor {
private double totalPerimeter = 0.0;
@Override
public void visitCircle(Circle circle) {
totalPerimeter += 2 * Math.PI * circle.getRadius();
}
@Override
public void visitRectangle(Rectangle rectangle) {
totalPerimeter += 2 * (rectangle.getWidth() + rectangle.getHeight());
}
public double getTotalPerimeter() {
return totalPerimeter;
}
}
Client Code
The client creates a list of shapes, instantiates a visitor, and iterates over the elements by calling accept, which triggers the double dispatch mechanism to invoke the correct visit method for each shape's runtime type.
java
import java.util.List;
import java.util.Arrays;
public class Client {
public static void main(String[] args) {
List<Shape> shapes = Arrays.asList(
new Circle(5.0),
new Rectangle(4.0, 6.0)
);
// Compute total area
AreaVisitor areaVisitor = new AreaVisitor();
for (Shape shape : shapes) {
shape.accept(areaVisitor);
}
System.out.println("Total area: " + areaVisitor.getTotalArea());
// Compute total perimeter
PerimeterVisitor perimeterVisitor = new PerimeterVisitor();
for (Shape shape : shapes) {
shape.accept(perimeterVisitor);
}
System.out.println("Total perimeter: " + perimeterVisitor.getTotalPerimeter());
}
}
import java.util.List;
import java.util.Arrays;
public class Client {
public static void main(String[] args) {
List<Shape> shapes = Arrays.asList(
new Circle(5.0),
new Rectangle(4.0, 6.0)
);
// Compute total area
AreaVisitor areaVisitor = new AreaVisitor();
for (Shape shape : shapes) {
shape.accept(areaVisitor);
}
System.out.println("Total area: " + areaVisitor.getTotalArea());
// Compute total perimeter
PerimeterVisitor perimeterVisitor = new PerimeterVisitor();
for (Shape shape : shapes) {
shape.accept(perimeterVisitor);
}
System.out.println("Total perimeter: " + perimeterVisitor.getTotalPerimeter());
}
}
When executed, the client outputs the total area (approximately 78.54 for the circle plus 24.0 for the rectangle, yielding 102.54) and total perimeter (approximately 31.416 for the circle plus 20.0 for the rectangle, yielding 51.416), confirming the visitors' accumulations. This illustrates double dispatch in Java: the virtual method call to accept resolves to the concrete shape's implementation based on its runtime type, which then invokes the correspondingly overloaded visit method on the visitor, enabling polymorphic behavior across both hierarchies without altering the shape classes.
Python Implementation
In Python, the Visitor pattern is implemented using the language's dynamic typing and duck typing, which obviate the need for explicit interface declarations found in statically typed languages like Java. Instead, classes adhere to the pattern through method presence, enabling runtime polymorphism via the accept method on elements and corresponding visit methods on visitors. This approach facilitates double dispatch, where the operation executed depends on both the element type and the visitor type, without modifying the element classes.
To illustrate, consider a file system hierarchy with File and Directory as concrete elements deriving from an abstract Node base class. The Node defines an abstract accept method that delegates to the visitor. Concrete elements implement accept by invoking the appropriate visit method on the provided visitor, passing self as the argument.
python
from abc import ABC, abstractmethod
from typing import Any
class Node(ABC):
@abstractmethod
def accept(self, visitor: 'Visitor') -> Any:
pass
class File(Node):
def __init__(self, name: str, size: int):
self.name = name
self.size = size
def accept(self, visitor: 'Visitor') -> Any:
return visitor.visit_file(self)
class Directory(Node):
def __init__(self, name: str, children: list[Node] = None):
self.name = name
self.children = children or []
def accept(self, visitor: 'Visitor') -> Any:
return visitor.visit_directory(self)
from abc import ABC, abstractmethod
from typing import Any
class Node(ABC):
@abstractmethod
def accept(self, visitor: 'Visitor') -> Any:
pass
class File(Node):
def __init__(self, name: str, size: int):
self.name = name
self.size = size
def accept(self, visitor: 'Visitor') -> Any:
return visitor.visit_file(self)
class Directory(Node):
def __init__(self, name: str, children: list[Node] = None):
self.name = name
self.children = children or []
def accept(self, visitor: 'Visitor') -> Any:
return visitor.visit_directory(self)
The Visitor is defined as an abstract base class with specific visit methods for each element type. Concrete visitors, such as SizeVisitor for computing total sizes and TypeVisitor for identifying node types, implement these methods. Notably, for composite structures like directories, the visitor handles traversal recursively within its visit_directory method, keeping the algorithm encapsulated in the visitor rather than the elements. This aligns with the pattern's goal of separating operations from the object structure.
python
class Visitor(ABC):
@abstractmethod
def visit_file(self, file: File) -> Any:
pass
@abstractmethod
def visit_directory(self, directory: Directory) -> Any:
pass
class SizeVisitor(Visitor):
def visit_file(self, file: File) -> int:
return file.size
def visit_directory(self, directory: Directory) -> int:
return sum(child.accept(self) for child in directory.children)
class TypeVisitor(Visitor):
def visit_file(self, file: File) -> str:
return "File"
def visit_directory(self, directory: Directory) -> str:
return "Directory"
class Visitor(ABC):
@abstractmethod
def visit_file(self, file: File) -> Any:
pass
@abstractmethod
def visit_directory(self, directory: Directory) -> Any:
pass
class SizeVisitor(Visitor):
def visit_file(self, file: File) -> int:
return file.size
def visit_directory(self, directory: Directory) -> int:
return sum(child.accept(self) for child in directory.children)
class TypeVisitor(Visitor):
def visit_file(self, file: File) -> str:
return "File"
def visit_directory(self, directory: Directory) -> str:
return "Directory"
Client code constructs the structure and applies visitors to perform operations. For instance, building a directory tree and using SizeVisitor computes the aggregate size by recursing through the hierarchy via the visitors' dispatch.
python
# Client usage example
root = Directory("root", [
File("doc.txt", 100),
Directory("docs", [
File("guide.pdf", 200),
File("notes.txt", 50)
])
])
size_visitor = SizeVisitor()
total_size = root.accept(size_visitor)
print(f"Total size: {total_size}") # Output: Total size: 350
type_visitor = TypeVisitor()
node_type = root.accept(type_visitor)
print(f"Root type: {node_type}") # Output: Root type: Directory
# Client usage example
root = Directory("root", [
File("doc.txt", 100),
Directory("docs", [
File("guide.pdf", 200),
File("notes.txt", 50)
])
])
size_visitor = SizeVisitor()
total_size = root.accept(size_visitor)
print(f"Total size: {total_size}") # Output: Total size: 350
type_visitor = TypeVisitor()
node_type = root.accept(type_visitor)
print(f"Root type: {node_type}") # Output: Root type: Directory
This yields 350 for the total size, as the SizeVisitor sums file sizes across the tree. The TypeVisitor simply returns the node type without traversal. Python's flexibility shines in adaptations like using getattr for dynamic dispatch in the accept method, allowing visits to unregistered types at runtime:
python
def accept(self, visitor: Visitor) -> Any:
method_name = f"visit_{type(self).__name__.lower()}"
if hasattr(visitor, method_name):
return getattr(visitor, method_name)(self)
raise AttributeError(f"No {method_name} method in visitor")
def accept(self, visitor: Visitor) -> Any:
method_name = f"visit_{type(self).__name__.lower()}"
if hasattr(visitor, method_name):
return getattr(visitor, method_name)(self)
raise AttributeError(f"No {method_name} method in visitor")
Such a generic accept leverages duck typing to invoke the correct method based on the element's type name, enhancing extensibility without predefined visit methods for every possible element— a feature less straightforward in stricter languages like Java, where explicit method overrides are required.
Comparisons with Iterator Pattern
The Iterator pattern provides a mechanism for accessing the elements of an aggregate object sequentially without exposing its underlying representation, thereby externalizing the traversal logic from the aggregate itself.[22] This approach ensures that clients can traverse collections uniformly, regardless of their internal structure, promoting encapsulation and flexibility in iteration algorithms.[22]
In comparison, the Visitor pattern emphasizes the separation of algorithms from the object structures they operate on, enabling the addition of new operations to elements without altering their classes.[2] While the Iterator is traversal-oriented and agnostic to the specific operations performed on elements, the Visitor integrates operations directly into the visitation process, often using double dispatch to handle type-specific behaviors in heterogeneous structures.[2] Both patterns externalize logic—Iterator for access mechanisms and Visitor for behavioral extensions—but the Iterator suits uniform sequential processing, whereas the Visitor excels in applying diverse, structure-aware computations.[2]
The patterns can be effectively combined by employing an Iterator to traverse a composite structure while applying a Visitor to execute targeted computations on each visited element, as seen in applications like processing tree-like data where traversal and operation need decoupling.[2] Historically, both the Iterator and Visitor patterns were introduced in the seminal Gang of Four (GoF) book on design patterns, with the Iterator addressing uniform access to aggregates and the Visitor tailored to heterogeneous operations on stable class hierarchies.
Comparisons with Strategy Pattern
The Strategy pattern defines a family of interchangeable algorithms, each encapsulated within its own class, enabling runtime selection and application to objects of a single type without altering the object's class.[23] In this pattern, a context object delegates the execution to a chosen strategy object through a common interface, promoting flexibility in algorithm variation while adhering to the Open/Closed Principle.[23]
In comparison, the Visitor pattern addresses the orthogonal concern of applying a unified set of operations across multiple types in an object hierarchy, without modifying the element classes themselves.[2] Whereas Strategy employs single dispatch to vary algorithms for one object type, Visitor utilizes double dispatch—where both the visitor and the element determine the executed method—allowing operations to be tailored to diverse element types within a structure.[24] This distinction arises from their intents: Strategy focuses on algorithmic polymorphism for a fixed context, while Visitor emphasizes operational polymorphism over a varying structure.[24]
Use cases further illustrate these differences. The Strategy pattern suits scenarios like implementing multiple sorting variants (e.g., quicksort or mergesort) on a single collection type, where the algorithm selection occurs at runtime.[23] Conversely, the Visitor pattern excels in multi-class processing, such as generating reports (e.g., financial summaries or audit logs) that traverse and operate differently on hierarchies like employee or shape objects.[2]
Both patterns enable extensibility by externalizing behavior, but overlaps are limited; Strategy offers a simpler alternative for non-hierarchical needs where varying the context is unnecessary, avoiding the added complexity of Visitor's double dispatch and structure traversal.[24]