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.

Watch it evolve1 / 10
The problem

A desk, held for a few minutes

DeskBooking takes the next free desk, holds it, and confirms. The hold is a flat 15 minutes.

80
Start
Stage 1
Stage 2
Stage 3
Final

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.

The first versionJava
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));
    }
}
«record»Member− name: String− paid: booleanDeskBooking− freeDesks: List<String>~ reserve(Member, String, int): String− clock(int): String

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():

After two changesJava
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.

Java
interface HoldPolicy {
    int minutesFor(Member member, String day);
}
«record»Member− name: String− paid: boolean«interface»HoldPolicy+ minutesFor(Member, String): intStandardHolds+ minutesFor(Member, String): intEventDayHolds+ minutesFor(Member, String): intDeskBooking− freeDesks: List<String>− holds: HoldPolicy~ reserve(Member, String, int): String− clock(int): String

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.

«record»Member− name: String− paid: boolean«interface»HoldPolicy+ minutesFor(Member, String): intStandardHolds+ minutesFor(Member, String): intEventDayHolds+ minutesFor(Member, String): intDeskBooking− freeDesks: List<String>− holds: HoldPolicy~ reserve(Member, String, int): String− clock(int): String

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

StandardHolds holds

Only the standard rules

Editing DeskBooking to use any other policy

HoldPolicy holds

Any hold policy

Passing a different object in

A desks field of type ArrayList

Only an array list

Editing the class to try another list

A desks field of type List

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 as MinutesCalculator.
  • Keep the stable side ignorant. After the fix, nothing in DeskBooking mentions 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. main decides 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 std::function, or a template parameter when the choice is fixed at compile time

JavaScript

An object with the method, or simply a function passed in

Python

A Protocol with classes, or a plain function such as hold_minutes(member, day)

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

1. A new rule says standing desks are held for only 10 minutes. What has to change in the fixed design?
2. Should the first version have had a HoldPolicy interface from the start?
3. DeskBooking declares its field as HoldPolicy holds. What does that buy over StandardHolds holds?
4. Which is the strongest sign that a piece of code varies?

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.