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
| Before | After |
|---|---|
| Four files edited per operation | One new visitor class |
| Tax, shipping and invoice code mixed in each item | Each visitor owns one operation |
instanceof chain falls through silently | A missing visit is a compile error |
| "How is GST calculated?" means opening three files | Open 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.MainReading 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.