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

We are building the checkout for a Flipkart style store. The cart has three item types: Electronics, Grocery and Clothing. The item types rarely change. What keeps changing is the list of things we calculate for them.

What the code does

Each item class carries every operation as a method:

abstract class CartItem {
    abstract double gst();          // finance
    abstract double shippingCost(); // logistics
    abstract String invoiceLine();  // accounts
}

And each subclass fills in its own rules:

class Grocery extends CartItem {
    double gst()          { return price * 0.05; }
    double shippingCost() { return weightKg * 25; }
    String invoiceLine()  { return String.format(...); }
}

For a cart with boAt Airdopes (₹1,499), Aashirvaad Atta 5kg (₹289) and Levi's jeans (₹2,999), it prints an invoice, ₹824.09 GST, ₹264 shipping and a total of ₹5,875.09. It works. The trouble starts with the next request.

Problem 1: one new operation, four files edited

Marketing asks for loyalty points. The "proper" way is to add abstract int loyaltyPoints() to CartItem and implement it in all three subclasses. That is four files touched for one feature, and every item class grows a little more.

After a year you have gst(), shippingCost(), invoiceLine(), loyaltyPoints(), returnWindowDays(), sellerCommission()... all inside the same small data classes.

Problem 2: unrelated rules live together

Open Grocery and you see tax rules, logistics rules and invoice formatting in one class. Finance, logistics and accounts all edit the same file. Merge conflicts happen, and a change to shipping can break a test about GST.

The class has many reasons to change. That breaks the Single Responsibility Principle (a class should do one job).

Problem 3: the instanceof shortcut

Tired of editing every item class, someone writes this:

class LoyaltyPointsCalculator {
    int pointsFor(CartItem item) {
        if (item instanceof Electronics) {
            return (int) (item.getPrice() / 100);
        } else if (item instanceof Grocery) {
            return (int) (item.getPrice() / 50);
        } else if (item instanceof Clothing) {
            return (int) (item.getPrice() / 75);
        }
        return 0;
    }
}

It looks harmless. Now add a Books item type. The compiler is happy, but the method returns 0 for every book. Customers stop getting points, and nobody notices until support tickets pile up.

This is the worst part: the compiler can't help you. Nothing forces this method to handle a new type.

Summary of the smells

SmellWhy it hurts
Operations inside data classesEvery new operation edits every item class
Many teams in one fileMerge conflicts, mixed concerns, harder tests
instanceof chainsNew types fall through silently to a wrong default
Operations spread across classesTo read "how do we calculate GST?" you open three files

What we want instead

We want each operation (GST, shipping, invoice, loyalty) in one class of its own, covering all item types. Adding an operation should mean adding a class, not editing the items. And if a new item type ever appears, the compiler should point at every operation that doesn't handle it yet.

That is the Visitor pattern. See the After version.

Run it:

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

The “four files” count describes a real project: one base type and three item files. This runnable lesson keeps its small classes together in one Main.java. What matters is how many classes must change, not how many files the demo uses.

In this topic

Before code1 file

Before/Main.java178 linesGitHub
package DesignPatterns.BehaviouralPatterns.VisitorPattern.Before;import java.util.List;/** * PROBLEM: every new operation on cart items is bolted onto the item classes. * GST, shipping and invoice export all live inside the items. Next sprint it * will be loyalty points, then return windows, then seller commission. */abstract class CartItem {    protected final String name;    protected final double price;    CartItem(String name, double price) {        this.name = name;        this.price = price;    }    String getName() {        return name;    }    double getPrice() {        return price;    }    // Operation 1: tax rules. Owned by the finance team.    abstract double gst();    // Operation 2: shipping rules. Owned by the logistics team.    abstract double shippingCost();    // Operation 3: invoice formatting. Owned by the accounts team.    abstract String invoiceLine();}class Electronics extends CartItem {    private final int warrantyMonths;    Electronics(String name, double price, int warrantyMonths) {        super(name, price);        this.warrantyMonths = warrantyMonths;    }    int getWarrantyMonths() {        return warrantyMonths;    }    @Override    double gst() {        return price * 0.18;    }    @Override    double shippingCost() {        return 99; // fragile packaging    }    @Override    String invoiceLine() {        return String.format(            "%-22s Rs %9.2f  GST 18%%  (%d months warranty)",            name, price, warrantyMonths        );    }}class Grocery extends CartItem {    private final double weightKg;    Grocery(String name, double price, double weightKg) {        super(name, price);        this.weightKg = weightKg;    }    double getWeightKg() {        return weightKg;    }    @Override    double gst() {        return price * 0.05;    }    @Override    double shippingCost() {        return weightKg * 25;    }    @Override    String invoiceLine() {        return String.format("%-22s Rs %9.2f  GST 5%%   (%.1f kg)", name, price, weightKg);    }}class Clothing extends CartItem {    private final String size;    Clothing(String name, double price, String size) {        super(name, price);        this.size = size;    }    String getSize() {        return size;    }    @Override    double gst() {        return price <= 2500 ? price * 0.05 : price * 0.18;    }    @Override    double shippingCost() {        return 40;    }    @Override    String invoiceLine() {        return String.format("%-22s Rs %9.2f  size %s", name, price, size);    }}/** * The "quick fix" for operation 4. Nobody wanted to touch three classes again, * so someone wrote an instanceof chain. Add a new item type (say, Books) and * this method quietly returns 0. The compiler will not warn you. */class LoyaltyPointsCalculator {    int pointsFor(CartItem item) {        if (item instanceof Electronics) {            return (int) (item.getPrice() / 100);        } else if (item instanceof Grocery) {            return (int) (item.getPrice() / 50);        } else if (item instanceof Clothing) {            return (int) (item.getPrice() / 75);        }        return 0;    }}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")        );        double subtotal = 0;        double gst = 0;        double shipping = 0;        int points = 0;        LoyaltyPointsCalculator loyalty = new LoyaltyPointsCalculator();        System.out.println("Invoice");        for (CartItem item : cart) {            System.out.println("  " + item.invoiceLine());            subtotal += item.getPrice();            gst += item.gst();            shipping += item.shippingCost();            points += loyalty.pointsFor(item);        }        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