The marketing team runs three offers this week:
| Code | Rule as marketing wrote it |
|---|---|
BLR100 | Cart above ₹499 in Bengaluru, or any Prime member |
BULK50 | 3 or more items and cart above ₹999, but not in Mumbai |
FESTIVE20 | Prime members in Bengaluru or Chennai |
Here is how they ended up in code.
What the code does
CouponService.eligibleCoupons(order) checks every rule by hand:
// BLR100: "cart above 499 in Bengaluru, or any Prime member"
if (order.cartTotal > 499) {
if (order.city.equals("Bengaluru")) {
coupons.add("BLR100");
} else if (order.isPrime) {
coupons.add("BLR100");
}
} else if (order.isPrime) {
coupons.add("BLR100");
}For four sample orders it prints:
Rs 650, Bengaluru, prime=false, items=2 -> [BLR100]
Rs 320, Pune, prime=true, items=1 -> [BLR100]
Rs 1200, Mumbai, prime=false, items=4 -> []
Rs 1500, Chennai, prime=true, items=3 -> [BLR100, BULK50, FESTIVE20]
The answers are right. The design is the problem.
Problem 1: a one-line rule became nine lines
Marketing's rule is (cartTotal > 499 AND city == "Bengaluru") OR isPrime. In code, isPrime is checked in two branches and coupons.add("BLR100") appears three times. To learn the rule, you must trace every branch. If you change one branch and forget the other, Prime users in Pune stop getting the coupon.
Problem 2: every tweak is a release
// BULK50: "3 or more items and cart above 999, but not in Mumbai"
// Last week it was "not in Delhi". Next week, who knows.
if (order.itemCount >= 3) {
if (order.cartTotal > 999) {
if (!order.city.equals("Mumbai")) {
coupons.add("BULK50");
}
}
}Mumbai becomes Delhi. ₹999 becomes ₹1,199. Each change needs a ticket, a code edit, a review, a test run and a deploy. Marketing may change a number on a Monday morning, and this is a lot of process for that.
Problem 3: the rules are invisible
Someone in marketing asks "which offers are live, and what are the conditions?" The honest answer is "let me read the Java". You can't print the rules, show them on an admin screen or store them in a database. You can't turn one off without a release.
Problem 4: no reuse
city == "Bengaluru" and isPrime appear in more than one offer, but each offer checks them in its own way. There are no small, named pieces you can test alone. You can only test the whole eligibleCoupons method.
Summary of the smells
| Smell | Why it hurts |
|---|---|
| Rules expressed as control flow | Hard to read, easy to break one branch |
| Rules hard-coded in Java | Every change needs a developer and a deploy |
| Rules not stored as data | Can't list, show, store or toggle them |
| No small building blocks | Same checks repeated, only big tests possible |
What we want instead
We want each rule to be a value: an object we can build, print, store and evaluate. We want to make it from tiny reusable pieces like "greater than", "equals", "and" and "or".
That is the Interpreter pattern. See the After version.
Run it:
javac -d out src/DesignPatterns/BehaviouralPatterns/InterpreterPattern/Before/Main.java
java -cp out DesignPatterns.BehaviouralPatterns.InterpreterPattern.Before.MainNested if statements are not always bad. They become a problem when the same small set of words (amounts, cities, membership, AND, OR, NOT) is mixed in new ways again and again. They also become a problem when you must inspect the rules or supply them from outside the service. If developers own three stable rules, plain Java may be the better design.