Separate an algorithm from the objects it operates on — letting you add new operations to a class hierarchy without modifying any of the classes.
The Visitor pattern lets you add new operations to an existing class hierarchy without modifying those classes. You define a visitor object that knows how to handle each class in the hierarchy, and each class accepts the visitor via a simple accept(visitor) call.
Think of a tax inspector visiting different business types: a restaurant, a shop, and a factory are all visited by the same inspector. Each business type knows how to let the inspector in (accept()). The inspector applies the right rules per business type (visit()). Next year you add a new inspector (visitor) with new rules — without changing any business class.
The pattern exploits double dispatch: the right visit() method is selected based on both the visitor's type and the element's type — something single-dispatch languages (Java, Python, JS) can't do naturally, which is exactly why accept() exists.
Key Idea: Elements say "I'll let you in" via accept(visitor). Visitors say "here's what I do to each of you" via visit(element). Add new operations by writing new visitors — zero changes to elements.
accept() ExistsMost OOP languages use single dispatch — the method called depends only on the runtime type of one object (the receiver). Visitor needs dispatch on two types: the element and the visitor. The accept() / visit() pair achieves this in two steps:
// Step 1 — dispatch on the element's type (which accept() is called) electronics.accept(taxVisitor); // ↑ runtime type = Electronics → calls Electronics.accept() // Step 2 — dispatch on the visitor's type (which visit() overload) // Inside Electronics.accept(): visitor.visit(this); // this = Electronics → calls TaxVisitor.visit(Electronics) // Runtime selects the right overload based on visitor AND element type. // Without accept(), you'd need instanceof everywhere: if (item instanceof Electronics) taxVisitor.visitElectronics((Electronics) item); else if (item instanceof Grocery) taxVisitor.visitGrocery((Grocery) item); // ... and this lives in the client, not the element. Fragile.
Remember: accept() is not wasted boilerplate — it's the mechanism that gives the visitor type information about the element, enabling correct method dispatch without instanceof.
Declares a visit() method for each concrete element class in the hierarchy. Adding a new element type requires updating this interface and all concrete visitors — that's the trade-off.
Implements one operation across the entire element hierarchy. Each visit() method handles the element-specific logic. TaxVisitor, ShippingVisitor, and DiscountVisitor are all separate concerns with no shared code.
Declares accept(visitor) — the only method elements need to add to support any visitor. The implementation is always the same one-liner: visitor.visit(this).
Implements accept(visitor) and provides any accessors visitors need to do their work (getPrice(), isPerishable(), getWarranty()). The element never knows which visitor is visiting.
A collection or composite that holds elements and can iterate them — passing each to a visitor. Often just a List, but can be a Composite tree.
Without Visitor, adding a new operation (e.g. "calculate tax") to a class hierarchy means adding a method to every class. This violates Open/Closed and pollutes domain classes with unrelated concerns.
// Without Visitor — every new operation pollutes every class
class Electronics {
double calculateTax() { return price * 0.18; } // operation 1
double calculateShipping() { return price > 5000 ? 0 : 99; } // operation 2
String exportToHTML() { ... } // operation 3
String exportToMarkdown() { ... } // operation 4
// Electronics now knows about taxes, shipping, HTML, Markdown...
}
// Add another operation → touch every class again.
// With Visitor — each operation is one new class
class TaxVisitor { visit(Electronics e) { return e.getPrice() * 0.18; } }
class ShippingVisitor { visit(Electronics e) { return e.getPrice() > 5000 ? 0 : 99; } }
class HTMLVisitor { visit(Electronics e) { ... } }
// Electronics stays clean. New operation = new visitor only.| Aspect | Visitor | Iterator | Strategy |
|---|---|---|---|
| Purpose | Add operations to a type hierarchy | Traverse a collection | Swap one algorithm for another |
| Knows element types? | Yes — one visit() per type | No — treats all uniformly | No — works on one type |
| Adding operations | Easy — new visitor class | N/A | Easy — new strategy class |
| Adding element types | Hard — update all visitors | Easy | Easy |
| Dispatch | Double dispatch | Single | Single |
Choose Visitor when: the element hierarchy is stable (rarely new classes) but operations are volatile (new ones added frequently). If elements change often, the cost of updating all visitors makes Visitor painful.
// Visitor Pattern — Tax Calculator for an E-Commerce Cart
// ─── Element interface ────────────────────────────────────────
interface CartItem {
String getName();
double getPrice();
void accept(CartVisitor visitor); // the key: accept any visitor
}
// ─── Visitor interface ────────────────────────────────────────
interface CartVisitor {
void visit(Electronics item);
void visit(Grocery item);
void visit(Clothing item);
}
// ─── Concrete Elements ────────────────────────────────────────
class Electronics implements CartItem {
private String name;
private double price;
private int warrantyMonths;
public Electronics(String name, double price, int warranty) {
this.name = name; this.price = price; this.warrantyMonths = warranty;
}
public String getName() { return name; }
public double getPrice() { return price; }
public int getWarranty() { return warrantyMonths; }
public void accept(CartVisitor visitor) { visitor.visit(this); }
}
class Grocery implements CartItem {
private String name;
private double price;
private boolean perishable;
public Grocery(String name, double price, boolean perishable) {
this.name = name; this.price = price; this.perishable = perishable;
}
public String getName() { return name; }
public double getPrice() { return price; }
public boolean isPerishable() { return perishable; }
public void accept(CartVisitor visitor) { visitor.visit(this); }
}
class Clothing implements CartItem {
private String name;
private double price;
private String size;
public Clothing(String name, double price, String size) {
this.name = name; this.price = price; this.size = size;
}
public String getName() { return name; }
public double getPrice() { return price; }
public String getSize() { return size; }
public void accept(CartVisitor visitor) { visitor.visit(this); }
}
// ─── Concrete Visitor 1: Tax Calculator ───────────────────────
class TaxVisitor implements CartVisitor {
private double totalTax = 0;
// Each item type has a different tax rate
public void visit(Electronics item) {
double tax = item.getPrice() * 0.18; // 18% GST
System.out.printf("[Tax] %-20s price=₹%.2f tax=₹%.2f (18%% GST)%n",
item.getName(), item.getPrice(), tax);
totalTax += tax;
}
public void visit(Grocery item) {
double rate = item.isPerishable() ? 0.00 : 0.05;
double tax = item.getPrice() * rate;
System.out.printf("[Tax] %-20s price=₹%.2f tax=₹%.2f (%.0f%% GST)%n",
item.getName(), item.getPrice(), tax, rate * 100);
totalTax += tax;
}
public void visit(Clothing item) {
double tax = item.getPrice() * 0.12; // 12% GST
System.out.printf("[Tax] %-20s price=₹%.2f tax=₹%.2f (12%% GST)%n",
item.getName(), item.getPrice(), tax);
totalTax += tax;
}
public double getTotalTax() { return totalTax; }
}
// ─── Concrete Visitor 2: Shipping Cost Calculator ─────────────
class ShippingVisitor implements CartVisitor {
private double totalShipping = 0;
public void visit(Electronics item) {
double fee = item.getPrice() > 5000 ? 0 : 99; // free shipping above ₹5000
System.out.printf("[Ship] %-20s fee=₹%.2f%n", item.getName(), fee);
totalShipping += fee;
}
public void visit(Grocery item) {
double fee = item.isPerishable() ? 49 : 29; // cold chain costs more
System.out.printf("[Ship] %-20s fee=₹%.2f%n", item.getName(), fee);
totalShipping += fee;
}
public void visit(Clothing item) {
System.out.printf("[Ship] %-20s fee=₹0.00 (flat rate included)%n", item.getName());
}
public double getTotalShipping() { return totalShipping; }
}
// ─── Concrete Visitor 3: Discount Eligibility Checker ─────────
class DiscountVisitor implements CartVisitor {
public void visit(Electronics item) {
if (item.getWarranty() >= 24)
System.out.println("[Discount] " + item.getName() + " → eligible for extended warranty bundle");
}
public void visit(Grocery item) {
if (!item.isPerishable())
System.out.println("[Discount] " + item.getName() + " → eligible for bulk discount");
}
public void visit(Clothing item) {
System.out.println("[Discount] " + item.getName() + " (size " + item.getSize() + ") → eligible for seasonal sale");
}
}
// ─── Client ───────────────────────────────────────────────────
public class App {
public static void main(String[] args) {
java.util.List<CartItem> cart = java.util.Arrays.asList(
new Electronics("Samsung TV", 45000, 24),
new Electronics("USB-C Cable", 499, 6),
new Grocery("Organic Milk", 89, true),
new Grocery("Basmati Rice 5kg", 450, false),
new Clothing("Linen Shirt", 1299, "L"),
new Clothing("Running Shoes", 2999, "42")
);
System.out.println("========== TAX CALCULATION ==========");
TaxVisitor taxVisitor = new TaxVisitor();
cart.forEach(item -> item.accept(taxVisitor));
System.out.printf("Total Tax: ₹%.2f%n%n", taxVisitor.getTotalTax());
System.out.println("======== SHIPPING CALCULATION ========");
ShippingVisitor shippingVisitor = new ShippingVisitor();
cart.forEach(item -> item.accept(shippingVisitor));
System.out.printf("Total Shipping: ₹%.2f%n%n", shippingVisitor.getTotalShipping());
System.out.println("======= DISCOUNT ELIGIBILITY =========");
DiscountVisitor discountVisitor = new DiscountVisitor();
cart.forEach(item -> item.accept(discountVisitor));
// Key insight: added 3 operations (tax, shipping, discount)
// without touching CartItem, Electronics, Grocery, or Clothing at all!
}
}When you have a fixed set of element classes (rarely changed) but frequently need to add new operations across all of them — tax rules, export formats, validation, statistics.
CartItem hierarchy + TaxVisitor, ShippingVisitor, ExportVisitorCompilers and interpreters use Visitor extensively — parse trees have a fixed set of node types, but you add many passes: type checking, optimisation, code generation, pretty printing.
ASTNode + EvaluatorVisitor, TypeCheckerVisitor, CodeGenVisitorWhen the same document structure needs to be exported in multiple formats — HTML, Markdown, PDF, plain text — each format is a visitor.
DocumentNode + HTMLExporter, MarkdownExporter, PDFExporterWhen you aggregate metrics or generate reports that touch multiple types in a hierarchy — word counts, cost summaries, size calculations.
FileSystemItem + SizeVisitor, PermissionAuditVisitorANTLR generates a parse tree with a fixed set of node types and produces a Visitor base class. You subclass it to add as many tree-walking operations as you need — evaluation, type checking, code generation — without touching the generated parser.
Java's Files.walkFileTree() accepts a FileVisitor with visitFile() and visitDirectory() methods. The file tree is the stable element hierarchy; your visitor implements whatever file-system operation you need.
Babel's plugin API is pure Visitor pattern. Your plugin returns a visitor object with methods named after AST node types. Babel walks the AST and calls your visitor methods — you add new transforms without touching the AST classes.
JUnit's test runner uses Visitor — a Test can be a single TestCase or a TestSuite (Composite). Visitors like ResultPrinter and SummaryCollector are applied to the whole tree, producing different outputs from the same run.