Low-level designCore9 min
Encapsulate what varies
Find the part of the code that keeps changing, separate it from the part that doesn't, and put it behind an interface. Then write the stable code against that interface, so changes stay on their side of the line.
A desk, held for a few minutes
DeskBooking takes the next free desk, holds it, and confirms. The hold is a flat 15 minutes.
1 · The situation
A desk, held for a few minutes
A coworking space lets people reserve a desk from an app. DeskBooking.reserve() takes the next free desk and holds it for a while; if the person doesn't arrive in time, the desk goes back. Times are minutes after midnight, so 570 is 9:30.
The method does four things: finds a free desk, takes it off the list, works out how long to hold it, and writes the confirmation. In the first version the hold is a flat 15 minutes. Python writes at_minute; the other names are the same in all four languages.
import java.util.ArrayList;
import java.util.List;
record Member(String name, boolean paid) {}
class DeskBooking {
private final List<String> freeDesks;
DeskBooking(List<String> desks) {
this.freeDesks = new ArrayList<>(desks);
}
String reserve(Member member, String day, int atMinute) {
if (freeDesks.isEmpty()) return member.name() + ": no desk free";
String desk = freeDesks.remove(0);
int hold = 15;
String until = clock(atMinute + hold);
return member.name() + ": " + desk + " on " + day + ", held until " + until;
}
private static String clock(int minute) {
return String.format("%d:%02d", minute / 60, minute % 60);
}
}
public class Main {
public static void main(String[] args) {
List<String> desks = List.of("D1", "D2", "D3");
DeskBooking booking = new DeskBooking(desks);
System.out.println(booking.reserve(new Member("Ana", true), "Tue", 570));
System.out.println(booking.reserve(new Member("Ben", false), "Tue", 580));
System.out.println(booking.reserve(new Member("Cy", false), "Sat", 600));
System.out.println(booking.reserve(new Member("Dee", true), "Sat", 610));
}
}The class diagram of this version, drawn from its code: one class, with the hold rule inside one method.
Ana: D1 on Tue, held until 9:45 Ben: D2 on Tue, held until 9:55 Cy: D3 on Sat, held until 10:15 Dee: no desk free
2 · The change
The same line, edited three times
Over a few months the managers change the hold rule twice. Paying members complain that 15 minutes isn't enough to park, so they get 30. Then weekends are quiet, so everyone gets an hour on Saturday and Sunday. Each change is small, and each lands in the middle of reserve():
class DeskBooking {
private final List<String> freeDesks;
DeskBooking(List<String> desks) {
this.freeDesks = new ArrayList<>(desks);
}
String reserve(Member member, String day, int atMinute) {
if (freeDesks.isEmpty()) return member.name() + ": no desk free";
String desk = freeDesks.remove(0);
int hold = member.paid() ? 30 : 15;
if (day.equals("Sat") || day.equals("Sun")) hold = 60;
String until = clock(atMinute + hold);
return member.name() + ": " + desk + " on " + day + ", held until " + until;
}
private static String clock(int minute) {
return String.format("%d:%02d", minute / 60, minute % 60);
}
}Ana: D1 on Tue, held until 10:00 Ben: D2 on Tue, held until 9:55 Cy: D3 on Sat, held until 11:00 Dee: no desk free
Change | Asked for by | Lines edited in reserve() |
|---|---|---|
A flat 15 minutes | The first version | The hold line |
Members get 30 | The membership team | The hold line |
Weekends get 60 | The weekend manager | The hold line, and a new one under it |
The history tells you something the code alone doesn't. Finding a desk, taking it and writing the confirmation haven't changed once; the hold rule has changed every time. Yet each change reopens the whole method, and each edit sits a line away from the code that hands out desks. The next rule, holds for an event day, would land in the same place again.
The practice checks find nothing wrong with any of these versions, and they're right: each one is a reasonable method. The problem only shows across versions, in where the edits keep falling.
3 · The fix
Give the changing part a home and an interface
Separate what varies, the hold rule, from what stays the same, the booking. Then make the booking depend only on an interface for the rule:
HoldPolicy has one method, minutesFor(member, day). It takes everything the rules so far have needed, and it's named for the decision it makes.
interface HoldPolicy {
int minutesFor(Member member, String day);
}The first four lines match the third version's output, and the event day adds a fifth:
Ana: D1 on Tue, held until 10:00 Ben: D2 on Tue, held until 9:55 Cy: D3 on Sat, held until 11:00 Dee: no desk free Eve: D1 on Fri, held until 11:00
4 · The principle
Find what varies; program to an interface
Encapsulate what varies says: identify the aspects of your code that change, and separate them from those that don't. It's the idea under most design patterns: Strategy hides an algorithm that varies, State a behavior that varies with a mode, Factory Method a class that varies, Observer the set of listeners. Its older form is information hiding: a module should hide a design decision that's likely to change, so that the rest of the program doesn't need to know it.
After the fix: the stable booking depends on HoldPolicy; each set of rules is a class behind it.
Its companion is program to an interface, not an implementation: declare variables, fields and parameters by the most general type that offers what the code uses. DeskBooking keeps a HoldPolicy, never a StandardHolds. The same goes for library types: a field declared as List can hold any list, while one declared as ArrayList ties the class to one choice.
Declared as | Can hold | What a change costs |
|---|---|---|
| Only the standard rules | Editing |
| Any hold policy | Passing a different object in |
A desks field of type | Only an array list | Editing the class to try another list |
A desks field of type | Any list | Nothing in the class |
5 · When breaking it is fine
Encapsulate what has varied, not what might
The first version was right to write 15 inline. Nothing had varied yet, and an interface with one implementation called FifteenMinutes would have been a guess. The hold rule earned its boundary by changing; most code never does.
Signal | Encapsulate now? |
|---|---|
A rule has changed two or three times, always in the same spot | Yes |
A requirement says "for now" or names a second case that's coming | Yes |
A rule owned by another team or a business decision, such as pricing or eligibility | Probably; it changes for reasons outside the code |
You can imagine it changing, but nobody has asked | No: keep it inline, and refactor when it does |
A boundary also fixes what can vary. minutesFor(member, day) can't see which desk was chosen, so a rule like "standing desks hold for 10 minutes" would change the interface and every policy. Choose the parameters from the rules you've seen, and expect to revise them.
6 · Getting it right
Let the history tell you where the line goes
- Read the history. Version control shows which lines change, how often and why. Lines that change together, for the same people, are one varying thing.
- Name the interface for the decision, such as
HoldPolicy, not for a mechanism, such asMinutesCalculator. - Keep the stable side ignorant. After the fix, nothing in
DeskBookingmentions members' plans or weekends. - Pass what the rules need, no more. A member and a day today; the whole booking only when a rule needs it.
- Let one object choose.
maindecides which policy applies; the booking never asks what day of the year it is.
In some languages the varying part is naturally a function rather than a class:
Language | A home for one varying rule |
|---|---|
Java | An interface with classes, or a functional interface filled by a lambda |
C++ | An abstract class, a |
JavaScript | An object with the method, or simply a function passed in |
Python | A |
7 · Look-alikes
One idea, many names
This principle sits under several others. What differs is the scale and the question.
Idea | How it relates |
|---|---|
Information hiding | The original form: hide each design decision likely to change inside one module |
Protected variations | The same idea named for its goal: wrap a point of instability in an interface |
Open/closed | What you get when the varying part is behind an interface: new cases without edits |
Strategy | The pattern for one varying algorithm, chosen per object |
Dependency inversion | Says the stable policy should own the interface, not the varying details |
YAGNI | The brake: encapsulate what has varied, not everything that might |
The checks recognize the fixed version as Strategy, which it is: a booking holding one of several interchangeable policies. Encapsulate what varies is the reasoning that leads there; Strategy is one of the shapes the result can take.
8 · In practice
What varies at the garage
In the parking-lot practice problem, the story tells you what varies: pricing changes on match days. Parking a car, finding a spot and issuing a ticket stay the same. A good design puts pricing behind an interface and lets the lot depend only on it.
The vehicle types vary too, in the size of spot they need, which is why each vehicle answers size() for itself. Design the parking lot and draw the line around each part the story says will change.
Check yourself
4 questions
Takeaways
Remember this
- Separate the parts of the code that change from the parts that stay the same.
- Put the varying part behind an interface named for the decision it makes.
- Program to an interface: declare fields and parameters by the most general type that does the job.
- Let the history of changes, not imagination, show what varies.
- Most design patterns are this principle applied to a particular kind of variation.