Low-level designCore9 min
Design by contract
Say what each method needs from its caller, what it promises in return, and what stays true of the object throughout. Then check it, so a bug stops at the call that broke the rules instead of surfacing much later.
A ferry tracks its car deck
CarDeck keeps the used length of a 20 m lane. Callers ask fits(), then board() or leave().
1 · The situation
A ferry's car deck
A small island ferry has one car lane, 20 meters long. CarDeck tracks how much of it is used: the booking system asks fits(length) before a car drives on, calls board(length) when it does, and leave(length) when a booking is canceled. Lengths are whole centimeters, so a 4.5 meter car is 450.
class CarDeck {
private final int capacityCm;
private int usedCm;
CarDeck(int capacityCm) {
this.capacityCm = capacityCm;
}
boolean fits(int lengthCm) {
return usedCm + lengthCm <= capacityCm;
}
void board(int lengthCm) {
usedCm += lengthCm;
}
void leave(int lengthCm) {
usedCm -= lengthCm;
}
int usedCm() {
return usedCm;
}
}
public class Main {
static String meters(int cm) {
String sign = cm < 0 ? "-" : "";
int c = Math.abs(cm);
return String.format("%s%d.%02d m", sign, c / 100, c % 100);
}
static void board(CarDeck deck, int lengthCm) {
if (!deck.fits(lengthCm)) {
System.out.println("board " + lengthCm + ": no room");
return;
}
deck.board(lengthCm);
System.out.println("board " + lengthCm + ": used " + meters(deck.usedCm()));
}
static void leave(CarDeck deck, int lengthCm) {
deck.leave(lengthCm);
System.out.println("leave " + lengthCm + ": used " + meters(deck.usedCm()));
}
public static void main(String[] args) {
CarDeck deck = new CarDeck(2000);
board(deck, 450);
board(deck, 480);
leave(deck, 480);
// The booking system retries the cancellation after a timeout.
leave(deck, 480);
for (int length : new int[] {520, 510, 500, 470}) board(deck, length);
}
}The class diagram of this version, drawn from its code: one class, and nothing about what its methods expect.
The program boards two cars and cancels the second. Then something goes wrong in the booking system: the cancellation times out and is retried, so the same car leaves twice. Four more cars arrive:
board 450: used 4.50 m board 480: used 9.30 m leave 480: used 4.50 m leave 480: used -0.30 m board 520: used 4.90 m board 510: used 10.00 m board 500: used 15.00 m board 470: used 19.70 m
The deck reports 19.70 m used. Five cars are aboard, 24.50 m of them.
2 · The change
A bug that surfaces four cars later
The used length went to -0.30 m, which can't be true of any deck, and nothing noticed. The deck then let on four cars as if the lane had 4.80 m more room than it does, and the last of them fit only on paper: 24.50 m of cars on a 20 m lane. The bug happened at the second leave(480); it shows up on the car deck, four cars later, as a car that can't drive on.
Calls between the bad leave(480) and the moment anyone could notice. Unchecked: the four boardings that follow, and then the ramp. Checked: none; the bad call itself fails.
class CarDeck {
private final int capacityCm;
private int usedCm;
CarDeck(int capacityCm) {
this.capacityCm = capacityCm;
}
boolean fits(int lengthCm) {
return usedCm + lengthCm <= capacityCm;
}
void board(int lengthCm) {
usedCm += lengthCm;
}
void leave(int lengthCm) {
usedCm -= lengthCm;
}
int usedCm() {
return usedCm;
}
}CarDeck never said what it expects. Its code assumes several things that nobody wrote down:
Unwritten assumption | Who broke it here | What happened |
|---|---|---|
A car leaving was aboard | The retried cancellation | The deck subtracted a car that wasn't there |
Lengths are positive | Nobody, yet | A negative length would board a car and free up room |
| Nobody, yet | A caller that skips the check overloads the deck |
Used length stays between 0 and the capacity | The two above, together | The deck's own numbers stopped being true |
The practice checks can't help here: they read a design's structure, and this is about the values passing through it at run time.
3 · The fix
Write the contract down, and check it
Start from the first version, turn each assumption into a check, and fail at once when one is broken:
board() requires a positive length that fits; leave() requires a positive length no larger than what's aboard. A small require() throws when one isn't met, with a message that names the call.
class CarDeck {
private final int capacityCm;
private int usedCm;
CarDeck(int capacityCm) {
require(capacityCm > 0, "a deck needs a positive capacity");
this.capacityCm = capacityCm;
}
boolean fits(int lengthCm) {
return usedCm + lengthCm <= capacityCm;
}
void board(int lengthCm) {
require(lengthCm > 0, "board(" + lengthCm + ") needs a positive length");
require(fits(lengthCm), "board(" + lengthCm + ") needs room; check fits() first");
usedCm += lengthCm;
checkInvariant();
}
void leave(int lengthCm) {
String call = "leave(" + lengthCm + ")";
require(lengthCm > 0, call + " needs a positive length");
require(lengthCm <= usedCm,
call + " needs " + lengthCm + " cm aboard, only " + usedCm + " cm used");
usedCm -= lengthCm;
checkInvariant();
}
int usedCm() {
return usedCm;
}
private void checkInvariant() {
if (usedCm < 0 || usedCm > capacityCm) {
throw new IllegalStateException("used length out of range: " + usedCm + " cm");
}
}
private static void require(boolean condition, String message) {
if (!condition) throw new IllegalArgumentException(message);
}
}Now the retried cancellation fails at the call that's wrong, with a message that says why, and the deck's numbers stay true. The fourth car of the second batch is turned away, because the deck really is full:
board 450: used 4.50 m board 480: used 9.30 m leave 480: used 4.50 m leave 480: refused, leave(480) needs 480 cm aboard, only 450 cm used board 520: used 9.70 m board 510: used 14.80 m board 500: used 19.80 m board 470: no room
The retried call checks its preconditions before it changes anything, and fails with a reason. The deck's state is untouched.
The deck didn't fix the booking system's bug; it made it impossible to miss. The real fix is in the caller: a retried cancellation should find the car already gone and do nothing.
4 · The principle
Preconditions, postconditions and invariants
Design by contract treats each method as an agreement between a caller and the class. The caller must meet the method's preconditions; in return, the method guarantees its postconditions; and throughout, the class keeps its invariants, the facts that are true of every object between calls. A broken contract is a bug, and it says whose bug: a failed precondition is the caller's, a failed postcondition or invariant is the class's.
Part | Who must keep it | For CarDeck |
|---|---|---|
Precondition | The caller, before the call |
|
Postcondition | The method, when it returns | After |
Invariant | The class, between every two calls | The used length is between 0 and the capacity |
The contract simplifies both sides. The caller knows exactly what it must do and what it gets back; the method doesn't need code for situations its preconditions rule out. That's the difference from defensive programming, where a method quietly copes with anything, such as ignoring a negative length: coping hides the caller's bug, and a contract exposes it.
Contracts also explain Liskov substitution precisely: a subclass may accept more than its parent (weaker preconditions) and promise more (stronger postconditions and invariants), but never the reverse.
5 · When breaking it is fine
Checks cost time, and some belong elsewhere
Checks aren't free:
- Run time. An invariant that walks a large structure after every call can cost more than the work itself. That's why several languages let you turn assertions off.
- Duplicate checks. If every layer checks the same value, the rule lives in several places and can drift.
- Noise. A method that is mostly checks hides what it does.
Leave a condition unchecked, but still written down, when checking it is expensive and the code is hot, when it's already guaranteed by a type (a value object that can't hold a negative length needs no check), or inside a private helper whose only callers are in the same class and already checked it. Disabling checks in production is a trade: faster code, and bugs that surface later, the way the first version's did.
Check | Keep it on in production? | Why |
|---|---|---|
A public method's precondition | Yes | It's part of the method's published contract, and cheap |
A cheap invariant | Usually | It catches the class's own bugs where they happen |
An expensive invariant or postcondition | Often not | Run it in tests and debug builds instead |
6 · Getting it right
Validate input at the boundary; check contracts inside
Two kinds of bad value look alike and need different handling:
- Bad input from outside, such as a length typed into a booking form as "4,5", is expected. Validate it where it enters the program, and tell the user what to fix.
- A broken contract inside, such as the retried cancellation, is a bug. Fail fast, loudly, with a message for the programmer.
Once input is validated at the boundary, the code inside can rely on its preconditions instead of checking again at every layer.
Language | Preconditions on public methods | Assertions for internal checks |
|---|---|---|
Java | Throw |
|
C++ | Throw |
|
JavaScript | Throw an | No built-in: |
Python | Raise |
|
Because assertions can be switched off, never use them for a public method's preconditions or for anything with a side effect: the check, and the effect, would vanish in an optimized build.
7 · Look-alikes
Contracts and their neighbors
Several ideas deal with bad values. They differ in whose problem the value is.
Idea | The question it answers |
|---|---|
Design by contract | What does each method require, ensure and keep true, and whose bug is a violation? |
Defensive programming | How does a method cope with anything it's given? It can hide the caller's bug |
Input validation | Is outside data acceptable? Expected failures, reported to the user |
Errors and exceptions | How does a failure travel to code that can handle it? |
Value objects | Can a type make the bad value impossible to construct, so no check is needed later? |
Liskov substitution | How may a subclass change its parent's contract? |
The strongest contract is one the type system enforces. A Length value object that refuses negative numbers in its constructor turns "lengths are positive" from a check in every method into a fact about every Length; see Value objects, equality and immutability.
8 · In practice
Contracts in the libraries you already use
Standard libraries publish contracts all the time. A list's get(index) requires an index inside the list and throws an index exception when it isn't; Java's Objects.requireNonNull states a precondition in one call; Guava's Preconditions offers checkArgument for arguments and checkState for the object's state.
In the practice problems, the same thinking applies to the vending machine's coins and selections and the parking lot's spots: decide which bad requests are a customer's mistake to report, and which are a programming error to stop at once.
Check yourself
4 questions
Takeaways
Remember this
- A method's contract has three parts: what it requires, what it ensures, and what stays true of its object.
- A failed precondition is the caller's bug; a failed postcondition or invariant is the class's.
- Check contracts and fail at once, so a bug stops where it happens instead of surfacing later.
- Validate outside input at the boundary; check contracts inside for programming errors.
- Assertions can be switched off, so public preconditions should throw ordinary exceptions.