What is Design by Contract?
Design by Contract (DbC), introduced by Bertrand Meyer for the Eiffel language, is an approach to software design where components define formal, precise, and verifiable interface specifications. It uses preconditions, postconditions, and invariants to specify the obligations and benefits of each component.
Think of it like a legal contract: both parties (caller and callee) have obligations, and both get benefits. If the caller fulfills the preconditions, the callee guarantees the postconditions.
The Three Pillars
1. Preconditions
โโโโโโโโโโโโโโ
What the CALLER must guarantee before calling a method.
If the precondition is not met, the method is not obligated to work.
Think: "You must do this before calling me."
Example: withdraw(amount) requires amount > 0 && amount <= balance
2. Postconditions
โโโโโโโโโโโโโโโ
What the METHOD guarantees after it completes (if preconditions were met).
Think: "I will do this if you meet the requirements."
Example: withdraw(amount) guarantees balance decreased by amount
3. Invariants
โโโโโโโโโโโ
Conditions that must ALWAYS be true for the object, before and after
any method call. Think: "This must always hold."
Example: balance must always be >= 0
Implementation Example
class BankAccount {
#balance;
#owner;
constructor(owner, initialBalance) {
// Invariant check: balance must be non-negative
if (initialBalance < 0) {
throw new Error("Invariant violated: initial balance cannot be negative");
}
this.#owner = owner;
this.#balance = initialBalance;
}
// Precondition: amount must be positive
// Postcondition: balance increases by amount
// Invariant: balance remains >= 0
deposit(amount) {
// โโโ PRECONDITION โโโ
if (amount <= 0) {
throw new Error("Precondition failed: deposit amount must be positive");
}
const oldBalance = this.#balance;
// โโโ METHOD BODY โโโ
this.#balance += amount;
// โโโ POSTCONDITION โโโ
if (this.#balance !== oldBalance + amount) {
throw new Error("Postcondition failed: balance not updated correctly");
}
// โโโ INVARIANT โโโ
this.#checkInvariants();
return this.#balance;
}
// Precondition: amount > 0 && amount <= balance
// Postcondition: balance decreases by amount
// Invariant: balance remains >= 0
withdraw(amount) {
// โโโ PRECONDITION โโโ
if (amount <= 0) {
throw new Error("Precondition failed: withdrawal amount must be positive");
}
if (amount > this.#balance) {
throw new Error("Precondition failed: insufficient funds");
}
const oldBalance = this.#balance;
// โโโ METHOD BODY โโโ
this.#balance -= amount;
// โโโ POSTCONDITION โโโ
if (this.#balance !== oldBalance - amount) {
throw new Error("Postcondition failed: balance not updated correctly");
}
// โโโ INVARIANT โโโ
this.#checkInvariants();
return this.#balance;
}
get balance() {
return this.#balance;
}
// โโโ INVARIANT CHECK โโโ
#checkInvariants() {
if (this.#balance < 0) {
throw new Error("Invariant violated: balance is negative");
}
if (!this.#owner || this.#owner.length === 0) {
throw new Error("Invariant violated: owner cannot be empty");
}
}
}
// Usage
const account = new BankAccount("Alice", 1000);
account.deposit(500); // โ Works โ preconditions met
account.withdraw(200); // โ Works โ preconditions met
try {
account.withdraw(-50); // โ Fails precondition
} catch (e) {
console.log(e.message); // "Precondition failed: withdrawal amount must be positive"
}
try {
account.withdraw(99999); // โ Fails precondition
} catch (e) {
console.log(e.message); // "Precondition failed: insufficient funds"
}
DbC in Practice
Benefits of Design by Contract:
1. Clear Documentation
Contracts ARE the documentation. No separate docs needed.
"What does this method expect? What does it guarantee?"
2. Better Error Detection
Catch errors at the point they occur, not downstream.
Preconditions catch bad inputs immediately.
3. Facilitates Reuse
Clear contracts make it safe to reuse components.
You know exactly what you need to provide.
4. Supports Testing
Contracts serve as built-in test assertions.
Every method call is implicitly a test.
Contracts vs Assertions:
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Feature โ Design by Contract โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Scope โ Per method and per class โ
โ Part of API โ Yes โ part of the interface โ
โ Can be disabled โ In production for performance โ
โ Checked at โ Call boundaries โ
โ Includes โ Pre, Post, Invariant โ
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DbC and Inheritance
When a subclass overrides a method, it must respect the contract of the parent class. This is closely related to the Liskov Substitution Principle:
DbC Rules for Inheritance:
Precondition Rule (Covariance):
Subclass preconditions must be EQUAL TO OR WEAKER than
the parent's. The subclass cannot add new preconditions.
Parent: withdraw(amount) โ amount > 0
Child: withdraw(amount) โ amount > 0 โ (same)
Child: withdraw(amount) โ amount > 0 && isBusinessDay โ (stronger)
Postcondition Rule (Contravariance):
Subclass postconditions must be EQUAL TO OR STRONGER than
the parent's. The subclass cannot weaken guarantees.
Parent: withdraw(amount) โ balance decreases by amount
Child: withdraw(amount) โ balance decreases by amount โ (same)
Child: withdraw(amount) โ balance decreases by amount minus fee โ (weaker)
Invariant Rule:
Subclass invariants must include ALL parent invariants.
The subclass can add new invariants but cannot remove parent ones.
Parent: balance >= 0
Child: balance >= 0 && accountActive == true โ (added invariant)
Child: (no balance check) โ (removed parent invariant)