I'll Be Backend
Principles · PHP

Design by contract: guard the invariants

Let the object keep its own rules, so no caller can break them.

14 steps4 chapters~6 min at 1.25×#design by contract#invariants#preconditions#encapsulation
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the PHP video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–9

Payoff

Steps 10–12

Recap

Steps 13–14

The code: before and after

Before

final class Stock
{
    private int $onHand;
    private int $reserved = 0;
    public function __construct(int $onHand)
    {
        $this->onHand = $onHand;
    }
    public function onHand(): int
    {
        return $this->onHand;
    }
    public function reserved(): int
    {
        return $this->reserved;
    }
    public function setOnHand(int $onHand): void
    {
        $this->onHand = $onHand;
    }
    public function setReserved(int $reserved): void
    {
        $this->reserved = $reserved;
    }
}
final class OrderDesk
{
    public function reserve(Stock $stock, int $quantity): void
    {
        if ($quantity < 1
            || $quantity > $stock->onHand() - $stock->reserved()) {
            throw new InvalidArgumentException("cannot reserve $quantity");
        }
        $stock->setReserved($stock->reserved() + $quantity);
    }
}
final class Dispatch
{
    public function ship(Stock $stock, int $quantity): void
    {
        if ($quantity < 1 || $quantity > $stock->reserved()) {
            throw new InvalidArgumentException("cannot ship $quantity");
        }
        $stock->setReserved($stock->reserved() - $quantity);
        $stock->setOnHand($stock->onHand() - $quantity);
    }
}
$lamps = new Stock(10);
(new OrderDesk())->reserve($lamps, 8);

After

final class Stock
{
    private int $onHand;
    private int $reserved = 0;
    public function __construct(int $onHand)
    {
        $this->onHand = $onHand;
        $this->checkInvariant();
    }
    public function onHand(): int
    {
        return $this->onHand;
    }
    public function reserved(): int
    {
        return $this->reserved;
    }
    public function available(): int
    {
        return $this->onHand - $this->reserved;
    }
    public function reserve(int $quantity): void
    {
        if ($quantity < 1 || $quantity > $this->available()) {
            throw new InvalidArgumentException("cannot reserve $quantity");
        }
        $this->reserved += $quantity;
        $this->checkInvariant();
    }
    public function ship(int $quantity): void
    {
        if ($quantity < 1 || $quantity > $this->reserved) {
            throw new InvalidArgumentException("cannot ship $quantity");
        }
        $this->reserved -= $quantity;
        $this->onHand -= $quantity;
        $this->checkInvariant();
    }
    public function writeOff(int $quantity): void
    {
        if ($quantity < 1 || $quantity > $this->available()) {
            throw new InvalidArgumentException("cannot write off $quantity");
        }
        $this->onHand -= $quantity;
        $this->checkInvariant();
    }
    private function checkInvariant(): void
    {
        if ($this->reserved < 0 || $this->reserved > $this->onHand) {
            throw new LogicException('invariant broken');
        }
    }
}
final class OrderDesk
{
    public function reserve(Stock $stock, int $quantity): void
    {
        $stock->reserve($quantity);
    }
}
final class Dispatch
{
    public function ship(Stock $stock, int $quantity): void
    {
        $stock->ship($quantity);
    }
}
final class Damage
{
    public function writeOff(Stock $stock, int $quantity): void
    {
        $stock->writeOff($quantity);
    }
}
$lamps = new Stock(10);
(new OrderDesk())->reserve($lamps, 8);
(new Damage())->writeOff($lamps, 5);
(new Dispatch())->ship($lamps, 8);

Transcript, step by step

Problem

Step 1: Ten desk lamps in stock Before

This is a small shop's stock of one product, desk lamps, and it keeps two counts, on hand and reserved. Each count has a public setter, and the order desk and dispatch both check the counts first and then call those setters. The script at the bottom starts with ten lamps and reserves eight of them for customers.

Step 2: Write off damaged lamps Change request

The warehouse wants to write off damaged lamps, so we add a Damage class with a write off method. It lowers on hand through the setter and checks nothing, because nothing tells its author there's a rule to keep. At the bottom, the script now writes off five lamps and then ships the eight it reserved.

Step 3: Every caller keeps the rule Code smell

The stock has a rule, reserved is never more than on hand, and here every caller has to remember it, because the stock itself never checks. The order desk checks it here, dispatch checks only reserved here and trusts that on hand still covers it, and the new write off checks nothing here. Start with ten on hand and eight reserved, then write off five, and you have five lamps for eight promised orders. Dispatch then ships eight from five on hand, so on hand drops to minus three, and no error shows up anywhere. Every new caller of a setter is one more place where someone can forget the rule.

Refactor

Step 4: Back it out, refactor first Plan

Let's back the write off out. The rule belongs to the stock, so we'll move every check into Stock first, and then add the write off there.

Step 5: Name what's available Extract Method

First, on hand minus reserved gets a name, available, as a method on Stock. The order desk's guard calls it, so the check means the same and still throws for the same quantities.

Step 6: Stock reserves itself Move Function

Next, reserve moves into Stock, with the guard clause as its first lines. That guard is the method's precondition, what must be true before the call, and any caller can read it right at the top. The order desk now just calls reserve on the stock, and the same quantities still throw the same exception.

Step 7: Stock ships itself Move Function

Ship moves the same way, and its precondition says you can only ship lamps that are reserved. Dispatch is down to one call to ship on the stock.

Step 8: Delete the setters Remove Setting Method

Nothing outside Stock calls set on hand or set reserved anymore, so we delete both setters. From here on, only Stock's own methods can change its counts, because the fields are private and PHP refuses any write from outside the class.

Step 9: Check the invariant Introduce Assertion

An invariant is a rule about an object's fields that holds after every public method, and here it's that reserved is never below zero and never above on hand. Check Invariant throws a Logic Exception when that rule breaks, and the constructor, reserve and ship each call it as their last line. A check at the end of a method is a postcondition, so the invariant is now a promise of every method that changes the stock. We throw on purpose, because PHP's assert is switched off in production by default, through the zend assertions setting. The one new behavior is in the constructor, which now refuses a negative on hand, because a stock of minus one breaks the invariant.

Payoff

Step 10: Add the write off again Payoff

Now the write off goes back in, this time as a method on Stock. Its precondition says you can only write off lamps that nobody reserved, and after lowering on hand it checks the invariant. Damage calls write off on the stock in one line, just like the order desk and dispatch. Reserve, ship and check invariant didn't change at all.

Step 11: It fails at the write off Payoff

Run the same day again, with ten on hand and eight reserved, and write off five. Only two lamps are available, so the write off throws right at the call with cannot write off five, and the counts stay ten and eight. The lamps are still broken, so the warehouse has to cancel three reservations first, but someone sees the overselling today, at the write off, before dispatch ships anything.

Step 12: Why it was easy Payoff

The order desk and dispatch now hold no checks, and the write off states its own precondition at the top. If a later method forgets its precondition, check invariant still throws before that method returns, so the error shows up at that call and not later at dispatch. The precondition is what keeps the counts unchanged, and the invariant catches what a missing precondition lets through. That's design by contract, and it fits here because the rule belongs to the stock, so the stock states and checks it once, and callers only meet a precondition they can read on the method.

Recap

Step 13: Where we started, where we ended Before / after

On the left, the callers changed the counts through Stock's setters, and Stock checked nothing itself. The order desk and dispatch each kept the rule with their own guard, and dispatch kept only half of it. On the right, the setters are gone, and Stock writes off through a method that checks its precondition first. Check invariant guards the rule at the end of it, the same check that every other method runs.

Step 14: When to reach for it Takeaways

Reach for it when an object has a rule between its fields, and several callers change those fields. Skip it when the object is plain data with no rule between its fields, like a record the code only reads. You'll spot it by callers that check an object's fields before they call its setters, with the same check in several places. PHP's own functions state preconditions too, so since PHP eight, str repeat throws a Value Error when its count is negative.