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
| Smell | Why it hurts |
|---|---|
| Operations inside data classes | Every new operation edits every item class |
| Many teams in one file | Merge conflicts, mixed concerns, harder tests |
instanceof chains | New types fall through silently to a wrong default |
| Operations spread across classes | To 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.MainThe “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.