Behavioral Patterns
Visitor
Separate algorithms from the objects on which they operate. Visitor lets you add new operations to existing class structures without modifying them.
The Visitor Pattern is a behavioral design pattern that allows you to separate algorithms from the objects on which they operate. By establishing a double dispatch mechanism, it lets you add new operations to a heterogeneous class hierarchy without modifying the source code of the classes themselves.
1. Learning Objectives
- Identify when to apply the Visitor pattern to decouple operations from complex class structures.
- Understand the mechanics of Double Dispatch and how it resolves concrete class types at runtime.
- Analyze the compile-time dependencies introduced by Visitor interfaces.
- Compare the structure and intent of Visitor, Composite, and Strategy patterns.
- Implement dynamic method dispatch visitors in Python (overcoming the lack of method overloading) and C++.
2. Problem & Naive Solution
Suppose you are building an e-commerce checkout engine. The system contains an object list representing a shopping cart: Book, Electronics, and Clothing. You need to calculate checkout metrics on these items:
- Calculate Sales Tax (varies by product category).
- Calculate Shipping Cost (depends on product weight and size).
- Calculate Loyalty Points earned.
The Naive Solution
A developer might implement these operations directly inside each product class:
This design introduces significant design flaws:
- Violates Single Responsibility Principle: The core domain models (
Electronics,Book) are bloated with tax rules, shipping formulas, and discount schemes that are unrelated to basic product properties. - Violates Open/Closed Principle: Every time a new operation is needed (e.g. export to XML, generate discount invoice), you must modify the source code of all product classes.
- Frequent Recompilations: Changes to tax regulations force recompilation of the entire product hierarchy, risking regressions in core classes.
3. Issues
Directly embedding algorithms inside class structures makes maintenance difficult. If the data structure (product catalog) is stable but the operations (tax calculations, reports) change frequently, subclassing fails, leading to duplicate code and tight coupling.
4. Pattern Introduction & UML
The Visitor Pattern solves this by extracting the operations into separate visitor classes.
The products (Elements) implement an ItemElement interface exposing an accept(Visitor) method.
When a visitor visits, the element calls visitor.visit(this).
This call triggers Double Dispatch, delegating execution to the appropriate overloaded method on the visitor based on the element's concrete type.
UML: Tax & Shipping Visitor
5. Participants
- Element (
ItemElement): Interface declaring theaccept(Visitor)method. - Concrete Element (
Book,Electronics): Implementsaccept(), calling the visitor back (visitor.visit(this)). - Visitor (
Visitor): Interface declaring overloadedvisit()methods for each concrete element class in the hierarchy. - Concrete Visitor (
TaxVisitor,ShippingWeightVisitor): Implements operations on the elements. - Object Structure (
ShoppingCart): Maintains the collection of elements and iterates over them, letting the visitor visit each.
6. Theory (Double Dispatch Mechanism)
To understand Visitor, you must understand Double Dispatch:
- Single Dispatch (Standard): Most object-oriented languages (like Java and C++) resolve method calls based on the dynamic type of the receiver object (dynamic polymorphism). Parameter types are resolved statically at compile-time.
- Double Dispatch: Bypasses static parameter binding.
1. First Dispatch: Client calls
element.accept(visitor). Dynamic dispatch resolves which element'saccept()method to call. 2. Second Dispatch: Insideaccept(), the element callsvisitor.visit(this). Sincethishas a concrete type known at compile-time within that class, it dispatches execution to the correct overloadedvisit()method on the visitor.
7. Syntax Explanation
Syntax construction details:
- Java: Uses method overloading (multiple
visit()methods with different parameter types) to support clean double dispatch. - Python: Lacks method overloading. You must use reflection (e.g. looking up method names dynamically:
getattr(self, f"visit_{type(element).__name__.lower()}")(element)) to route execution. - C++: Requires forward declaring concrete element classes in the visitor header file to resolve compiler dependencies.
8. Step-by-Step Implementation
- Step 1: Create the
Visitorinterface defining overloadedvisit()methods for each element subclass. - Step 2: Create the
ItemElementinterface declaringaccept(Visitor). - Step 3: Implement
accept(Visitor)inside the concrete element classes, callingvisitor.visit(this). - Step 4: Build concrete visitor classes implementing the
Visitorinterface to calculate specific metrics. - Step 5: Write the client code to iterate over the elements and pass the visitor to each.
9. Complete Code (Mini Project)
10. Code Walkthrough
Let's review the double dispatch execution path:
- First Dispatch: The client calls
item.accept(taxVisitor)polymorphically. Dynamic dispatch resolves which element'saccept()method to call (e.g.Book.accept()). - Second Dispatch: Inside
Book.accept(), the class executesvisitor.visit(this). Sincethishas the concrete typeBookknown at compile-time within that class, it routes execution to the correct overloadedvisit(Book)method on the visitor. - Decoupled Algorithms: Calculations are isolated inside visitor subclasses (
TaxVisitor,ShippingVisitor). Adding a new calculation doesn't require modifying the product classes.
11. Execution Flow
- Accept Call: Client calls
accept(visitor)on an element. - Type Resolution: Dynamic dispatch resolves which element's
accept()method is called. - Visit Call: The element calls
visitor.visit(this). - Double Dispatch: The compiler matches the overloaded method, executing the concrete visitor logic.
- Return Value: The calculation result is returned to the client.
12. Internal Working (JVM Dispatch & Memory Layout)
How the JVM resolves double dispatch at runtime:
- Dynamic Method Dispatch table (vtable): The first dispatch (
item.accept()) performs a runtime lookup in the class's Virtual Method Table (vtable) to find the correct method address. - Static Overload Resolution: The second dispatch (
visitor.visit(this)) is resolved statically by the compiler because the type ofthisis explicitly known inside each element class. This avoids a second vtable lookup, keeping execution fast.
13. Complexity Analysis
- Time Complexity: $O(1)$ constant time overhead to resolve the double dispatch.
- Space Complexity: $O(1)$ constant memory overhead to hold base class reference pointers.
14. Best Practices
- Use when Elements are Stable: Only apply Visitor when the class hierarchy is stable. If new product types (e.g.
Grocery) are added frequently, you must update theVisitorinterface and all its subclasses, defeating the pattern's benefit. - Keep Visitors Stateless: Avoid storing state inside visitor classes. Instead, pass calculation results back using return types or accumulator parameters.
15. Common Mistakes
- Skipping Double Dispatch: Using
instanceofchecks inside visitors instead of implementingaccept()loops, which breaks runtime polymorphism. - Applying to Unstable Hierarchies: Using Visitor in rapidly changing class hierarchies, causing massive refactoring work whenever a new class is added.
16. Framework Usage
- Abstract Syntax Tree (AST) Parsers: Compilers (like javac, python compiler, Babel) use Visitor to traverse AST node structures, applying optimizations, type checks, and code generation steps polymorphically.
- Static Analysis Tools: Code linters (like ESLint, Checkstyle) parse code structures into trees and use visitors to identify security vulnerabilities or formatting issues.
17. Interview Discussion
Answer: Because it resolves the method to execute using two lookups: 1. Resolves which concrete element's
accept(Visitor) method to execute.
2. Resolves which overloaded visit(ConcreteElement) method to call on the visitor based on the element's type.
Answer: By using reflection to look up method names dynamically at runtime based on the class name of the element (e.g. mapping elements to method names like
visit_book).
Answer: Adding a new concrete element class forces you to update the
Visitor interface and modify all concrete visitor implementations, creating significant maintenance overhead in unstable hierarchies.
18. Practice Exercises
- Easy: Write a Python program containing a visitor that prints elements as formatted strings.
- Medium: Design a
Shapehierarchy (Circle, Rectangle) with aDrawingVisitorrendering shapes on different graphics backends. - Hard: Build a file system representations explorer. Elements are files and directories. Implement visitors that print directory trees and calculate total file sizes.
19. Challenge Problem
Design an Abstract Syntax Tree (AST) Math Equation Compiler. The expression compiler represents equation elements in a tree: NumberNode, AdditionNode, and MultiplicationNode. You must implement visitors to perform two tasks: EvaluationVisitor (computes the final numeric value) and LaTeXGeneratorVisitor (compiles the node tree into LaTeX markup format strings). Write this system in Java, Python, or C++ and test it with nested expressions.
20. Summary & Cheat Sheet
- Visitor separates algorithms from the objects on which they operate.
- Enables double dispatch: dynamic dispatch on the element type and static overloading on the visitor.
- Only use Visitor when the element class hierarchy is stable.
- Avoid using
instanceofchecks inside visitors to preserve polymorphism.
21. Quiz
1. What is the primary purpose of the Visitor design pattern?
A) To adapt incompatible interfaces
B) To separate algorithms from the objects on which they operate, allowing new operations to be added without modifying the classes (Correct)
C) To control object instantiation pools
2. What is Double Dispatch?
A) Running execution on two threads concurrently
B) A dynamic lookup resolving both the receiver class type and the visitor parameter type at runtime (Correct)
C) Compiling classes using two separate compilers
3. Why is the second dispatch on the visitor method resolved statically in Java/C++?
A) To bypass class encapsulation rules
B) Because the concrete element class is explicitly known within its own accept() method (Correct)
C) To prevent compiler warnings
4. What is a major drawback of using the Visitor pattern?
A) It triggers stack overflow errors
B) Adding a new element class forces you to update the Visitor interface and all its subclasses (Correct)
C) It disables virtual table dispatch lookups
5. Which of the following is a classic example of the Visitor pattern in compilation tools?
A) Abstract Syntax Tree (AST) code checkers and compilers (Correct)
B) Database connection pools
C) Stream readers
6. How does Python support Visitor without method overloading?
A) By using multiple inheritance declarations
B) By using reflection to resolve method names dynamically based on the element class name (Correct)
C) By compiling code using Java compilers
7. What is the time complexity of executing a visitor calculation on a single element?
A) $O(\log N)$
B) $O(1)$ (Correct)
C) $O(N)$
8. When should you avoid using the Visitor pattern?
A) When the class hierarchy changes frequently (Correct)
B) When the operations change frequently
C) When running on multi-threaded CPUs
9. In C++, why are concrete element classes forward-declared in the visitor header?
A) To save memory allocation
B) To resolve compiler dependencies because elements reference visitors and visitors reference concrete elements (Correct)
C) To enable thread synchronization
10. Does the Visitor pattern support the Open/Closed Principle?
A) Yes, because you can introduce new visitors (operations) without modifying the element classes (Correct)
B) No, because adding new visitors forces rewriting all element classes
C) Only when using AST parsers
22. Next Lesson Preview
In the next lesson, we will explore the Mediator Pattern. We will learn how to reduce chaotic dependencies between classes by forcing them to communicate solely through a central mediator object!
Related Topics
- StrategyDefine a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients.
- IteratorAccess elements of an aggregate object sequentially without exposing its underlying representation (list, stack, tree, graph).
- ObserverDefine a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.