Skip to content

Visitor Pattern: Add New Operations Without Touching Your Classes

Pull operations like GST, shipping and invoice export out of your item classes, so a new operation is a new class and the items never change.

Builds on Method overriding and polymorphism and OCP - The Open/Closed Principle

Stage 3advanced2 min read

The refactor moves every operation out of the item classes into its own visitor. The items keep their data and one tiny method, accept.

Step 1: items only accept a visitor

interface CartItem {
    String getName();
    double getPrice();
    <R> R accept(ItemVisitor<R> visitor);
}

class Electronics implements CartItem {
    // name, price, warrantyMonths + getters

    public <R> R accept(ItemVisitor<R> visitor) {
        return visitor.visit(this);
    }
}

Grocery and Clothing look the same. No GST, no shipping, no formatting. Just data.

That one-line accept is the key. Inside Electronics, this is known to be an Electronics, so Java calls visit(Electronics). You never check the type yourself.

Step 2: the visitor interface

interface ItemVisitor<R> {
    R visit(Electronics item);
    R visit(Grocery item);
    R visit(Clothing item);
}

One method per item type. R is a generic type (a placeholder for the return type). Each operation picks its own: Double for GST, String for an invoice line, Integer for loyalty points. You do not need to store results in fields.

Step 3: one class per operation

class GstCalculator implements ItemVisitor<Double> {
    public Double visit(Electronics item) { return item.getPrice() * 0.18; }
    public Double visit(Grocery item)     { return item.getPrice() * 0.05; }
    public Double visit(Clothing item) {
        double rate = item.getPrice() <= 2500 ? 0.05 : 0.18;
        return item.getPrice() * rate;
    }
}

All GST rules, for every item type, are in one file. The finance team owns this class, and nobody else needs to open it. ShippingCalculator and InvoiceLineExporter follow the same shape.

Step 4: the client picks the visitor

for (CartItem item : cart) {
    System.out.println("  " + item.accept(exporter));
    gst      += item.accept(gstCalculator);
    shipping += item.accept(shippingCalculator);
    points   += item.accept(loyalty);
}

Same output as before: ₹824.09 GST, ₹264 shipping, ₹5,875.09 total, 58 loyalty points.

Adding loyalty points now

This was the painful request in the Before code. Now it is one new class and zero edits to the items:

class LoyaltyPointsCalculator implements ItemVisitor<Integer> {
    public Integer visit(Electronics item) { return (int) (item.getPrice() / 100); }
    public Integer visit(Grocery item)     { return (int) (item.getPrice() / 50); }
    public Integer visit(Clothing item)    { return (int) (item.getPrice() / 75); }
}

What if we add Books?

Add R visit(Books item); to ItemVisitor. Now GstCalculator, ShippingCalculator, InvoiceLineExporter and LoyaltyPointsCalculator all fail to compile until someone decides what books should do. Compare that with the instanceof chain that silently returned 0.

That is also the cost. Every visitor has to change for one new type. Visitor is the right call here only because item types are stable and operations keep coming.

How the problems went away

BeforeAfter
Four files edited per operationOne new visitor class
Tax, shipping and invoice code mixed in each itemEach visitor owns one operation
instanceof chain falls through silentlyA missing visit is a compile error
"How is GST calculated?" means opening three filesOpen GstCalculator

On Java 21+

If you can use Java 21, a sealed CartItem with records and a pattern matching switch gives the same compile-time check with less code. The guide shows the snippet.

Run it:

javac -d out src/DesignPatterns/BehaviouralPatterns/VisitorPattern/After/Main.java
java -cp out DesignPatterns.BehaviouralPatterns.VisitorPattern.After.Main

Reading the dispatch without jargon

Read item.accept(gstCalculator) in two hops. First, the real item picks its accept method, so an Electronics object enters Electronics.accept. Inside it, this is an Electronics, so visitor.visit(this) selects visit(Electronics). Then the real visitor object runs its GST code. That is all double dispatch is.

The lesson uses double to keep the arithmetic simple to read. Production money code should normally use BigDecimal, an explicit scale and an agreed RoundingMode, because double can be off by a fraction of a cent. Changing the number type does not change the Visitor structure.

In this topic

After code1 file

After/Main.java239 linesGitHub
package DesignPatterns.BehaviouralPatterns.VisitorPattern.After;import java.util.List;// ============================================================================// 1. ELEMENTS: plain data plus one accept() method. No business rules here.// ============================================================================interface CartItem {    String getName();    double getPrice();    <R> R accept(ItemVisitor<R> visitor);}class Electronics implements CartItem {    private final String name;    private final double price;    private final int warrantyMonths;    Electronics(String name, double price, int warrantyMonths) {        this.name = name;        this.price = price;        this.warrantyMonths = warrantyMonths;    }    public String getName() {        return name;    }    public double getPrice() {        return price;    }    int getWarrantyMonths() {        return warrantyMonths;    }    @Override    public <R> R accept(ItemVisitor<R> visitor) {        // "this" is an Electronics here, so Java picks visit(Electronics)        return visitor.visit(this);    }}class Grocery implements CartItem {    private final String name;    private final double price;    private final double weightKg;    Grocery(String name, double price, double weightKg) {        this.name = name;        this.price = price;        this.weightKg = weightKg;    }    public String getName() {        return name;    }    public double getPrice() {        return price;    }    double getWeightKg() {        return weightKg;    }    @Override    public <R> R accept(ItemVisitor<R> visitor) {        return visitor.visit(this);    }}class Clothing implements CartItem {    private final String name;    private final double price;    private final String size;    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;    }    String getSize() {        return size;    }    @Override    public <R> R accept(ItemVisitor<R> visitor) {        return visitor.visit(this);    }}// ============================================================================// 2. VISITOR: one visit method per element type. R is what the operation returns.// ============================================================================interface ItemVisitor<R> {    R visit(Electronics item);    R visit(Grocery item);    R visit(Clothing item);}// ============================================================================// 3. CONCRETE VISITORS: one class per operation, all rules in one place.// ============================================================================class GstCalculator implements ItemVisitor<Double> {    @Override    public Double visit(Electronics item) {        return item.getPrice() * 0.18;    }    @Override    public Double visit(Grocery item) {        return item.getPrice() * 0.05;    }    @Override    public Double visit(Clothing item) {        double rate = item.getPrice() <= 2500 ? 0.05 : 0.18;        return item.getPrice() * rate;    }}class ShippingCalculator implements ItemVisitor<Double> {    @Override    public Double visit(Electronics item) {        return 99.0;    }    @Override    public Double visit(Grocery item) {        return item.getWeightKg() * 25;    }    @Override    public Double visit(Clothing item) {        return 40.0;    }}class InvoiceLineExporter implements ItemVisitor<String> {    @Override    public String visit(Electronics item) {        return String.format(            "%-22s Rs %9.2f  GST 18%%  (%d months warranty)",            item.getName(), item.getPrice(), item.getWarrantyMonths()        );    }    @Override    public String visit(Grocery item) {        return String.format(            "%-22s Rs %9.2f  GST 5%%   (%.1f kg)",            item.getName(), item.getPrice(), item.getWeightKg()        );    }    @Override    public String visit(Clothing item) {        return String.format("%-22s Rs %9.2f  size %s", item.getName(), item.getPrice(), item.getSize());    }}// The new operation from the Before code. Zero changes to the item classes.class LoyaltyPointsCalculator implements ItemVisitor<Integer> {    @Override    public Integer visit(Electronics item) {        return (int) (item.getPrice() / 100);    }    @Override    public Integer visit(Grocery item) {        return (int) (item.getPrice() / 50);    }    @Override    public Integer visit(Clothing item) {        return (int) (item.getPrice() / 75);    }}// ============================================================================// 4. CLIENT// ============================================================================public class Main {    public static void main(String[] args) {        List<CartItem> cart = List.of(            new Electronics("boAt Airdopes 141", 1499, 12),            new Grocery("Aashirvaad Atta 5kg", 289, 5),            new Clothing("Levi's 511 Jeans", 2999, "32")        );        GstCalculator gstCalculator = new GstCalculator();        ShippingCalculator shippingCalculator = new ShippingCalculator();        InvoiceLineExporter exporter = new InvoiceLineExporter();        LoyaltyPointsCalculator loyalty = new LoyaltyPointsCalculator();        double subtotal = 0;        double gst = 0;        double shipping = 0;        int points = 0;        System.out.println("Invoice");        for (CartItem item : cart) {            System.out.println("  " + item.accept(exporter));            subtotal += item.getPrice();            gst += item.accept(gstCalculator);            shipping += item.accept(shippingCalculator);            points += item.accept(loyalty);        }        System.out.printf("Subtotal: Rs %.2f%n", subtotal);        System.out.printf("GST:      Rs %.2f%n", gst);        System.out.printf("Shipping: Rs %.2f%n", shipping);        System.out.printf("Total:    Rs %.2f%n", subtotal + gst + shipping);        System.out.println("Loyalty points earned: " + points);    }}

Done with this topic?

How well do you know it?

PreviousMediator Pattern: Talk to the Hub, Not to Each Other
Practise 7 questions