How to Implement the Visitor Pattern Without Violating Encapsulation in Java
You can implement the Visitor pattern in Java without breaking encapsulation by keeping element fields private, exposing only the accept() method and domain-specific operations, and ensuring visitors interact with elements through a stable interface rather than raw state getters.
The Visitor pattern enables you to add new operations to object structures without modifying the elements themselves. However, classic implementations often violate encapsulation by forcing elements to expose internal state through public getters. The iluwatar/java-design-patterns repository demonstrates a clean alternative that maintains strong encapsulation while preserving the pattern's extensibility benefits.
Why Standard Visitor Implementations Break Encapsulation
The classic Visitor pattern relies on double-dispatch: an element's accept(Visitor v) method calls back to v.visit(this). While this decouples operations from elements, it creates a temptation to add getter methods that expose private fields so visitors can read internal state.
Consider this encapsulation-breaking approach:
class Soldier extends Unit {
private int ammo;
public int getAmmo() { return ammo; } // Exposes internal state
}
class AmmoVisitor implements UnitVisitor {
@Override
public void visit(Soldier s) {
System.out.println("Ammo: " + s.getAmmo()); // Violates encapsulation
}
}
Once visitors depend on raw getters like getAmmo(), the internal representation becomes locked into the public API. Refactoring the field type or name breaks all visitors, creating maintenance debt.
Encapsulation-Preserving Strategies from java-design-patterns
The army-unit hierarchy implementation in the repository shows how to maintain private state while supporting visitor operations. The key is limiting the surface area between elements and visitors to a controlled, semantic API.
Hide Internal State Behind the accept() Method
In src/main/java/com/iluwatar/visitor/Unit.java, the abstract base class keeps its children array private and final. The only entry point for external operations is the accept(UnitVisitor visitor) method, which traverses the hierarchy without exposing the collection structure:
public abstract class Unit {
private final Unit[] children;
public Unit(Unit... children) {
this.children = children;
}
public void accept(UnitVisitor visitor) {
Arrays.stream(children).forEach(child -> child.accept(visitor));
}
}
Concrete elements like Soldier override accept() to call visitor.visit(this), then delegate to the parent for recursive traversal. No internal arrays, counters, or states are exposed directly to visitors.
Provide Domain-Specific Operations Instead of Raw Getters
Rather than exposing private fields through getters, elements should offer semantic methods that describe what the visitor needs to know. For example, instead of exposing private int ammo via getAmmo(), provide a method like ammoInfo() that returns a processed string:
class Soldier extends Unit {
private int ammo;
public String ammoInfo() {
return ammo > 0 ? "Fully stocked" : "Out of ammo";
}
}
This keeps the internal representation hidden while giving visitors the derived data they need to perform their operations.
Use Visitor Context Objects for Complex Scenarios
When visitors need multiple pieces of information or mutable state, pass a context object through the accept signature. The element populates the context with derived data rather than exposing its fields. This approach keeps the element's state private while allowing visitors to collect complex information across the hierarchy.
Source Code Analysis: The Visitor Hierarchy
The repository implements a clean separation between the element hierarchy (Unit → Soldier/Sergeant/Commander) and the operation hierarchy (UnitVisitor → specific visitors). This design prevents visitors from accessing internal fields while still enabling type-specific operations.
The Element Structure
Unit.java serves as the composite base. Soldier.java, Sergeant.java, and Commander.java extend this base, each overriding accept() to enable double-dispatch without revealing internals:
// src/main/java/com/iluwatar/visitor/Soldier.java
public class Soldier extends Unit {
public Soldier(Unit... children) { super(children); }
@Override
public void accept(UnitVisitor visitor) {
visitor.visit(this);
super.accept(visitor);
}
@Override
public String toString() { return "soldier"; }
}
Note that Soldier exposes no state fields—only the accept() method and a toString() for identification.
The Visitor Interface
UnitVisitor.java declares specific visit methods for each concrete element type, avoiding generic object parameters that would require reflection or casting:
// src/main/java/com/iluwatar/visitor/UnitVisitor.java
public interface UnitVisitor {
void visit(Soldier soldier);
void visit(Sergeant sergeant);
void visit(Commander commander);
}
Concrete Visitor Implementation
SoldierVisitor.java demonstrates how to implement an operation without accessing private state. It only overrides visit(Soldier) and provides empty implementations for other types, focusing solely on its domain concern:
// src/main/java/com/iluwatar/visitor/SoldierVisitor.java
@Slf4j
public class SoldierVisitor implements UnitVisitor {
@Override
public void visit(Soldier soldier) {
LOGGER.info("Greetings {}", soldier);
}
@Override public void visit(Sergeant sergeant) {}
@Override public void visit(Commander commander) {}
}
Running the Encapsulated Visitor Example
The App.java entry point demonstrates how clients use the pattern without ever seeing internal element state:
// src/main/java/com/iluwatar/visitor/App.java
public class App {
public static void main(String[] args) {
var commander = new Commander(
new Sergeant(new Soldier(), new Soldier(), new Soldier()),
new Sergeant(new Soldier(), new Soldier(), new Soldier())
);
commander.accept(new SoldierVisitor());
commander.accept(new SergeantVisitor());
commander.accept(new CommanderVisitor());
}
}
Each visitor traverses the entire hierarchy through the encapsulated accept() mechanism, executing only on the element types they care about while remaining ignorant of the internal children array structure.
Extending Operations Without Breaking Encapsulation
The primary benefit of this design is the ability to add new operations while keeping the element classes closed for modification.
To add a new operation, create a class implementing UnitVisitor. The new visitor interacts with elements exclusively through their public accept() methods and any domain-specific accessors you choose to expose. The existing element hierarchy requires no changes, maintaining the Open/Closed Principle.
If you must add a new element type (e.g., General), you will need to update UnitVisitor to include visit(General general). This is the well-known trade-off of the Visitor pattern: the interface is stable against new operations but requires modification when the element hierarchy changes.
Summary
- Keep element fields private and avoid creating public getters solely for visitor access.
- Expose only the
accept()method as the entry point for external operations, implementing double-dispatch without revealing internal structure. - Use domain-specific methods instead of raw state getters when visitors need element information.
- Implement specific visitor interfaces that work with concrete types rather than generic objects, eliminating the need for reflection.
- Add new operations by creating visitor classes without modifying existing element code, preserving encapsulation and following the Open/Closed Principle.
Frequently Asked Questions
Does the Visitor pattern always violate encapsulation?
No. While naive implementations often force elements to expose internal state through getters, the pattern itself does not require this. By keeping fields private and providing only semantic methods or using context objects, you maintain strong encapsulation while still enabling double-dispatch operations.
How does the accept() method preserve encapsulation?
The accept() method acts as a controlled gateway. It allows the visitor to perform an operation on the element without giving the visitor direct access to private fields. The element decides what to expose—whether through toString(), domain-specific methods, or by passing derived data to the visitor—keeping its internal representation hidden.
What is the trade-off when using Visitor with encapsulation?
The main trade-off is that adding new element types requires updating the visitor interface and all existing visitor implementations. However, adding new operations (visitors) requires no changes to the element hierarchy. This exchanges element extensibility for operation extensibility, which is ideal when the element structure is stable but operations change frequently.
Can I use reflection to avoid adding getters for visitors?
While reflection allows access to private fields without getters, it severely breaks encapsulation and creates brittle code that fails when internal field names or types change. The java-design-patterns implementation avoids reflection entirely by using type-specific visit methods in the UnitVisitor interface, ensuring compile-time safety and encapsulation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →