The forces behind every pattern in this part, and the case for not adding one: how much a class has to know about its neighbours, whether its parts belong together, and when copying, simplifying or waiting is the better engineering.
Your delivery app sends a text when an order is placed, accepted, picked up and delivered. Someone wrote the first message, someone copied it for the second, and by the fourth the wording had drifted. Now marketing wants a tracking link in every message. How many places do you edit, and how do you know you found them all?
That is one of five forces that decide whether a design is cheap or expensive to change: coupling, cohesion, DRY, KISS and YAGNI. They are not patterns. They are the reasons patterns exist, and the reasons not to use one. Every later lesson in this part is a tool for relieving one of them, and each lesson will ask you to say which. You have met them in the parking lot (the god class), the vending machine ("say it, don't build it") and the file system (one shape for every operation).
A junk drawer holds batteries, menus and one sock, with nothing in common but the drawer. A cohesive class is a drawer with one job.
The first version: the same SMS in four places#
The first version is four small handlers. Each builds its own text and sends it. Each is simple, and they all know the same things:
# sms_v1.py: the order-status SMS, written out in four handlers
def send_sms(phone: str, text: str) -> None:
print(f"sms to {phone}: {text}")
def on_placed(order: dict) -> None:
send_sms(order["phone"], f"Hi {order['name']}, your order #{order['id']} is placed. Total {order['total']} rupees.")
def on_accepted(order: dict) -> None:
send_sms(order["phone"], f"Hi {order['name']}, your order #{order['id']} is accepted by the restaurant.")
def on_picked_up(order: dict) -> None:
send_sms(order["phone"], f"Hi {order['name']}, order {order['id']} is on its way.") # copied, then edited by hand: drifted
def on_delivered(order: dict) -> None:
send_sms(order["phone"], f"Hi {order['name']}, your order #{order['id']} was delivered. Enjoy!")
order = {"id": 1001, "name": "Asha", "phone": "90000-00001", "total": 590}
for handler in (on_placed, on_accepted, on_picked_up, on_delivered):
handler(order)// SmsV1.java: the order-status SMS, written out in four handlers
class SmsV1 {
record Order(int id, String name, String phone, int total) {}
static void sendSms(String phone, String text) { System.out.println("sms to " + phone + ": " + text); }
static void onPlaced(Order o) { sendSms(o.phone(), "Hi " + o.name() + ", your order #" + o.id() + " is placed. Total " + o.total() + " rupees."); }
static void onAccepted(Order o) { sendSms(o.phone(), "Hi " + o.name() + ", your order #" + o.id() + " is accepted by the restaurant."); }
static void onPickedUp(Order o) { sendSms(o.phone(), "Hi " + o.name() + ", order " + o.id() + " is on its way."); } // copied, then edited by hand: drifted
static void onDelivered(Order o) { sendSms(o.phone(), "Hi " + o.name() + ", your order #" + o.id() + " was delivered. Enjoy!"); }
public static void main(String[] args) {
Order order = new Order(1001, "Asha", "90000-00001", 590);
onPlaced(order);
onAccepted(order);
onPickedUp(order);
onDelivered(order);
}
}// sms_v1.cpp: the order-status SMS, written out in four handlers
#include <iostream>
#include <string>
struct Order {
int id;
std::string name;
std::string phone;
int total;
};
void send_sms(const std::string& phone, const std::string& text) { std::cout << "sms to " << phone << ": " << text << "\n"; }
void on_placed(const Order& o) {
send_sms(o.phone, "Hi " + o.name + ", your order #" + std::to_string(o.id) + " is placed. Total " + std::to_string(o.total) + " rupees.");
}
void on_accepted(const Order& o) { send_sms(o.phone, "Hi " + o.name + ", your order #" + std::to_string(o.id) + " is accepted by the restaurant."); }
void on_picked_up(const Order& o) { send_sms(o.phone, "Hi " + o.name + ", order " + std::to_string(o.id) + " is on its way."); } // copied, then edited by hand: drifted
void on_delivered(const Order& o) { send_sms(o.phone, "Hi " + o.name + ", your order #" + std::to_string(o.id) + " was delivered. Enjoy!"); }
int main() {
Order order{1001, "Asha", "90000-00001", 590};
on_placed(order);
on_accepted(order);
on_picked_up(order);
on_delivered(order);
}// smsV1.js: the order-status SMS, written out in four handlers
function sendSms(phone, text) {
console.log(`sms to ${phone}: ${text}`);
}
function onPlaced(order) {
sendSms(order.phone, `Hi ${order.name}, your order #${order.id} is placed. Total ${order.total} rupees.`);
}
function onAccepted(order) {
sendSms(order.phone, `Hi ${order.name}, your order #${order.id} is accepted by the restaurant.`);
}
function onPickedUp(order) {
sendSms(order.phone, `Hi ${order.name}, order ${order.id} is on its way.`); // copied, then edited by hand: drifted
}
function onDelivered(order) {
sendSms(order.phone, `Hi ${order.name}, your order #${order.id} was delivered. Enjoy!`);
}
const order = { id: 1001, name: "Asha", phone: "90000-00001", total: 590 };
for (const handler of [onPlaced, onAccepted, onPickedUp, onDelivered]) handler(order);sms to 90000-00001: Hi Asha, your order #1001 is placed. Total 590 rupees.
sms to 90000-00001: Hi Asha, your order #1001 is accepted by the restaurant.
sms to 90000-00001: Hi Asha, order 1001 is on its way.
sms to 90000-00001: Hi Asha, your order #1001 was delivered. Enjoy!Read the third line: on_picked_up says "order 1001" where the others say "your order #1001". The four handlers all contain the same piece of knowledge, how an order-status SMS is worded and addressed, and now they disagree about it.
- One change, four edits. The tracking link goes into every handler, and you must find all of them. A handler in a file nobody remembered keeps the old text.
- Copies drift. The third message is the proof: nobody decided it should differ, and it does.
- Bugs are copied too. A wrong phone field is wrong four times, and fixed once, in the copy someone happened to notice.
- A reader cannot tell what is deliberate. Is "order 1001" a different wording on purpose? Only a search and a guess will say.
How many edits?
Marketing wants every status message to end with a tracking link. In the first version, how many handlers change, and what is the quiet risk?
One strong answer
Four handlers change, and the risk is the one you do not find. A fifth place, a script or a test that builds the same text, keeps the old wording and nobody sees it until a customer does. The cause is that one piece of knowledge, the message format, has no single home. Giving it one is what the next section is about.
DRY: one place for each piece of knowledge#
Andrew Hunt and David Thomas stated DRY ("don't repeat yourself") in The Pragmatic Programmer (1999) as: every piece of knowledge must have a single, unambiguous, authoritative representation within a system. The key word is knowledge. DRY is not "never write the same line twice". It says that when a fact or a rule is true of your system, it should be written down in one place, so that changing it means one edit.
Here the messages keep their own wording (that really differs per status) and share everything else: how a greeting and an order id are written, and who receives the text:
# sms_v2.py: one place builds an order-status SMS; each status only supplies its own wording
STATUS_TEXT = {
"placed": "is placed. Total {total} rupees.",
"accepted": "is accepted by the restaurant.",
"picked_up": "is on its way.",
"delivered": "was delivered. Enjoy!",
}
def status_sms(order: dict, status: str, tracking: str = "") -> str: # the greeting and the id format live here only
text = STATUS_TEXT[status].replace("{total}", str(order["total"]))
sms = f"Hi {order['name']}, your order #{order['id']} {text}"
return f"{sms} Track: {tracking}" if tracking else sms
def notify(order: dict, status: str, tracking: str = "") -> None: # the recipient rule lives here only
print(f"sms to {order['phone']}: {status_sms(order, status, tracking)}")
order = {"id": 1001, "name": "Asha", "phone": "90000-00001", "total": 590}
for status in STATUS_TEXT:
notify(order, status)
print("with a tracking link, one new parameter in one place:")
for status in STATUS_TEXT:
notify(order, status, "t.example/1001")// SmsV2.java: one place builds an order-status SMS; each status only supplies its own wording
import java.util.LinkedHashMap;
import java.util.Map;
class SmsV2 {
record Order(int id, String name, String phone, int total) {}
static final Map<String, String> STATUS_TEXT = new LinkedHashMap<>();
static {
STATUS_TEXT.put("placed", "is placed. Total {total} rupees.");
STATUS_TEXT.put("accepted", "is accepted by the restaurant.");
STATUS_TEXT.put("picked_up", "is on its way.");
STATUS_TEXT.put("delivered", "was delivered. Enjoy!");
}
static String statusSms(Order o, String status, String tracking) { // the greeting and the id format live here only
String text = STATUS_TEXT.get(status).replace("{total}", String.valueOf(o.total()));
String sms = "Hi " + o.name() + ", your order #" + o.id() + " " + text;
return tracking.isEmpty() ? sms : sms + " Track: " + tracking;
}
static void notify(Order o, String status, String tracking) { // the recipient rule lives here only
System.out.println("sms to " + o.phone() + ": " + statusSms(o, status, tracking));
}
public static void main(String[] args) {
Order order = new Order(1001, "Asha", "90000-00001", 590);
for (String status : STATUS_TEXT.keySet()) notify(order, status, "");
System.out.println("with a tracking link, one new parameter in one place:");
for (String status : STATUS_TEXT.keySet()) notify(order, status, "t.example/1001");
}
}// sms_v2.cpp: one place builds an order-status SMS; each status only supplies its own wording
#include <iostream>
#include <string>
#include <utility>
#include <vector>
struct Order {
int id;
std::string name;
std::string phone;
int total;
};
const std::vector<std::pair<std::string, std::string>> STATUS_TEXT = {
{"placed", "is placed. Total {total} rupees."},
{"accepted", "is accepted by the restaurant."},
{"picked_up", "is on its way."},
{"delivered", "was delivered. Enjoy!"},
};
std::string status_sms(const Order& o, std::string text, const std::string& tracking) { // the greeting and the id format live here only
auto at = text.find("{total}");
if (at != std::string::npos) text.replace(at, 7, std::to_string(o.total));
std::string sms = "Hi " + o.name + ", your order #" + std::to_string(o.id) + " " + text;
return tracking.empty() ? sms : sms + " Track: " + tracking;
}
void notify(const Order& o, const std::string& text, const std::string& tracking) { // the recipient rule lives here only
std::cout << "sms to " << o.phone << ": " << status_sms(o, text, tracking) << "\n";
}
int main() {
Order order{1001, "Asha", "90000-00001", 590};
for (const auto& s : STATUS_TEXT) notify(order, s.second, "");
std::cout << "with a tracking link, one new parameter in one place:\n";
for (const auto& s : STATUS_TEXT) notify(order, s.second, "t.example/1001");
}// smsV2.js: one place builds an order-status SMS; each status only supplies its own wording
const STATUS_TEXT = {
placed: "is placed. Total {total} rupees.",
accepted: "is accepted by the restaurant.",
picked_up: "is on its way.",
delivered: "was delivered. Enjoy!",
};
function statusSms(order, status, tracking = "") { // the greeting and the id format live here only
const text = STATUS_TEXT[status].replace("{total}", String(order.total));
const sms = `Hi ${order.name}, your order #${order.id} ${text}`;
return tracking ? `${sms} Track: ${tracking}` : sms;
}
function notify(order, status, tracking = "") { // the recipient rule lives here only
console.log(`sms to ${order.phone}: ${statusSms(order, status, tracking)}`);
}
const order = { id: 1001, name: "Asha", phone: "90000-00001", total: 590 };
for (const status of Object.keys(STATUS_TEXT)) notify(order, status);
console.log("with a tracking link, one new parameter in one place:");
for (const status of Object.keys(STATUS_TEXT)) notify(order, status, "t.example/1001");sms to 90000-00001: Hi Asha, your order #1001 is placed. Total 590 rupees.
sms to 90000-00001: Hi Asha, your order #1001 is accepted by the restaurant.
sms to 90000-00001: Hi Asha, your order #1001 is on its way.
sms to 90000-00001: Hi Asha, your order #1001 was delivered. Enjoy!
with a tracking link, one new parameter in one place:
sms to 90000-00001: Hi Asha, your order #1001 is placed. Total 590 rupees. Track: t.example/1001
sms to 90000-00001: Hi Asha, your order #1001 is accepted by the restaurant. Track: t.example/1001
sms to 90000-00001: Hi Asha, your order #1001 is on its way. Track: t.example/1001
sms to 90000-00001: Hi Asha, your order #1001 was delivered. Enjoy! Track: t.example/1001The numbers, so you can check them: the order is 590 rupees, and only the placed text mentions the total. The first four lines are what the copies printed, with one difference, and it is the point: the third now reads "your order #1001 is on its way", the same as the rest, because there is only one greeting to get wrong. The last four lines are the new feature, the tracking link: one optional parameter on the shared builder, passed through by notify, and no status's wording touched.
That second case is real, and it is where over-eager DRY hurts. Suppose a senior discount and a student discount are both 10 percent off. The two functions look identical, so someone merges them into one concession(price). Then students get 15 percent, so the function gets a kind flag; then seniors are excluded during a sale, so it gets a second flag. The merged function now has branches for two ideas that were never one. Sandi Metz put it as: duplication is far cheaper than the wrong abstraction. The fix is to un-merge: two small functions that happen to start the same.
Three questions help you tell them apart:
| Question | If yes | If no |
|---|---|---|
| Will a change to one always be a change to the other? | Same knowledge: share it | Coincidence: leave the copies |
| Do the copies have the same owner and reason to change? (the first lesson's test) | Share it | Keep them apart |
| Not sure yet? Is this the third copy? (the rule of three, from Don Roberts in Martin Fowler's Refactoring) | Now you can see the real shape: refactor | Two copies are cheap; wait |
Four handlers each built the same text. Move the greeting and the id format into one builder, and a tracking link is one edit.
Coupling: how much one part has to know about another#
Coupling is the degree to which one part of a system depends on another: how much it has to know about it, and so how likely a change in the other is to force a change in this one. The terms come from the structured-design work of the 1970s (Wayne Stevens, Glenford Myers and Larry Constantine). Some coupling is unavoidable, since parts that cooperate must know something about each other. The goal is to keep it small, explicit and pointed at things that are stable.
Here is the classic scale, from the tightest to the loosest:
| Kind | What the caller depends on | Example |
|---|---|---|
| Content | Another part's internals, read or changed behind its back | Code that appends to order._items directly, skipping the Order methods that keep the total right |
| Common | A shared mutable global | Two classes both reading and writing one module-level settings object |
| External | A format or protocol imposed from outside, known in several places | Three modules that each parse the vendor's settlement file by column position |
| Control | Which branch the other should take | Passing a flag such as kind="student" that makes the callee behave differently |
| Stamp | A whole structure, though it needs one field | Passing a full Order to a function that only needs the city |
| Data | Just the values it needs | delivery_fee(origin, destination), with only what the rule uses |
The scale is about modules passing things to each other. Objects add one more way to couple: knowing how other objects are put together. The Law of Demeter (from Karl Lieberherr's team, 1987) is the rule against it: a method should talk only to its own object, its fields, its parameters and the objects it creates, not to the parts of those objects. "Don't talk to strangers." A chain of dots that walks through several objects is the smell, and the nearer those fields are to private, the nearer it is to content coupling. (A fluent chain such as a list's map(...).filter(...) is not one: each step returns the same kind of thing and exposes no internals.)
Here is a class with exactly that smell, reaching through an order to its customer and restaurant addresses to answer three unrelated questions:
# reach_v1.py: a grab-bag class whose three jobs each reach through an order's parts
class Address:
def __init__(self, city: str) -> None:
self.city = city
class Customer:
def __init__(self, name: str, phone: str, address: Address) -> None:
self.name, self.phone, self.address = name, phone, address
class Restaurant:
def __init__(self, name: str, address: Address) -> None:
self.name, self.address = name, address
class Order:
def __init__(self, customer: Customer, restaurant: Restaurant, subtotal: int) -> None:
self.customer, self.restaurant, self.subtotal = customer, restaurant, subtotal
class OrderUtils: # a grab bag: pricing, privacy and display, all reaching inside an order
@staticmethod
def delivery_fee(order: Order) -> int:
return 20 if order.customer.address.city == order.restaurant.address.city else 45
@staticmethod
def masked_phone(order: Order) -> str:
phone = order.customer.phone
return phone[:2] + "X" * (len(phone) - 4) + phone[-2:]
@staticmethod
def label(order: Order) -> str:
return f"{order.customer.name}: {order.subtotal + OrderUtils.delivery_fee(order)} rupees"
order = Order(Customer("Asha", "9000000001", Address("Pune")), Restaurant("Biryani House", Address("Pune")), 500)
print("fee:", OrderUtils.delivery_fee(order))
print("phone:", OrderUtils.masked_phone(order))
print("label:", OrderUtils.label(order))// ReachV1.java: a grab-bag class whose three jobs each reach through an order's parts
class ReachV1 {
record Address(String city) {}
record Customer(String name, String phone, Address address) {}
record Restaurant(String name, Address address) {}
record Order(Customer customer, Restaurant restaurant, int subtotal) {}
static final class OrderUtils { // a grab bag: pricing, privacy and display, all reaching inside an order
static int deliveryFee(Order order) {
return order.customer().address().city().equals(order.restaurant().address().city()) ? 20 : 45;
}
static String maskedPhone(Order order) {
String phone = order.customer().phone();
return phone.substring(0, 2) + "X".repeat(phone.length() - 4) + phone.substring(phone.length() - 2);
}
static String label(Order order) {
return order.customer().name() + ": " + (order.subtotal() + deliveryFee(order)) + " rupees";
}
}
public static void main(String[] args) {
Order order = new Order(new Customer("Asha", "9000000001", new Address("Pune")), new Restaurant("Biryani House", new Address("Pune")), 500);
System.out.println("fee: " + OrderUtils.deliveryFee(order));
System.out.println("phone: " + OrderUtils.maskedPhone(order));
System.out.println("label: " + OrderUtils.label(order));
}
}// reach_v1.cpp: a grab-bag class whose three jobs each reach through an order's parts
#include <iostream>
#include <string>
struct Address {
std::string city;
};
struct Customer {
std::string name;
std::string phone;
Address address;
};
struct Restaurant {
std::string name;
Address address;
};
struct Order {
Customer customer;
Restaurant restaurant;
int subtotal;
};
struct OrderUtils { // a grab bag: pricing, privacy and display, all reaching inside an order
static int delivery_fee(const Order& order) { return order.customer.address.city == order.restaurant.address.city ? 20 : 45; }
static std::string masked_phone(const Order& order) {
const std::string& phone = order.customer.phone;
return phone.substr(0, 2) + std::string(phone.size() - 4, 'X') + phone.substr(phone.size() - 2);
}
static std::string label(const Order& order) {
return order.customer.name + ": " + std::to_string(order.subtotal + delivery_fee(order)) + " rupees";
}
};
int main() {
Order order{{"Asha", "9000000001", {"Pune"}}, {"Biryani House", {"Pune"}}, 500};
std::cout << "fee: " << OrderUtils::delivery_fee(order) << "\n";
std::cout << "phone: " << OrderUtils::masked_phone(order) << "\n";
std::cout << "label: " << OrderUtils::label(order) << "\n";
}// reachV1.js: a grab-bag class whose three jobs each reach through an order's parts
class Address {
constructor(city) {
this.city = city;
}
}
class Customer {
constructor(name, phone, address) {
Object.assign(this, { name, phone, address });
}
}
class Restaurant {
constructor(name, address) {
Object.assign(this, { name, address });
}
}
class Order {
constructor(customer, restaurant, subtotal) {
Object.assign(this, { customer, restaurant, subtotal });
}
}
class OrderUtils { // a grab bag: pricing, privacy and display, all reaching inside an order
static deliveryFee(order) {
return order.customer.address.city === order.restaurant.address.city ? 20 : 45;
}
static maskedPhone(order) {
const phone = order.customer.phone;
return phone.slice(0, 2) + "X".repeat(phone.length - 4) + phone.slice(-2);
}
static label(order) {
return `${order.customer.name}: ${order.subtotal + OrderUtils.deliveryFee(order)} rupees`;
}
}
const order = new Order(new Customer("Asha", "9000000001", new Address("Pune")), new Restaurant("Biryani House", new Address("Pune")), 500);
console.log("fee:", OrderUtils.deliveryFee(order));
console.log("phone:", OrderUtils.maskedPhone(order));
console.log("label:", OrderUtils.label(order));fee: 20
phone: 90XXXXXX01
label: Asha: 520 rupeesCheck the numbers: the customer and the restaurant are both in Pune, so the fee is 20; the phone 9000000001 keeps its first two and last two digits; the order is 500 plus the 20 fee, so the label says 520 rupees.
- A change to a distant class breaks you. If
Addresssplitscityintoareaandcity, every function that wrote.address.citychanges, in files the address change never mentioned. - A test builds the whole graph. To check one fee you create an
Order, aCustomer, aRestaurantand twoAddressobjects: five objects for one comparison. - The class that owns the data has no say.
Customercannot decide how its own phone is masked, because the function reading it lives in a grab bag. - The grab bag grows.
OrderUtilsholds pricing, privacy and display: three jobs nobody would put in one class on purpose.
What does the test have to build?
You want a test that the fee is 45 when the customer and the restaurant are in different cities. In the first version, what must the test create, and what is the one thing the fee rule actually needs?
One strong answer
Five objects: an Order, a Customer, a Restaurant and two Address objects. What the rule needs is two things that each know their city. The distance between those two lists is the coupling: the function knows the shape of the whole graph to use one string from each end.
Cohesion: do the parts of a class belong together?#
Cohesion is how closely the parts of one module (a class, a function, a file) belong together. A cohesive class has methods and data that all serve one purpose; a class with low cohesion is a set of unrelated things that happen to share a name. The classic scale (Yourdon and Constantine's Structured Design, 1979) has seven levels, from the worst to the best:
| Kind | Its parts are together because they | Example |
|---|---|---|
| Coincidental | Happen to share a file | OrderUtils: pricing, phone masking and display |
| Logical | Do the same kind of thing, and a flag picks one | send(kind, ...) that emails, texts or pushes |
| Temporal | Run at the same time | A startup() that opens the database, warms a cache and prints a banner |
| Procedural | Run in a fixed order | The steps of a checkout script, each on different data |
| Communicational | Work on the same data | Functions that each read the same order to print a receipt and a label |
| Sequential | Feed each other: one's output is the next one's input | Parse a file, then validate the rows, then save them |
| Functional | All contribute to one well-defined job | delivery_fee: everything in it computes the fee |
OrderUtils, Helpers and Utils are the usual names for the worst end.
Cohesion is the property; the single responsibility principle from the earlier lesson is a rule for getting it: group what changes for the same reason. Metrics exist (LCOM, "lack of cohesion of methods"), but in practice you judge cohesion by reading. A quick test: can you say what the class is for in a short phrase without the word "and"?
Coupling and cohesion are usually fixed together. Here is the same code with each question moved to the class that holds the data, and the fee rule taking only the two things it needs:
# reach_v2.py: each class answers questions about its own data; callers ask one object, one step away
class Address:
def __init__(self, city: str) -> None:
self.city = city
class Customer:
def __init__(self, name: str, phone: str, address: Address) -> None:
self.name, self.phone, self.address = name, phone, address
def city(self) -> str:
return self.address.city
def masked_phone(self) -> str: # privacy rules live with the phone number
return self.phone[:2] + "X" * (len(self.phone) - 4) + self.phone[-2:]
class Restaurant:
def __init__(self, name: str, address: Address) -> None:
self.name, self.address = name, address
def city(self) -> str:
return self.address.city
def delivery_fee(origin, destination) -> int: # needs only two things that know their city
return 20 if origin.city() == destination.city() else 45
class Order:
def __init__(self, customer: Customer, restaurant: Restaurant, subtotal: int) -> None:
self.customer, self.restaurant, self.subtotal = customer, restaurant, subtotal
def label(self) -> str:
return f"{self.customer.name}: {self.subtotal + delivery_fee(self.restaurant, self.customer)} rupees"
class StubPlace: # a test double that only knows its city
def __init__(self, city: str) -> None:
self._city = city
def city(self) -> str:
return self._city
order = Order(Customer("Asha", "9000000001", Address("Pune")), Restaurant("Biryani House", Address("Pune")), 500)
print("fee:", delivery_fee(order.restaurant, order.customer))
print("phone:", order.customer.masked_phone())
print("label:", order.label())
print("fee between two stub places:", delivery_fee(StubPlace("Pune"), StubPlace("Pune")))// ReachV2.java: each class answers questions about its own data; callers ask one object, one step away
class ReachV2 {
record Address(String city) {}
interface Place { String city(); } // anything that knows which city it is in
record Customer(String name, String phone, Address address) implements Place {
public String city() { return address.city(); }
String maskedPhone() { // privacy rules live with the phone number
return phone.substring(0, 2) + "X".repeat(phone.length() - 4) + phone.substring(phone.length() - 2);
}
}
record Restaurant(String name, Address address) implements Place {
public String city() { return address.city(); }
}
static int deliveryFee(Place origin, Place destination) { // needs only two things that know their city
return origin.city().equals(destination.city()) ? 20 : 45;
}
record Order(Customer customer, Restaurant restaurant, int subtotal) {
String label() { return customer.name() + ": " + (subtotal + deliveryFee(restaurant, customer)) + " rupees"; }
}
record StubPlace(String city) implements Place {} // a test double that only knows its city
public static void main(String[] args) {
Order order = new Order(new Customer("Asha", "9000000001", new Address("Pune")), new Restaurant("Biryani House", new Address("Pune")), 500);
System.out.println("fee: " + deliveryFee(order.restaurant(), order.customer()));
System.out.println("phone: " + order.customer().maskedPhone());
System.out.println("label: " + order.label());
System.out.println("fee between two stub places: " + deliveryFee(new StubPlace("Pune"), new StubPlace("Pune")));
}
}// reach_v2.cpp: each class answers questions about its own data; callers ask one object, one step away
#include <iostream>
#include <string>
#include <utility>
struct Address {
std::string city;
};
class Place { // anything that knows which city it is in
public:
virtual ~Place() = default;
virtual std::string city() const = 0;
};
struct Customer : Place {
std::string name;
std::string phone;
Address address;
Customer(std::string n, std::string p, Address a) : name(std::move(n)), phone(std::move(p)), address(std::move(a)) {}
std::string city() const override { return address.city; }
std::string masked_phone() const { // privacy rules live with the phone number
return phone.substr(0, 2) + std::string(phone.size() - 4, 'X') + phone.substr(phone.size() - 2);
}
};
struct Restaurant : Place {
std::string name;
Address address;
Restaurant(std::string n, Address a) : name(std::move(n)), address(std::move(a)) {}
std::string city() const override { return address.city; }
};
int delivery_fee(const Place& origin, const Place& destination) { // needs only two things that know their city
return origin.city() == destination.city() ? 20 : 45;
}
struct Order {
Customer customer;
Restaurant restaurant;
int subtotal;
std::string label() const {
return customer.name + ": " + std::to_string(subtotal + delivery_fee(restaurant, customer)) + " rupees";
}
};
struct StubPlace : Place { // a test double that only knows its city
std::string c;
explicit StubPlace(std::string city) : c(std::move(city)) {}
std::string city() const override { return c; }
};
int main() {
Order order{Customer("Asha", "9000000001", {"Pune"}), Restaurant("Biryani House", {"Pune"}), 500};
std::cout << "fee: " << delivery_fee(order.restaurant, order.customer) << "\n";
std::cout << "phone: " << order.customer.masked_phone() << "\n";
std::cout << "label: " << order.label() << "\n";
std::cout << "fee between two stub places: " << delivery_fee(StubPlace("Pune"), StubPlace("Pune")) << "\n";
}// reachV2.js: each class answers questions about its own data; callers ask one object, one step away
class Address {
constructor(city) {
this.city = city;
}
}
class Customer {
constructor(name, phone, address) {
Object.assign(this, { name, phone, address });
}
city() {
return this.address.city;
}
maskedPhone() { // privacy rules live with the phone number
return this.phone.slice(0, 2) + "X".repeat(this.phone.length - 4) + this.phone.slice(-2);
}
}
class Restaurant {
constructor(name, address) {
Object.assign(this, { name, address });
}
city() {
return this.address.city;
}
}
function deliveryFee(origin, destination) { // needs only two things that know their city
return origin.city() === destination.city() ? 20 : 45;
}
class Order {
constructor(customer, restaurant, subtotal) {
Object.assign(this, { customer, restaurant, subtotal });
}
label() {
return `${this.customer.name}: ${this.subtotal + deliveryFee(this.restaurant, this.customer)} rupees`;
}
}
const stubPlace = (city) => ({ city: () => city }); // a test double that only knows its city
const order = new Order(new Customer("Asha", "9000000001", new Address("Pune")), new Restaurant("Biryani House", new Address("Pune")), 500);
console.log("fee:", deliveryFee(order.restaurant, order.customer));
console.log("phone:", order.customer.maskedPhone());
console.log("label:", order.label());
console.log("fee between two stub places:", deliveryFee(stubPlace("Pune"), stubPlace("Pune")));fee: 20
phone: 90XXXXXX01
label: Asha: 520 rupees
fee between two stub places: 20The first three lines are exactly what the grab bag printed: nothing a user sees has changed. The last line is the gain. delivery_fee needs only two things that have a city(), so a test passes two tiny stubs and there is no graph to build. The privacy rule moved into Customer, which owns the phone, and OrderUtils is gone.
%% title: Each class knows only its neighbours
classDiagram
class Place {
<<interface>>
+city() str
}
class Order {
+label() str
}
class Customer {
+name str
+city() str
+masked_phone() str
}
class Restaurant {
+name str
+city() str
}
class Address {
+city str
}
Place <|.. Customer
Place <|.. Restaurant
Order --> Customer
Order --> Restaurant
Customer --> Address
Restaurant --> AddressRead the arrows: Order knows its two neighbours and nothing about addresses, and only Customer and Restaurant touch an Address. If Address splits its city, two one-line methods change and nothing else. (Place is an interface in Java and C++, as in the program; in Python and JavaScript the rule simply relies on "has a city()", and no type is needed.)
A fee rule that walks through four objects to compare two cities breaks when an address changes. Ask the customer and the restaurant for their city.
KISS and YAGNI: the case for not adding it#
KISS ("keep it simple, stupid", usually credited to the aircraft engineer Kelly Johnson at Lockheed) says to choose the design a reader can understand at a glance. Simple does not mean short or clever. A one-line trick that nobody can read is shorter and not simpler. Simple means few moving parts, each one doing an obvious job.
YAGNI ("you aren't gonna need it") comes from Extreme Programming: implement things when you actually need them, never when you only foresee that you will (Ron Jeffries' formulation). Speculative code costs three times: you build it, you carry it (every reader and every later change must work around it), and when the guess was wrong you repair or delete it.
Here is the settings page that makes everything configurable. The first version was built for every option anyone might want, with validation and change listeners on each:
# options_v1.py: a settings system built for every option anyone might want; the app reads two
class Option:
def __init__(self, name: str, default: str, check) -> None:
self.name, self.value, self.check = name, default, check
self.listeners = [] # "in case something must react when an option changes"
self.reads = 0
def set(self, value: str) -> None:
if not self.check(value):
raise ValueError(f"bad value for {self.name}")
self.value = value
for listener in self.listeners:
listener(value)
def get(self) -> str:
self.reads += 1
return self.value
OPTIONS = {o.name: o for o in [
Option("currency", "INR", lambda v: len(v) == 3),
Option("tip_percent", "10", lambda v: v.isdigit() and int(v) <= 30),
Option("rounding", "down", lambda v: v in ("down", "up")),
Option("locale", "en-IN", lambda v: True),
Option("receipt_footer", "", lambda v: True),
Option("theme", "light", lambda v: v in ("light", "dark")),
]}
def tip_line(subtotal: int) -> str:
percent = int(OPTIONS["tip_percent"].get())
return f"Tip {percent} percent on {subtotal}: {subtotal * percent // 100} {OPTIONS['currency'].get()}"
print(tip_line(500))
print("options defined:", len(OPTIONS))
print("options the app reads:", sum(1 for o in OPTIONS.values() if o.reads > 0))// OptionsV1.java: a settings system built for every option anyone might want; the app reads two
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
class OptionsV1 {
static final class Option {
final String name;
String value;
final Predicate<String> check;
final List<Consumer<String>> listeners = new ArrayList<>(); // "in case something must react when an option changes"
int reads = 0;
Option(String name, String value, Predicate<String> check) {
this.name = name;
this.value = value;
this.check = check;
}
void set(String v) {
if (!check.test(v)) throw new IllegalArgumentException("bad value for " + name);
value = v;
for (Consumer<String> l : listeners) l.accept(v);
}
String get() {
reads++;
return value;
}
}
static final Map<String, Option> OPTIONS = new LinkedHashMap<>();
static void define(String name, String value, Predicate<String> check) { OPTIONS.put(name, new Option(name, value, check)); }
static String tipLine(int subtotal) {
int percent = Integer.parseInt(OPTIONS.get("tip_percent").get());
return "Tip " + percent + " percent on " + subtotal + ": " + (subtotal * percent / 100) + " " + OPTIONS.get("currency").get();
}
public static void main(String[] args) {
define("currency", "INR", v -> v.length() == 3);
define("tip_percent", "10", v -> v.chars().allMatch(Character::isDigit) && Integer.parseInt(v) <= 30);
define("rounding", "down", v -> v.equals("down") || v.equals("up"));
define("locale", "en-IN", v -> true);
define("receipt_footer", "", v -> true);
define("theme", "light", v -> v.equals("light") || v.equals("dark"));
System.out.println(tipLine(500));
System.out.println("options defined: " + OPTIONS.size());
System.out.println("options the app reads: " + OPTIONS.values().stream().filter(o -> o.reads > 0).count());
}
}// options_v1.cpp: a settings system built for every option anyone might want; the app reads two
#include <algorithm>
#include <cctype>
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
struct Option {
std::string name;
std::string value;
std::function<bool(const std::string&)> check;
std::vector<std::function<void(const std::string&)>> listeners; // "in case something must react when an option changes"
int reads = 0;
void set(const std::string& v) {
if (!check(v)) throw std::invalid_argument("bad value for " + name);
value = v;
for (auto& l : listeners) l(v);
}
const std::string& get() {
++reads;
return value;
}
};
std::vector<Option> options; // kept in the order they were defined
Option& option(const std::string& name) {
return *std::find_if(options.begin(), options.end(), [&](const Option& o) { return o.name == name; });
}
std::string tip_line(int subtotal) {
int percent = std::stoi(option("tip_percent").get());
return "Tip " + std::to_string(percent) + " percent on " + std::to_string(subtotal) + ": " + std::to_string(subtotal * percent / 100) + " " + option("currency").get();
}
int main() {
auto digits = [](const std::string& v) { return !v.empty() && std::all_of(v.begin(), v.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); };
options.push_back({"currency", "INR", [](const std::string& v) { return v.size() == 3; }, {}, 0});
options.push_back({"tip_percent", "10", [&](const std::string& v) { return digits(v) && std::stoi(v) <= 30; }, {}, 0});
options.push_back({"rounding", "down", [](const std::string& v) { return v == "down" || v == "up"; }, {}, 0});
options.push_back({"locale", "en-IN", [](const std::string&) { return true; }, {}, 0});
options.push_back({"receipt_footer", "", [](const std::string&) { return true; }, {}, 0});
options.push_back({"theme", "light", [](const std::string& v) { return v == "light" || v == "dark"; }, {}, 0});
std::cout << tip_line(500) << "\n";
std::cout << "options defined: " << options.size() << "\n";
std::cout << "options the app reads: " << std::count_if(options.begin(), options.end(), [](const Option& o) { return o.reads > 0; }) << "\n";
}// optionsV1.js: a settings system built for every option anyone might want; the app reads two
class Option {
constructor(name, value, check) {
Object.assign(this, { name, value, check });
this.listeners = []; // "in case something must react when an option changes"
this.reads = 0;
}
set(value) {
if (!this.check(value)) throw new Error(`bad value for ${this.name}`);
this.value = value;
for (const listener of this.listeners) listener(value);
}
get() {
this.reads += 1;
return this.value;
}
}
const OPTIONS = new Map([
new Option("currency", "INR", (v) => v.length === 3),
new Option("tip_percent", "10", (v) => /^\d+$/.test(v) && Number(v) <= 30),
new Option("rounding", "down", (v) => v === "down" || v === "up"),
new Option("locale", "en-IN", () => true),
new Option("receipt_footer", "", () => true),
new Option("theme", "light", (v) => v === "light" || v === "dark"),
].map((o) => [o.name, o]));
function tipLine(subtotal) {
const percent = Number(OPTIONS.get("tip_percent").get());
return `Tip ${percent} percent on ${subtotal}: ${Math.floor((subtotal * percent) / 100)} ${OPTIONS.get("currency").get()}`;
}
console.log(tipLine(500));
console.log("options defined:", OPTIONS.size);
console.log("options the app reads:", [...OPTIONS.values()].filter((o) => o.reads > 0).length);Tip 10 percent on 500: 50 INR
options defined: 6
options the app reads: 2Six options exist, each with a validator and a list of listeners "in case something must react"; the app reads two of them. Nobody has asked to change any option at run time, so the listener lists are empty and every set path is unused. That machinery is untested, unread and still maintained, and it hides the only two values that matter.
# options_v2.py: the two values the app uses, written down as two values
CURRENCY = "INR"
TIP_PERCENT = 10
def tip_line(subtotal: int) -> str:
return f"Tip {TIP_PERCENT} percent on {subtotal}: {subtotal * TIP_PERCENT // 100} {CURRENCY}"
print(tip_line(500))// OptionsV2.java: the two values the app uses, written down as two values
class OptionsV2 {
static final String CURRENCY = "INR";
static final int TIP_PERCENT = 10;
static String tipLine(int subtotal) {
return "Tip " + TIP_PERCENT + " percent on " + subtotal + ": " + (subtotal * TIP_PERCENT / 100) + " " + CURRENCY;
}
public static void main(String[] args) {
System.out.println(tipLine(500));
}
}// options_v2.cpp: the two values the app uses, written down as two values
#include <iostream>
#include <string>
const std::string CURRENCY = "INR";
const int TIP_PERCENT = 10;
std::string tip_line(int subtotal) {
return "Tip " + std::to_string(TIP_PERCENT) + " percent on " + std::to_string(subtotal) + ": " + std::to_string(subtotal * TIP_PERCENT / 100) + " " + CURRENCY;
}
int main() { std::cout << tip_line(500) << "\n"; }// optionsV2.js: the two values the app uses, written down as two values
const CURRENCY = "INR";
const TIP_PERCENT = 10;
function tipLine(subtotal) {
return `Tip ${TIP_PERCENT} percent on ${subtotal}: ${Math.floor((subtotal * TIP_PERCENT) / 100)} ${CURRENCY}`;
}
console.log(tipLine(500));Tip 10 percent on 500: 50 INRThe output is the same line. The second version is two values and one function, and the day someone really needs the tip to vary per restaurant is the day to add a parameter: a small, local change that costs the same later as it does now.
The settings system defines six validated options with change listeners, and the app reads two. The unused machinery still has to be carried.
How the five pull against each other#
The five ideas agree on the goal (code that is cheap to change) and disagree on the means, and the disagreement is the interesting part:
| Tension | Why they collide | How to settle it |
|---|---|---|
| DRY and coupling | Shared code ties all its users together: change it and every user changes | Share knowledge that must change together. Across teams or services, a small copy is often cheaper than a shared dependency |
| DRY and KISS | An abstraction removes the copies and adds indirection | Wait for the third copy, then keep the abstraction as small as the real shape |
| YAGNI and open/closed | Open/closed wants an interface ready for the next case; YAGNI says wait for it | Close against variation you have evidence for, as the earlier lesson said: the second or third real case, not a guess |
| Cohesion and coupling | Splitting a class raises its cohesion and adds links between the pieces | Split along real seams (different reasons to change), not by method count |
Patterns sit on top of these forces. Strategy and Observer lower coupling between a caller and what it calls. Facade lowers the coupling between a client and a subsystem. Template Method and Decorator remove duplicated skeletons. Each one costs files, names and indirection, so before adding one, ask three things: which force is it relieving, is that force real today (do I have a second case, a second copy, a second owner?), and what will it cost the next reader? If you cannot answer the first two, the plain code is the better design.
When not to use it#
- DRY applied to anything that looks alike. Merging two rules that only resemble each other, then adding flags as they diverge, is the wrong abstraction. Keep two small copies until they prove to be one thing.
- Chasing zero coupling. Parts that cooperate must know something about each other, and a design with an interface on every line is not loosely coupled, only indirect. Reduce coupling where change is likely: vendors, rules that vary, the clock.
- Tiny classes in the name of cohesion. A class per method is not cohesion, it is scattering. A class is cohesive when you can name its job; one with six methods all about the tax rule is perfectly cohesive.
- KISS as an excuse for no structure. One 400-line function is not simple; it is easy to start and hard to read. Simple means the parts are few and each is obvious.
- YAGNI as an excuse to ignore known requirements. If the interviewer said "two kinds of payment", the second one is not speculative. YAGNI is about guesses.
Two discounts that are both 10 percent today look like one rule. Merge them, and each change adds a flag to a function that was never one idea.
Where it shows up in the case studies#
| Case study | Force | How |
|---|---|---|
| Parking lot | Cohesion and coupling | Its common mistakes list "a ParkingLot god class with the pricing if/elif chain, the spot search and the printing all inside park()": three jobs in one method, the invoice from the single-responsibility lesson in a parking costume. The fix there is the one used here: one class per job, with the pricing behind a strategy |
| Vending machine | YAGNI and KISS | Its patterns table lists change-making as an optional Strategy and says "say it; don't build it unasked", and a callout says that with two states and two events an enum and a match would be clearer than the State pattern |
| In-memory file system | DRY | Every operation starts with the same steps, written once as _parts, _walk and _parent_of, so each operation raises the same errors in the same order: the knowledge of how a path is resolved has one home |
In the interview#
Name the force behind each choice, in one breath: "The four SMS handlers all knew how a message is worded, so I gave that one home and each status only supplies its text. The fee rule needs two things that know their city, not the whole order graph, so I pass those. And I did not build a settings framework, because the app reads two values."
Then expect the follow-ups:
- "What is the difference between coupling and cohesion?" Coupling is between modules: how much one must know about another. Cohesion is inside one: how much its parts belong together. You want low coupling between parts and high cohesion within them, and the two usually improve together.
- "Is duplication always bad?" No. Duplication of knowledge is: one rule written in four places will drift. Two functions that look alike but change for different reasons should stay apart, and a small copy can be cheaper than a shared dependency across a team boundary.
- "When do you introduce the abstraction?" At the second or third real case, when you can see the true shape, not on the first guess. The rule of three is a heuristic, not a law.
- "Isn't YAGNI just an excuse to not design?" No. It applies to features and flexibility for a guessed future. Clean code and tests are what make waiting safe, and some decisions (a public API, a data format) are worth thinking through early.
Common mistakes#
- "DRY means no repeated lines." It is about knowledge. Merging lookalikes that change for different reasons is how the wrong abstraction is born.
- A
UtilsorHelpersclass: a grab bag of unrelated jobs, the lowest cohesion there is. - A chain of dots through several objects in a rule that needs one value from each end.
- "Loose coupling" as "add an interface everywhere". It is fewer, more stable dependencies, not more files.
- Building the framework first: options, plugins and extension points nobody asked for, justified as "flexibility".
- YAGNI used to skip a stated requirement, or to skip tests.
Coupling, cohesion, DRY, KISS and YAGNI
Q1/4 Two functions, senior_price and student_price, both take 10 percent off today. Someone merges them into one function. What is the risk?
Answer & explanation
Answer: B
DRY is about knowledge, not text. Two rules that look alike but change for different reasons are a coincidence, and a shared function turns the next change to either one into a branch. Duplication is cheaper than the wrong abstraction.
Q2/4 A delivery-fee function writes order.customer.address.city and order.restaurant.address.city. What is the problem, and what is the fix?
Answer & explanation
Answer: C
A chain of dots makes the function depend on every object along the way, so a change to Address breaks it and a test must build the whole graph. Asking one neighbour per step, or passing in what the rule needs, keeps the dependency small.
Q3/4 Which describes a class with low cohesion?
Answer & explanation
Answer: C
Cohesion is how closely a class's parts belong together. A grab bag of unrelated jobs is the lowest end of the scale. A class with six methods about one rule is cohesive, and a one-method class is not automatically better.
Q4/4 The settings system has six validated options with change listeners, and the app reads two. What does YAGNI say?
Answer & explanation
Answer: B
YAGNI is about capabilities built for a guessed future. It does not forbid ever adding configuration; it says to add it when a real need appears, when it costs about the same as now.
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.


