01What is a reason to change, in the single responsibility principle?
A person or team who can ask for a change, such as finance for tax rules. A class should answer to only one.
02In a class diagram, what does ╌╌▷ mean?
Implements: the class at the tail implements the interface at the head.
03What is polymorphism?
One method call, many behaviours. The code that runs depends on the class of the object.
Your food delivery app launches a new offer almost every festival. If each offer means an edit to the class that computes discounts, each launch can break the offers that are already live.
This lesson shows how to add an offer without touching code that works.
The problem
Checkout asks one class for the money off. The class picks the rule with an if-else on the coupon code.
class DiscountCalculator {
int discount(String code, int amount) {
if (code.equals("NEW_USER")) {
return amount * 20 / 100; // 20% off
} else if (code.equals("WEEKEND")) {
return 50; // flat Rs 50 off
} else if (code.equals("PREMIUM")) {
return Math.min(amount * 10 / 100, 100); // 10%, at most Rs 100
}
return 0;
}
}For a Rs 800 order, the three codes give Rs 160, Rs 50 and Rs 80 off. Any other code falls through and gives 0.
The festive request
Marketing asks: "For Diwali, add a FESTIVE code: 25% off, at most Rs 150."
Today, discount("FESTIVE", 800) returns 0. To ship the offer, you must:
- Open
DiscountCalculator, a class that is live and tested. - Add a fourth branch to its if-else.
- Retest all four rules, because they share one method.
- Redeploy the class that every offer depends on.
Each campaign adds one more branch. After a year of offers the method has dozens of them, and one wrong edit breaks offers that customers use today.
The idea
Open for extension, closed for change: add new behaviour by adding code, not by editing code that already works.
Bertrand Meyer named the principle in 1988. In a class design, it works like this:
- Give every rule the same shape: an .
- Put each rule in its own small class.
- Let
Checkouttalk only to the interface.
┌──────────────────────────────────────────────────┐ │ │ │ ┌──────────────────────────┐ │ │ │ Checkout │ │ │ ├──────────────────────────┤ │ │ │ + pay(amount, discount) │ │ │ └───┬──────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ <<interface>> │ │ │ │ Discount │ │ │ ├──────────────────────────┤ │ │ │ + apply(amount): int │ │ │ └──────────────────────────┘ │ │ △ │ │ ╎ │ │ ├╌╌ NewUserDiscount 20% off │ │ ├╌╌ WeekendDiscount Rs 50 off │ │ ├╌╌ PremiumDiscount 10%, max 100 │ │ └╌╌ FestiveDiscount new: 25%, max 150 │ │ ──▶ uses ╌╌▷ implements │ │ │ └──────────────────────────────────────────────────┘
Discountis the one shape every rule shares: take an amount, return the money off.- Each rule is a small class that implements
Discount. Checkoutholds aDiscountand callsapply. It never asks which rule it holds.- A new offer is a new class at the bottom. Nothing above it changes.
"Open" means the design accepts new rules. "Closed" means the classes that work today stay as they are.
The fix
Step 1: one interface, one class per rule
Each branch of the old if-else moves into its own class. The math is the same.
Step 2: Checkout takes any discount
Checkout.pay receives a Discount and subtracts what apply returns.
interface Discount {
int apply(int amount);
}
class NewUserDiscount implements Discount {
public int apply(int amount) { return amount * 20 / 100; }
}
class WeekendDiscount implements Discount {
public int apply(int amount) { return 50; }
}
class PremiumDiscount implements Discount {
public int apply(int amount) { return Math.min(amount * 10 / 100, 100); }
}
class Checkout {
int pay(int amount, Discount discount) {
return amount - discount.apply(amount);
}
}checkout.pay(800, new WeekendDiscount()) returns 750. Polymorphism picks the right apply, so Checkout needs no if-else at all.
The festive request again
Now the Diwali offer is one new class:
class FestiveDiscount implements Discount {
public int apply(int amount) { return Math.min(amount * 25 / 100, 150); }
}checkout.pay(800, new FestiveDiscount()) returns 650. 25% of 800 is 200, and the cap brings it down to 150. The three old rules still give 160, 50 and 80.
- If-else on the codea new offer edits
- -Open a live, tested class
- -Add a fourth branch
- -Retest all four rules
- -Redeploy every offer
- One class per rulea new offer adds
- +Write FestiveDiscount
- +Test only the new class
- +Old rules stay untouched
- +Checkout stays untouched
One place still turns the text "FESTIVE" into a FestiveDiscount object: a lookup table, or a . That place gains one line, but it holds no discount math. Factories come in the creational patterns chapter.
How it runs
- Cart page to Checkout:pay(800, d)
- Checkout to FestiveDiscount:apply(800)
- FestiveDiscount to Checkout:150
- Checkout to Cart page:650
- The cart page passes the amount and a discount object to
Checkout. Checkoutcallsapplyon it. It does not know the object is aFestiveDiscount.Checkoutsubtracts the reply and returns the amount to pay.
Swap d for a WeekendDiscount and the same three calls return 750.
When to use it
Signals in a requirement
- An if-else or switch on a type or code that grows every sprint.
- Words such as "more payment providers later", "new offers each festival", "rules per city".
- The same switch in several files. One new case means edits in all of them.
When it is too much
- Two cases that never change. A short if-else is clearer than an interface and two classes (YAGNI, from DRY, KISS, YAGNI and composition).
- Guessing the future. Wait for the second or third real request, then pull out the interface. You will know its shape by then.
Refactor a growing if-else in four steps
- Write the interface from what every branch takes and returns.
- Move each branch into its own class, with the math unchanged.
- Make the caller accept the interface instead of a code.
- Delete the if-else and run the old tests. Each rule must give the same answer as before.
Open-closed or single responsibility?
| Single responsibility | Open-closed | |
|---|---|---|
| The question | Who asks this class to change? | Can I add a case without edits? |
| The smell | one class, many teams | a growing if-else on a type |
| The fix | split by reason to change | one interface, a class per case |
The two often work together. The EmailSender from Single responsibility and WhatsAppSender could share a Sender interface. Then Checkout loops over a list of senders, and a new channel is one new class in that list.
- 01
Open for extension, closed for change: add a class, do not edit one that works.
- 02
The tool is an interface plus one class per rule. The caller talks only to the interface.
- 03
The festive offer became one new class. Checkout and the three old rules did not change.
- 04
Do not add an interface for two cases that never change. Wait for real requests.
Read more
- The Open Closed Principle by Robert C. Martin, with plugins as the big example
- Comparator in the Java 21 docs
- Strategy on refactoring.guru, the pattern behind this lesson's fix
Next: Liskov substitution. A child class must keep every promise of its parent, and the square that breaks a rectangle's promise.