Three design principles you will be asked to name, and the three questions behind them: who asks for changes, how do I add a case without editing old code, and does my subclass keep its parent's promises?
An invoice arrives in your inbox. Three different groups of people care about it. Finance cares about the tax: they change the rate. Design cares about how it looks: they change the layout. Operations cares about how it is sent: they change the mail provider. If one class prices the invoice, prints it and emails it, then each of those three groups edits the same file.
This lesson covers the first three of the five SOLID principles: Single responsibility, Open/closed and Liskov substitution. (The other two, Interface segregation and Dependency inversion, are in the next lesson.) Each gets a precise definition, a runnable before and after, and a note on when it is over-applied. You have already seen them at work in the parking lot, the vending machine and chess.
One invoice class, three owners: finance changes the tax, design the layout, operations the mail. Split it so each group edits only its own class.
The first version: one class that prices, prints and emails#
The first invoice most people write is one class with three methods. Each method is simple. Together they have three owners:
# invoice_v1.py: one class that prices, prints and emails
class Invoice:
def __init__(self, number: int, customer: str, lines: list) -> None:
self.number = number
self.customer = customer
self.lines = lines # (name, quantity, unit price in rupees)
def total(self) -> int: # reason to change 1: finance changes the tax rule
subtotal = sum(qty * price for _, qty, price in self.lines)
return subtotal + subtotal * 18 // 100 # 18 percent tax, whole rupees
def render(self) -> str: # reason to change 2: design changes the layout
rows = [f"{name} x{qty} = {qty * price}" for name, qty, price in self.lines]
return f"Invoice {self.number} for {self.customer}\n" + "\n".join(rows) + f"\nTotal: {self.total()}"
def send(self, address: str) -> None: # reason to change 3: operations changes the mail provider
print(f"sent to {address}: Invoice {self.number}, {self.total()} rupees")
invoice = Invoice(1001, "Asha", [("Notebook", 3, 100), ("Pen", 4, 50)])
print(invoice.render())
invoice.send("asha@example.com")// InvoiceV1.java: one class that prices, prints and emails
import java.util.List;
class InvoiceV1 {
record Line(String name, int qty, int price) {} // price in rupees
static final class Invoice {
private final int number;
private final String customer;
private final List<Line> lines;
Invoice(int number, String customer, List<Line> lines) { this.number = number; this.customer = customer; this.lines = lines; }
int total() { // reason to change 1: finance changes the tax rule
int subtotal = 0;
for (Line l : lines) subtotal += l.qty() * l.price();
return subtotal + subtotal * 18 / 100; // 18 percent tax, whole rupees
}
String render() { // reason to change 2: design changes the layout
StringBuilder out = new StringBuilder("Invoice " + number + " for " + customer);
for (Line l : lines) out.append("\n").append(l.name()).append(" x").append(l.qty()).append(" = ").append(l.qty() * l.price());
return out.append("\nTotal: ").append(total()).toString();
}
void send(String address) { // reason to change 3: operations changes the mail provider
System.out.println("sent to " + address + ": Invoice " + number + ", " + total() + " rupees");
}
}
public static void main(String[] args) {
Invoice invoice = new Invoice(1001, "Asha", List.of(new Line("Notebook", 3, 100), new Line("Pen", 4, 50)));
System.out.println(invoice.render());
invoice.send("asha@example.com");
}
}// invoice_v1.cpp: one class that prices, prints and emails
#include <iostream>
#include <string>
#include <utility>
#include <vector>
struct Line {
std::string name;
int qty;
int price; // rupees
};
class Invoice {
int number_;
std::string customer_;
std::vector<Line> lines_;
public:
Invoice(int number, std::string customer, std::vector<Line> lines)
: number_(number), customer_(std::move(customer)), lines_(std::move(lines)) {}
int total() const { // reason to change 1: finance changes the tax rule
int subtotal = 0;
for (const auto& l : lines_) subtotal += l.qty * l.price;
return subtotal + subtotal * 18 / 100; // 18 percent tax, whole rupees
}
std::string render() const { // reason to change 2: design changes the layout
std::string out = "Invoice " + std::to_string(number_) + " for " + customer_;
for (const auto& l : lines_) out += "\n" + l.name + " x" + std::to_string(l.qty) + " = " + std::to_string(l.qty * l.price);
return out + "\nTotal: " + std::to_string(total());
}
void send(const std::string& address) const { // reason to change 3: operations changes the mail provider
std::cout << "sent to " << address << ": Invoice " << number_ << ", " << total() << " rupees\n";
}
};
int main() {
Invoice invoice(1001, "Asha", {{"Notebook", 3, 100}, {"Pen", 4, 50}});
std::cout << invoice.render() << "\n";
invoice.send("asha@example.com");
}// invoiceV1.js: one class that prices, prints and emails
class Invoice {
constructor(number, customer, lines) {
this.number = number;
this.customer = customer;
this.lines = lines; // [name, quantity, unit price in rupees]
}
total() { // reason to change 1: finance changes the tax rule
const subtotal = this.lines.reduce((sum, [, qty, price]) => sum + qty * price, 0);
return subtotal + Math.floor((subtotal * 18) / 100); // 18 percent tax, whole rupees
}
render() { // reason to change 2: design changes the layout
const rows = this.lines.map(([name, qty, price]) => `${name} x${qty} = ${qty * price}`);
return `Invoice ${this.number} for ${this.customer}\n${rows.join("\n")}\nTotal: ${this.total()}`;
}
send(address) { // reason to change 3: operations changes the mail provider
console.log(`sent to ${address}: Invoice ${this.number}, ${this.total()} rupees`);
}
}
const invoice = new Invoice(1001, "Asha", [["Notebook", 3, 100], ["Pen", 4, 50]]);
console.log(invoice.render());
invoice.send("asha@example.com");Invoice 1001 for Asha
Notebook x3 = 300
Pen x4 = 200
Total: 590
sent to asha@example.com: Invoice 1001, 590 rupeesThe numbers, so you can check them: 3 notebooks at 100 and 4 pens at 50 make a subtotal of 500; 18 percent tax is 90; the total is 590. Money is a whole number of rupees, never a float.
Nothing here is wrong today. The trouble is in the comments: the class has three reasons to change, and they belong to three different groups of people.
- One change, three people's code. When finance changes the tax rate, the edit lands in the class that also holds the layout and the mail call, and a typo there can break the printout.
- You cannot test one job alone. To check the tax you must build an invoice that also knows how to print and send. To check the layout you must pay for tax logic you do not care about.
- Merge conflicts. Finance and design edit the same file in the same week.
- A new variant has nowhere to go. A second layout (a thermal-printer receipt) or a second mail provider becomes an
ifinside the same methods.
Who asks for each change?
Finance wants the tax rate at 12 percent. Next quarter, design wants a new layout. Which methods of Invoice change each time, and what do the two changes have in common?
One strong answer
total() changes for finance, render() for design, and send() for operations. What the changes have in common is the class: all three land in the same file, owned by three groups. A class whose methods change for different reasons has more than one responsibility.
Single responsibility: one reason to change#
Robert C. Martin states the single responsibility principle as: a class should have one, and only one, reason to change. In his later wording, a "reason to change" is a group of people (an actor) who ask for changes, so the principle says: gather what changes for the same reason, and separate what changes for different reasons.
One reading of that is wrong, and interviewers hear it often: "a class should do one thing." That is a rule for functions. A class with six methods all concerned with the tax rule has one responsibility. A class with a single method that both computes a tax and writes a file has two.
The practical test: name who would ask for a change to this class. If the answer is "finance, and also design, and also operations", split it. If the answer is one group, leave it alone, however many methods it has.
Here is the invoice split along its three seams. The tax rule, the layout and the delivery each get a small class; the invoice keeps its data and its subtotal:
# invoice_srp.py: money rules, layout and delivery are three classes; the invoice is only data
from dataclasses import dataclass
@dataclass(frozen=True)
class Line:
name: str
qty: int
price: int # rupees
@dataclass(frozen=True)
class Invoice:
number: int
customer: str
lines: tuple
def subtotal(self) -> int:
return sum(line.qty * line.price for line in self.lines)
class TaxRule: # changes when finance changes the rate
def __init__(self, percent: int) -> None:
self.percent = percent
def total(self, invoice: Invoice) -> int:
subtotal = invoice.subtotal()
return subtotal + subtotal * self.percent // 100
class InvoicePrinter: # changes when design changes the layout
def render(self, invoice: Invoice, total: int) -> str:
rows = [f"{line.name} x{line.qty} = {line.qty * line.price}" for line in invoice.lines]
return f"Invoice {invoice.number} for {invoice.customer}\n" + "\n".join(rows) + f"\nTotal: {total}"
class Mailer: # changes when operations changes the mail provider
def send(self, address: str, subject: str) -> None:
print(f"sent to {address}: {subject}")
invoice = Invoice(1001, "Asha", (Line("Notebook", 3, 100), Line("Pen", 4, 50)))
tax = TaxRule(18)
total = tax.total(invoice)
print(InvoicePrinter().render(invoice, total))
Mailer().send("asha@example.com", f"Invoice {invoice.number}, {total} rupees")
print(f"total at 12 percent tax: {TaxRule(12).total(invoice)}")// InvoiceSrp.java: money rules, layout and delivery are three classes; the invoice is only data
import java.util.List;
class InvoiceSrp {
record Line(String name, int qty, int price) {} // price in rupees
record Invoice(int number, String customer, List<Line> lines) {
int subtotal() {
int sum = 0;
for (Line l : lines) sum += l.qty() * l.price();
return sum;
}
}
static final class TaxRule { // changes when finance changes the rate
private final int percent;
TaxRule(int percent) { this.percent = percent; }
int total(Invoice invoice) {
int subtotal = invoice.subtotal();
return subtotal + subtotal * percent / 100;
}
}
static final class InvoicePrinter { // changes when design changes the layout
String render(Invoice invoice, int total) {
StringBuilder out = new StringBuilder("Invoice " + invoice.number() + " for " + invoice.customer());
for (Line l : invoice.lines()) out.append("\n").append(l.name()).append(" x").append(l.qty()).append(" = ").append(l.qty() * l.price());
return out.append("\nTotal: ").append(total).toString();
}
}
static final class Mailer { // changes when operations changes the mail provider
void send(String address, String subject) { System.out.println("sent to " + address + ": " + subject); }
}
public static void main(String[] args) {
Invoice invoice = new Invoice(1001, "Asha", List.of(new Line("Notebook", 3, 100), new Line("Pen", 4, 50)));
TaxRule tax = new TaxRule(18);
int total = tax.total(invoice);
System.out.println(new InvoicePrinter().render(invoice, total));
new Mailer().send("asha@example.com", "Invoice " + invoice.number() + ", " + total + " rupees");
System.out.println("total at 12 percent tax: " + new TaxRule(12).total(invoice));
}
}// invoice_srp.cpp: money rules, layout and delivery are three classes; the invoice is only data
#include <iostream>
#include <string>
#include <utility>
#include <vector>
struct Line {
std::string name;
int qty;
int price; // rupees
};
struct Invoice {
int number;
std::string customer;
std::vector<Line> lines;
int subtotal() const {
int sum = 0;
for (const auto& l : lines) sum += l.qty * l.price;
return sum;
}
};
class TaxRule { // changes when finance changes the rate
int percent_;
public:
explicit TaxRule(int percent) : percent_(percent) {}
int total(const Invoice& invoice) const {
int subtotal = invoice.subtotal();
return subtotal + subtotal * percent_ / 100;
}
};
class InvoicePrinter { // changes when design changes the layout
public:
std::string render(const Invoice& invoice, int total) const {
std::string out = "Invoice " + std::to_string(invoice.number) + " for " + invoice.customer;
for (const auto& l : invoice.lines) out += "\n" + l.name + " x" + std::to_string(l.qty) + " = " + std::to_string(l.qty * l.price);
return out + "\nTotal: " + std::to_string(total);
}
};
class Mailer { // changes when operations changes the mail provider
public:
void send(const std::string& address, const std::string& subject) const {
std::cout << "sent to " << address << ": " << subject << "\n";
}
};
int main() {
Invoice invoice{1001, "Asha", {{"Notebook", 3, 100}, {"Pen", 4, 50}}};
TaxRule tax(18);
int total = tax.total(invoice);
std::cout << InvoicePrinter().render(invoice, total) << "\n";
Mailer().send("asha@example.com", "Invoice " + std::to_string(invoice.number) + ", " + std::to_string(total) + " rupees");
std::cout << "total at 12 percent tax: " << TaxRule(12).total(invoice) << "\n";
}// invoiceSrp.js: money rules, layout and delivery are three classes; the invoice is only data
class Invoice {
constructor(number, customer, lines) {
this.number = number;
this.customer = customer;
this.lines = lines; // [name, quantity, unit price in rupees]
Object.freeze(this);
}
subtotal() {
return this.lines.reduce((sum, [, qty, price]) => sum + qty * price, 0);
}
}
class TaxRule { // changes when finance changes the rate
constructor(percent) {
this.percent = percent;
}
total(invoice) {
const subtotal = invoice.subtotal();
return subtotal + Math.floor((subtotal * this.percent) / 100);
}
}
class InvoicePrinter { // changes when design changes the layout
render(invoice, total) {
const rows = invoice.lines.map(([name, qty, price]) => `${name} x${qty} = ${qty * price}`);
return `Invoice ${invoice.number} for ${invoice.customer}\n${rows.join("\n")}\nTotal: ${total}`;
}
}
class Mailer { // changes when operations changes the mail provider
send(address, subject) {
console.log(`sent to ${address}: ${subject}`);
}
}
const invoice = new Invoice(1001, "Asha", [["Notebook", 3, 100], ["Pen", 4, 50]]);
const total = new TaxRule(18).total(invoice);
console.log(new InvoicePrinter().render(invoice, total));
new Mailer().send("asha@example.com", `Invoice ${invoice.number}, ${total} rupees`);
console.log(`total at 12 percent tax: ${new TaxRule(12).total(invoice)}`);Invoice 1001 for Asha
Notebook x3 = 300
Pen x4 = 200
Total: 590
sent to asha@example.com: Invoice 1001, 590 rupees
total at 12 percent tax: 560The first five lines are exactly what the single class printed, so nothing a user sees has changed. The last line is the gain: finance's change is now one argument, TaxRule(12), in a class that knows nothing about layout or mail. 500 plus 12 percent (60) is 560.
%% title: Single responsibility: one class per reason to change
classDiagram
class Invoice {
+number int
+customer str
+lines tuple
+subtotal() int
}
class TaxRule {
+percent int
+total(invoice) int
}
class InvoicePrinter {
+render(invoice, total) str
}
class Mailer {
+send(address, subject)
}
TaxRule ..> Invoice : reads
InvoicePrinter ..> Invoice : readsRead the diagram. The three classes that change for different reasons do not point at each other: TaxRule and InvoicePrinter each read the Invoice, which is plain data, and Mailer needs nothing from the other two. A change finance asks for lands in TaxRule and nowhere else.
Each class can now be tested alone: the tax rule with a few invoices and no printer, the printer with a made-up total, and the mailer swapped for a fake with the same send(address, subject), so no test sends mail.
Open/closed: add a case without editing the old ones#
Bertrand Meyer's formulation (Object-Oriented Software Construction, 1988) is that software entities should be open for extension but closed for modification. In practice: a new case should arrive as new code, and the code that already works, and is already tested, should not be edited to make room for it. The way there is to put the part that varies behind an abstraction, so a new case is a new class that meets it. (Meyer meant extending a class by subclassing it; the reading used today, popularised by Robert Martin, extends through an interface.)
Here is what the opposite looks like, in a place where it happens all the time: coupon codes at checkout. The first coupon was an if. Then marketing asked for another, and another:
# coupons_v1.py: one function, and an elif for every coupon marketing has ever asked for
def apply_coupon(subtotal: int, code: str) -> int:
if code == "FLAT50":
return subtotal - min(50, subtotal)
elif code == "PCT20":
return subtotal - subtotal * 20 // 100
elif code == "FIRST": # added in March
return subtotal - min(120, subtotal * 30 // 100)
elif code == "FESTIVE": # added in October: the fourth time this function is opened
return subtotal - (75 if subtotal >= 400 else 0)
else:
raise ValueError(f"unknown coupon: {code}")
for code in ["FLAT50", "PCT20", "FIRST", "FESTIVE"]:
print(f"{code}: 500 -> {apply_coupon(500, code)}, 300 -> {apply_coupon(300, code)}")// CouponsV1.java: one method, and an else-if for every coupon marketing has ever asked for
class CouponsV1 {
static int applyCoupon(int subtotal, String code) {
if (code.equals("FLAT50")) {
return subtotal - Math.min(50, subtotal);
} else if (code.equals("PCT20")) {
return subtotal - subtotal * 20 / 100;
} else if (code.equals("FIRST")) { // added in March
return subtotal - Math.min(120, subtotal * 30 / 100);
} else if (code.equals("FESTIVE")) { // added in October: the fourth time this method is opened
return subtotal - (subtotal >= 400 ? 75 : 0);
} else {
throw new IllegalArgumentException("unknown coupon: " + code);
}
}
public static void main(String[] args) {
for (String code : new String[] {"FLAT50", "PCT20", "FIRST", "FESTIVE"}) {
System.out.println(code + ": 500 -> " + applyCoupon(500, code) + ", 300 -> " + applyCoupon(300, code));
}
}
}// coupons_v1.cpp: one function, and an else-if for every coupon marketing has ever asked for
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <string>
int applyCoupon(int subtotal, const std::string& code) {
if (code == "FLAT50") {
return subtotal - std::min(50, subtotal);
} else if (code == "PCT20") {
return subtotal - subtotal * 20 / 100;
} else if (code == "FIRST") { // added in March
return subtotal - std::min(120, subtotal * 30 / 100);
} else if (code == "FESTIVE") { // added in October: the fourth time this function is opened
return subtotal - (subtotal >= 400 ? 75 : 0);
} else {
throw std::invalid_argument("unknown coupon: " + code);
}
}
int main() {
for (const std::string code : {"FLAT50", "PCT20", "FIRST", "FESTIVE"}) {
std::cout << code << ": 500 -> " << applyCoupon(500, code) << ", 300 -> " << applyCoupon(300, code) << "\n";
}
}// couponsV1.js: one function, and an else-if for every coupon marketing has ever asked for
function applyCoupon(subtotal, code) {
if (code === "FLAT50") {
return subtotal - Math.min(50, subtotal);
} else if (code === "PCT20") {
return subtotal - Math.floor((subtotal * 20) / 100);
} else if (code === "FIRST") { // added in March
return subtotal - Math.min(120, Math.floor((subtotal * 30) / 100));
} else if (code === "FESTIVE") { // added in October: the fourth time this function is opened
return subtotal - (subtotal >= 400 ? 75 : 0);
} else {
throw new Error(`unknown coupon: ${code}`);
}
}
for (const code of ["FLAT50", "PCT20", "FIRST", "FESTIVE"]) {
console.log(`${code}: 500 -> ${applyCoupon(500, code)}, 300 -> ${applyCoupon(300, code)}`);
}FLAT50: 500 -> 450, 300 -> 250
PCT20: 500 -> 400, 300 -> 240
FIRST: 500 -> 380, 300 -> 210
FESTIVE: 500 -> 425, 300 -> 300Check one row: FIRST is 30 percent off with the discount capped at 120. On a 500-rupee cart, 30 percent is 150, which the cap cuts to 120, so the total is 380. On a 300-rupee cart, 30 percent is 90, under the cap, so the total is 210.
- Every new coupon reopens working code. A typo in the
FESTIVEbranch can breakFLAT50, so every release retests all four. - The cases cannot be tested alone. There is nothing to hand a test except a string and the whole ladder.
- It was predictable. By the third coupon the pattern was visible: coupons keep arriving.
A fifth coupon
Marketing asks for a fifth coupon: 10 percent off, but only on the first 200 rupees of the cart. In the ladder, what changes? What would you want to change instead?
One strong answer
In the ladder, apply_coupon is edited a fifth time. What you want is for the fifth coupon to be new code only: a new class that says how much discount it gives for a subtotal, plus one line where coupons are registered. The function that applies a coupon should not be opened, because the way a coupon works varies while the job (take a subtotal, give a discount) does not.
The fix is to make the job an interface and each coupon a class. It is the same move as in the Strategy lesson, seen from the principle's side: Strategy is how you get open/closed for a rule that varies.
# coupons_v2.py: each coupon is a class; applying one never changes
from abc import ABC, abstractmethod
class Coupon(ABC):
"""The job: how many rupees off a subtotal."""
@abstractmethod
def discount(self, subtotal: int) -> int: ...
class FlatOff(Coupon):
def __init__(self, amount: int) -> None:
self.amount = amount
def discount(self, subtotal: int) -> int:
return min(self.amount, subtotal)
class PercentOff(Coupon):
def __init__(self, percent: int, cap: int = None) -> None:
self.percent, self.cap = percent, cap
def discount(self, subtotal: int) -> int:
off = subtotal * self.percent // 100
return off if self.cap is None else min(off, self.cap)
class MinSpendFlat(Coupon):
def __init__(self, min_spend: int, amount: int) -> None:
self.min_spend, self.amount = min_spend, amount
def discount(self, subtotal: int) -> int:
return self.amount if subtotal >= self.min_spend else 0
COUPONS = {
"FLAT50": FlatOff(50),
"PCT20": PercentOff(20),
"FIRST": PercentOff(30, cap=120),
"FESTIVE": MinSpendFlat(400, 75),
}
def apply_coupon(subtotal: int, code: str) -> int: # closed: never edited to add a coupon
if code not in COUPONS:
raise ValueError(f"unknown coupon: {code}")
return subtotal - COUPONS[code].discount(subtotal)
# --- later, in another file: the fifth coupon is a new class and one registry line ---
class PercentOfFirst(Coupon):
"""percent off, but only on the first `limit` rupees of the cart"""
def __init__(self, limit: int, percent: int) -> None:
self.limit, self.percent = limit, percent
def discount(self, subtotal: int) -> int:
return min(subtotal, self.limit) * self.percent // 100
COUPONS["FIRST200"] = PercentOfFirst(200, 10)
for code in ["FLAT50", "PCT20", "FIRST", "FESTIVE", "FIRST200"]:
print(f"{code}: 500 -> {apply_coupon(500, code)}, 300 -> {apply_coupon(300, code)}")// CouponsV2.java: each coupon is a class; applying one never changes
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class CouponsV2 {
/** The job: how many rupees off a subtotal. */
interface Coupon {
int discount(int subtotal);
}
static final class FlatOff implements Coupon {
private final int amount;
FlatOff(int amount) { this.amount = amount; }
public int discount(int subtotal) { return Math.min(amount, subtotal); }
}
static final class PercentOff implements Coupon {
private final int percent, cap;
PercentOff(int percent, int cap) { this.percent = percent; this.cap = cap; }
PercentOff(int percent) { this(percent, Integer.MAX_VALUE); }
public int discount(int subtotal) { return Math.min(subtotal * percent / 100, cap); }
}
static final class MinSpendFlat implements Coupon {
private final int minSpend, amount;
MinSpendFlat(int minSpend, int amount) { this.minSpend = minSpend; this.amount = amount; }
public int discount(int subtotal) { return subtotal >= minSpend ? amount : 0; }
}
static final Map<String, Coupon> COUPONS = new HashMap<>(Map.of(
"FLAT50", new FlatOff(50),
"PCT20", new PercentOff(20),
"FIRST", new PercentOff(30, 120),
"FESTIVE", new MinSpendFlat(400, 75)));
static int applyCoupon(int subtotal, String code) { // closed: never edited to add a coupon
Coupon coupon = COUPONS.get(code);
if (coupon == null) throw new IllegalArgumentException("unknown coupon: " + code);
return subtotal - coupon.discount(subtotal);
}
// --- later, in another file: the fifth coupon is a new class and one registry line ---
/** percent off, but only on the first `limit` rupees of the cart */
static final class PercentOfFirst implements Coupon {
private final int limit, percent;
PercentOfFirst(int limit, int percent) { this.limit = limit; this.percent = percent; }
public int discount(int subtotal) { return Math.min(subtotal, limit) * percent / 100; }
}
public static void main(String[] args) {
COUPONS.put("FIRST200", new PercentOfFirst(200, 10));
for (String code : List.of("FLAT50", "PCT20", "FIRST", "FESTIVE", "FIRST200")) {
System.out.println(code + ": 500 -> " + applyCoupon(500, code) + ", 300 -> " + applyCoupon(300, code));
}
}
}// coupons_v2.cpp: each coupon is a class; applying one never changes
#include <algorithm>
#include <climits>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
// The job: how many rupees off a subtotal.
class Coupon {
public:
virtual ~Coupon() = default;
virtual int discount(int subtotal) const = 0;
};
class FlatOff : public Coupon {
int amount_;
public:
explicit FlatOff(int amount) : amount_(amount) {}
int discount(int subtotal) const override { return std::min(amount_, subtotal); }
};
class PercentOff : public Coupon {
int percent_, cap_;
public:
explicit PercentOff(int percent, int cap = INT_MAX) : percent_(percent), cap_(cap) {}
int discount(int subtotal) const override { return std::min(subtotal * percent_ / 100, cap_); }
};
class MinSpendFlat : public Coupon {
int minSpend_, amount_;
public:
MinSpendFlat(int minSpend, int amount) : minSpend_(minSpend), amount_(amount) {}
int discount(int subtotal) const override { return subtotal >= minSpend_ ? amount_ : 0; }
};
std::map<std::string, std::shared_ptr<const Coupon>> COUPONS = {
{"FLAT50", std::make_shared<FlatOff>(50)},
{"PCT20", std::make_shared<PercentOff>(20)},
{"FIRST", std::make_shared<PercentOff>(30, 120)},
{"FESTIVE", std::make_shared<MinSpendFlat>(400, 75)},
};
int applyCoupon(int subtotal, const std::string& code) { // closed: never edited to add a coupon
auto it = COUPONS.find(code);
if (it == COUPONS.end()) throw std::invalid_argument("unknown coupon: " + code);
return subtotal - it->second->discount(subtotal);
}
// --- later, in another file: the fifth coupon is a new class and one registry line ---
// percent off, but only on the first `limit` rupees of the cart
class PercentOfFirst : public Coupon {
int limit_, percent_;
public:
PercentOfFirst(int limit, int percent) : limit_(limit), percent_(percent) {}
int discount(int subtotal) const override { return std::min(subtotal, limit_) * percent_ / 100; }
};
int main() {
COUPONS["FIRST200"] = std::make_shared<PercentOfFirst>(200, 10);
for (const std::string code : {"FLAT50", "PCT20", "FIRST", "FESTIVE", "FIRST200"}) {
std::cout << code << ": 500 -> " << applyCoupon(500, code) << ", 300 -> " << applyCoupon(300, code) << "\n";
}
}// couponsV2.js: each coupon is a class; applying one never changes
// The job: any object with discount(subtotal), the rupees off. JavaScript has no interfaces,
// so the contract is the method name.
class FlatOff {
constructor(amount) {
this.amount = amount;
}
discount(subtotal) {
return Math.min(this.amount, subtotal);
}
}
class PercentOff {
constructor(percent, cap = Infinity) {
Object.assign(this, { percent, cap });
}
discount(subtotal) {
return Math.min(Math.floor((subtotal * this.percent) / 100), this.cap);
}
}
class MinSpendFlat {
constructor(minSpend, amount) {
Object.assign(this, { minSpend, amount });
}
discount(subtotal) {
return subtotal >= this.minSpend ? this.amount : 0;
}
}
const COUPONS = new Map([
["FLAT50", new FlatOff(50)],
["PCT20", new PercentOff(20)],
["FIRST", new PercentOff(30, 120)],
["FESTIVE", new MinSpendFlat(400, 75)],
]);
function applyCoupon(subtotal, code) { // closed: never edited to add a coupon
if (!COUPONS.has(code)) throw new Error(`unknown coupon: ${code}`);
return subtotal - COUPONS.get(code).discount(subtotal);
}
// --- later, in another file: the fifth coupon is a new class and one registry line ---
/** percent off, but only on the first `limit` rupees of the cart */
class PercentOfFirst {
constructor(limit, percent) {
Object.assign(this, { limit, percent });
}
discount(subtotal) {
return Math.floor((Math.min(subtotal, this.limit) * this.percent) / 100);
}
}
COUPONS.set("FIRST200", new PercentOfFirst(200, 10));
for (const code of ["FLAT50", "PCT20", "FIRST", "FESTIVE", "FIRST200"]) {
console.log(`${code}: 500 -> ${applyCoupon(500, code)}, 300 -> ${applyCoupon(300, code)}`);
}FLAT50: 500 -> 450, 300 -> 250
PCT20: 500 -> 400, 300 -> 240
FIRST: 500 -> 380, 300 -> 210
FESTIVE: 500 -> 425, 300 -> 300
FIRST200: 500 -> 480, 300 -> 280%% title: Open/closed: a coupon is a class behind one interface
classDiagram
class Coupon {
<<interface>>
+discount(subtotal) int
}
class FlatOff
class PercentOff
class MinSpendFlat
class PercentOfFirst
Coupon <|.. FlatOff
Coupon <|.. PercentOff
Coupon <|.. MinSpendFlat
Coupon <|.. PercentOfFirstapply_coupon points only at the interface, so adding PercentOfFirst (the bottom class, added later) draws one new triangle and changes no existing box.
The first four lines are the ladder's output, unchanged. The fifth is the new coupon: 10 percent of the first 200 rupees is 20, so a 500-rupee cart pays 480 and a 300-rupee cart pays 280. apply_coupon was not edited. What changed was a new class and one registry line, and that line is a real edit, worth naming: it lands in the wiring, a list of names, not in the code that prices a cart.
A coupon ladder is reopened for every new coupon. Put the discount behind an interface and a new coupon becomes a new class, with the apply function untouched.
Liskov substitution: a subclass keeps its parent's promises#
The Liskov substitution principle is named after Barbara Liskov (1987; stated formally by Liskov and Wing, 1994). In plain words: code written for a parent must keep working, unchanged, when it is handed any subclass. A subclass is a valid substitute only if it honours everything the parent promises, not just its method names.
That is a statement about behaviour, and it is the one the type checker cannot see. A subclass can have every method with the right signature and still break the promise:
| The parent promised | A subclass breaks it by | Example |
|---|---|---|
| What callers must give (a precondition) | demanding more | A withdraw that now needs the amount to be at least 100, where the parent accepted any positive amount |
| What it delivers (a postcondition) | delivering less | A withdraw that sometimes does nothing, where the parent always paid out |
| What is always true (an invariant) | letting it become false | A subclass that lets the balance go negative |
| How it can fail | failing in a new way | A withdraw that throws a "not supported" error callers never handle |
Here is the classic case, in an everyday setting. An Account promises that withdraw(n) succeeds whenever 0 < n <= balance. Someone adds a ViewOnlyAccount, shared with an accountant who may look but not touch, and makes it a subclass so it "is an account":
# accounts_v1.py: a view-only account that inherits withdraw and then refuses it
class Account:
"""Promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance."""
def __init__(self, name: str, balance: int) -> None:
self.name, self.balance = name, balance
def withdraw(self, rupees: int) -> None:
if rupees <= 0 or rupees > self.balance:
raise ValueError("invalid withdrawal")
self.balance -= rupees
class ViewOnlyAccount(Account): # shared with an accountant: look, don't touch
def withdraw(self, rupees: int) -> None:
raise PermissionError("view-only account cannot withdraw")
def pay_bills(accounts: list, rupees: int) -> None: # written against Account's promise
for account in accounts:
account.withdraw(rupees)
print(f"{account.name}: paid {rupees}, balance {account.balance}")
accounts = [Account("savings", 1000), ViewOnlyAccount("accountant-view", 1200), Account("current", 500)]
try:
pay_bills(accounts, 300)
except Exception as e:
print("bill run stopped:", e)
print("current: never reached, balance", accounts[2].balance)// AccountsV1.java: a view-only account that inherits withdraw and then refuses it
import java.util.List;
class AccountsV1 {
/** Promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance. */
static class Account {
final String name;
int balance;
Account(String name, int balance) { this.name = name; this.balance = balance; }
void withdraw(int rupees) {
if (rupees <= 0 || rupees > balance) throw new IllegalArgumentException("invalid withdrawal");
balance -= rupees;
}
}
static final class ViewOnlyAccount extends Account { // shared with an accountant: look, don't touch
ViewOnlyAccount(String name, int balance) { super(name, balance); }
@Override void withdraw(int rupees) { throw new UnsupportedOperationException("view-only account cannot withdraw"); }
}
static void payBills(List<Account> accounts, int rupees) { // written against Account's promise
for (Account account : accounts) {
account.withdraw(rupees);
System.out.println(account.name + ": paid " + rupees + ", balance " + account.balance);
}
}
public static void main(String[] args) {
List<Account> accounts = List.of(new Account("savings", 1000), new ViewOnlyAccount("accountant-view", 1200), new Account("current", 500));
try {
payBills(accounts, 300);
} catch (RuntimeException e) {
System.out.println("bill run stopped: " + e.getMessage());
}
System.out.println("current: never reached, balance " + accounts.get(2).balance);
}
}// accounts_v1.cpp: a view-only account that inherits withdraw and then refuses it
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
// Promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance.
class Account {
public:
std::string name;
int balance;
Account(std::string n, int b) : name(std::move(n)), balance(b) {}
virtual ~Account() = default;
virtual void withdraw(int rupees) {
if (rupees <= 0 || rupees > balance) throw std::invalid_argument("invalid withdrawal");
balance -= rupees;
}
};
class ViewOnlyAccount : public Account { // shared with an accountant: look, don't touch
public:
using Account::Account;
void withdraw(int) override { throw std::logic_error("view-only account cannot withdraw"); }
};
void payBills(const std::vector<std::unique_ptr<Account>>& accounts, int rupees) { // written against Account's promise
for (const auto& account : accounts) {
account->withdraw(rupees);
std::cout << account->name << ": paid " << rupees << ", balance " << account->balance << "\n";
}
}
int main() {
std::vector<std::unique_ptr<Account>> accounts;
accounts.push_back(std::make_unique<Account>("savings", 1000));
accounts.push_back(std::make_unique<ViewOnlyAccount>("accountant-view", 1200));
accounts.push_back(std::make_unique<Account>("current", 500));
try {
payBills(accounts, 300);
} catch (const std::exception& e) {
std::cout << "bill run stopped: " << e.what() << "\n";
}
std::cout << "current: never reached, balance " << accounts[2]->balance << "\n";
}// accountsV1.js: a view-only account that inherits withdraw and then refuses it
class Account {
// Promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance.
constructor(name, balance) {
this.name = name;
this.balance = balance;
}
withdraw(rupees) {
if (rupees <= 0 || rupees > this.balance) throw new Error("invalid withdrawal");
this.balance -= rupees;
}
}
class ViewOnlyAccount extends Account { // shared with an accountant: look, don't touch
withdraw(rupees) {
throw new Error("view-only account cannot withdraw");
}
}
function payBills(accounts, rupees) { // written against Account's promise
for (const account of accounts) {
account.withdraw(rupees);
console.log(`${account.name}: paid ${rupees}, balance ${account.balance}`);
}
}
const accounts = [new Account("savings", 1000), new ViewOnlyAccount("accountant-view", 1200), new Account("current", 500)];
try {
payBills(accounts, 300);
} catch (e) {
console.log("bill run stopped:", e.message);
}
console.log("current: never reached, balance", accounts[2].balance);savings: paid 300, balance 700
bill run stopped: view-only account cannot withdraw
current: never reached, balance 500Everything compiled and ran. The type checker was satisfied: a ViewOnlyAccount is an Account in every signature. But pay_bills was written against Account's promise, and the subclass broke it. The bill run stopped halfway and the current account was never paid: the failure shows up in code that did nothing wrong.
- Callers must check the type. Anywhere that takes an
Account, someone addsif not isinstance(a, ViewOnlyAccount). Every such check is the polymorphism failing. - Tests of the parent do not cover it. The
Accounttests pass; no test ever handspay_billsa view-only account. - It spreads. "Not supported" subclasses breed more type checks and more of the if-ladder the hierarchy was meant to avoid.
The fix is to stop claiming the thing is something it is not. What the view-only account can promise is looking, so that is the parent. Withdrawing is an extra promise, made only by the accounts that keep it:
# accounts_v2.py: looking and withdrawing are different promises, so they are different types
class AccountView:
"""Promise: you can read the name and the balance."""
def __init__(self, name: str, balance: int) -> None:
self.name = name
self._balance = balance
@property
def balance(self) -> int:
return self._balance
class Account(AccountView):
"""Adds a promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance."""
def withdraw(self, rupees: int) -> None:
if rupees <= 0 or rupees > self._balance:
raise ValueError("invalid withdrawal")
self._balance -= rupees
class ViewOnlyAccount(AccountView): # looks, and promises nothing more
pass
def pay_bills(accounts: list, rupees: int) -> None: # only things that keep the withdraw promise
for account in accounts:
account.withdraw(rupees)
print(f"{account.name}: paid {rupees}, balance {account.balance}")
def report(views: list) -> str: # works for any account view
return ", ".join(f"{v.name} {v.balance}" for v in views)
payable = [Account("savings", 1000), Account("current", 500)]
pay_bills(payable, 300)
print("report:", report(payable + [ViewOnlyAccount("accountant-view", 1200)]))// AccountsV2.java: looking and withdrawing are different promises, so they are different types
import java.util.ArrayList;
import java.util.List;
class AccountsV2 {
/** Promise: you can read the name and the balance. */
static class AccountView {
final String name;
protected int balance;
AccountView(String name, int balance) { this.name = name; this.balance = balance; }
int balance() { return balance; }
}
/** Adds a promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance. */
static final class Account extends AccountView {
Account(String name, int balance) { super(name, balance); }
void withdraw(int rupees) {
if (rupees <= 0 || rupees > balance) throw new IllegalArgumentException("invalid withdrawal");
balance -= rupees;
}
}
static final class ViewOnlyAccount extends AccountView { // looks, and promises nothing more
ViewOnlyAccount(String name, int balance) { super(name, balance); }
}
static void payBills(List<Account> accounts, int rupees) { // only things that keep the withdraw promise
for (Account account : accounts) {
account.withdraw(rupees);
System.out.println(account.name + ": paid " + rupees + ", balance " + account.balance());
}
}
static String report(List<? extends AccountView> views) { // works for any account view
List<String> parts = new ArrayList<>();
for (AccountView v : views) parts.add(v.name + " " + v.balance());
return String.join(", ", parts);
}
public static void main(String[] args) {
List<Account> payable = List.of(new Account("savings", 1000), new Account("current", 500));
payBills(payable, 300);
List<AccountView> all = new ArrayList<>(payable);
all.add(new ViewOnlyAccount("accountant-view", 1200));
System.out.println("report: " + report(all));
}
}// accounts_v2.cpp: looking and withdrawing are different promises, so they are different types
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
// Promise: you can read the name and the balance.
class AccountView {
protected:
int balance_;
public:
std::string name;
AccountView(std::string n, int b) : balance_(b), name(std::move(n)) {}
virtual ~AccountView() = default;
int balance() const { return balance_; }
};
// Adds a promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance.
class Account : public AccountView {
public:
using AccountView::AccountView;
void withdraw(int rupees) {
if (rupees <= 0 || rupees > balance_) throw std::invalid_argument("invalid withdrawal");
balance_ -= rupees;
}
};
class ViewOnlyAccount : public AccountView { // looks, and promises nothing more
public:
using AccountView::AccountView;
};
void payBills(std::vector<Account>& accounts, int rupees) { // only things that keep the withdraw promise
for (auto& account : accounts) {
account.withdraw(rupees);
std::cout << account.name << ": paid " << rupees << ", balance " << account.balance() << "\n";
}
}
std::string report(const std::vector<const AccountView*>& views) { // works for any account view
std::string out;
for (const auto* v : views) out += (out.empty() ? "" : ", ") + v->name + " " + std::to_string(v->balance());
return out;
}
int main() {
std::vector<Account> payable = {Account("savings", 1000), Account("current", 500)};
payBills(payable, 300);
ViewOnlyAccount view("accountant-view", 1200);
std::cout << "report: " << report({&payable[0], &payable[1], &view}) << "\n";
}// accountsV2.js: looking and withdrawing are different promises, so they are different types
class AccountView {
// Promise: you can read the name and the balance.
constructor(name, balance) {
this.name = name;
this._balance = balance; // the underscore means "for this class and its subclasses" (a convention)
}
get balance() {
return this._balance;
}
}
class Account extends AccountView {
// Adds a promise: withdraw(rupees) succeeds whenever 0 < rupees <= balance.
withdraw(rupees) {
if (rupees <= 0 || rupees > this._balance) throw new Error("invalid withdrawal");
this._balance -= rupees;
}
}
class ViewOnlyAccount extends AccountView {} // looks, and promises nothing more
function payBills(accounts, rupees) { // only things that keep the withdraw promise
for (const account of accounts) {
account.withdraw(rupees);
console.log(`${account.name}: paid ${rupees}, balance ${account.balance}`);
}
}
function report(views) { // works for any account view
return views.map((v) => `${v.name} ${v.balance}`).join(", ");
}
const payable = [new Account("savings", 1000), new Account("current", 500)];
payBills(payable, 300);
console.log("report:", report([...payable, new ViewOnlyAccount("accountant-view", 1200)]));savings: paid 300, balance 700
current: paid 300, balance 200
report: savings 700, current 200, accountant-view 1200%% title: Liskov: looking and withdrawing are different promises
classDiagram
class AccountView {
+name str
+balance int
}
class Account {
+withdraw(rupees)
}
class ViewOnlyAccount
AccountView <|-- Account
AccountView <|-- ViewOnlyAccountAccount is still a kind of AccountView: anything that only looks works with every account, and report takes a view-only one too. What ViewOnlyAccount no longer does is claim a promise it cannot keep. In Java, C++ and TypeScript the compiler now stops someone passing it to pay_bills. (In Python and JavaScript the call fails only at run time, with "no such method"; a type checker such as mypy, or a test, catches it earlier.)
Not every subclass is a violation. A SavingsAccount(Account) that adds interest and leaves withdraw exactly as promised is a valid substitute. Subclassing is allowed; breaking the parent's promises is not.
The classic interview version is the square and the rectangle. Geometry says a square is a rectangle, so Square(Rectangle) looks right. But a mutable Rectangle promises that set_width leaves the height alone, and a square cannot keep that promise. Code written for rectangles (set the width to 5, the height to 4, expect an area of 20) gets 16 from a square. The real-world "is-a" does not survive the parent's promises. Make the shapes immutable, or give both a Shape parent with only area(), and the conflict goes away.
Two more notes. The promise is the parent's contract: if Account's documented contract were "withdraw may be refused", then refusing is within it, and callers would be written to handle refusal. The violation is always relative to what the parent promised, which is why the promise belongs in the docstring. And a practical test: run the parent's tests against every subclass. A subclass that fails a test the parent passes is not a substitute.
Is it a substitute?
A MinimumWithdrawAccount extends Account and refuses any withdrawal under 100 rupees. Does it keep Account's promise that withdraw(n) succeeds whenever 0 < n <= balance?
One strong answer
No. It demands more from callers than the parent did: a withdrawal of 50 rupees from a balance of 500 is inside the parent's promise and now fails. A subclass may loosen what it accepts or tighten what it delivers, never the reverse. If a minimum is a real business rule, it belongs in a different type, or in a policy the caller can see, not in a class that claims to be an ordinary Account.
A showroom socket looks real but isn't wired: every plug fits and nothing gets power, like a subclass that breaks its parent's promise.
How the three fit together#
The three principles answer three questions about the same design:
| Principle | The question | The tool in this lesson |
|---|---|---|
| Single responsibility | Who asks for changes to this class? | Split Invoice into the tax rule, the printer and the mailer |
| Open/closed | How do I add a case without editing old code? | An interface, so a new coupon is a new class |
| Liskov substitution | Is a subclass safe to hand to code written for its parent? | Split looking from withdrawing |
SRP finds the seams. OCP lets you add behaviour on a seam by adding code. LSP keeps the classes you add safe to use. They lean on one another: the coupon interface is only open/closed if every coupon keeps the same promise (a Coupon that returns more than the subtotal would break apply_coupon), and that is Liskov.
Where it shows up in the case studies#
| Case study | Principle | How |
|---|---|---|
| Parking lot | Open/closed | A weekend tariff is a new PricingStrategy class passed to the constructor; ParkingLot is not edited |
| Vending machine | Open/closed, and Liskov | Each state is one small class, so "maintenance mode" is a new class, not a new branch in four methods. The base State refuses every event by default, so every state answers every event: an unhandled one is a safe refusal, never a crash |
| Chess | Liskov | The game calls moves on every piece the same way and each subclass answers in kind, so that loop never branches on the piece's kind |
Single responsibility is the one the parking lot names as a mistake: its common mistakes list a ParkingLot god class "with the pricing if/elif chain, the spot search and the printing all inside park()", which is the invoice from this lesson in another costume.
When not to use it#
These principles are guidelines, not laws, and each has a way to be taken too far:
- One method per class (SRP). Splitting until every class has a single method turns a 40-line invoice into 14 files and a reader has to open them all to follow one request. A responsibility is a reason to change, not a method. If one group of people would always change two things together, keep them together.
- Splitting before anything varies (SRP). A small class with one owner and no second layout in sight does not need a printer class yet. Split when a second reason to change appears, which is cheap.
- An interface for every
if(OCP). Abstracting before a second case exists is guesswork and adds files for nothing. Two fixed cases that come from the problem (ascending or descending) are anif. Wait for the evidence: the third coupon is when the ladder has earned its refactor. - Claiming to be closed to everything (OCP). Say which variation the design is closed against, and accept that an unexpected change means an edit.
- Banning inheritance (LSP). The principle does not say "never subclass"; it says a subclass must keep its parent's promises.
InsufficientFundsextendingWalletErrorandSavingsAccountextendingAccountare fine. A hierarchy of "not supported" overrides is the smell.
A responsibility is a reason to change, not a method: one method per class turns a 40 line invoice into 14 files.
In the interview#
Name the principle with the reason, in one breath: "The invoice has three reasons to change, tax, layout and mail, owned by three groups, so I split it into a tax rule, a printer and a mailer. The coupon rule varies, so it is an interface and a new coupon is a new class, with apply_coupon never edited. And any subclass has to keep its parent's promises, so a view-only account is not an Account."
Then expect the follow-ups:
- "What exactly is a responsibility?" A reason to change, tied to a group of people who ask for the change. Not "one method" and not "one thing". Ask who would request a change to this class.
- "Isn't open/closed impossible? You always edit something." You edit the wiring: the registry, the constructor call. The principle is that the logic that works is not reopened. And it is relative to a variation you chose; say which one.
- "Give an example of breaking Liskov." A subclass that throws "not supported" from an inherited method (a view-only account whose
withdrawthrows), one that demands a bigger minimum than its parent, or a square that extends a mutable rectangle. Callers written for the parent then break, far from the cause. - "How do you spot a Liskov violation?" Any
isinstancecheck in code that takes the parent type, any override that throws "not supported", and a subclass that fails the parent's tests. - "Does Strategy follow from these?" Yes. Open/closed says a varying rule should be added as new code; an interface plus a class per rule is the Strategy pattern, and the Strategy lesson builds it.
Common mistakes#
- "A class should do one thing." That is the rule for a function. A class's single responsibility is one reason to change.
- A class per method, then a design with 14 files and no reason.
- An interface with one implementation and no second case in sight, justified as "open/closed".
- A subclass that throws "not supported", or tightens a rule the parent accepted, then an
isinstancecheck in the caller to work around it. - Calling inheritance bad because of Liskov. The principle is about keeping promises.
SRP, OCP and LSP
Q1/4 An Invoice class has total(), render() and send(). Finance changes the tax, design changes the layout, operations changes the mail provider. What does that tell you?
Answer & explanation
Answer: B
A responsibility is a reason to change, tied to who asks for the change. Three groups editing one class is the signal to split it. One method per class is the over-application: split by owner, not by method count.
Q2/4 apply_coupon has been reopened four times, once for each coupon. What is the open/closed fix?
Answer & explanation
Answer: C
Open/closed means a new case arrives as new code, without reopening the code that works. An interface for the varying rule, a class per coupon and a registry line gives that. Moving or commenting the ladder still edits it every time.
Q3/4 A ViewOnlyAccount extends Account and overrides withdraw to throw. pay_bills(accounts) calls withdraw on each. Which statement is right?
Answer & explanation
Answer: C
Liskov is about behaviour, not signatures. Code written for the parent must keep working with any subclass. A subclass that refuses an operation the parent promised breaks that, and the failure shows up in the caller.
Q4/4 Which statement about these principles is correct?
Answer & explanation
Answer: C
Closed means closed against a chosen kind of change, never all changes. A responsibility is a reason to change, not a method. Liskov constrains subclasses to keep their parent's promises, and inheritance that does is fine.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


