Subclassing Order per shipping policy (FlatRateOrder, FreeOverFiftyOrder...) multiplies classes with every rule. Composition hands Order a rule object behind a small interface (Subsection 4.10.1), and a decorator such as FreeOver wraps one rule in another at run time.
<?php
interface ShippingRule { public function cost(int $subtotal): int; }
final class FlatRate implements ShippingRule {
public function __construct(private int $cents) {}
public function cost(int $subtotal): int { return $this->cents; }
}
final class FreeOver implements ShippingRule { // decorator: wraps another rule
public function __construct(private int $min, private ShippingRule $next) {}
public function cost(int $s): int { return $s >= $this->min ? 0 : $this->next->cost($s); }
}
final class Order { // has-a rule, not is-a subclass
public function __construct(private ShippingRule $rule, private array $lines = []) {}
public function total(): int {
$s = array_sum($this->lines);
return $s + $this->rule->cost($s);
}
}
$rule = new FreeOver(5000, new FlatRate(499));
echo new Order($rule, [1999])->total(), ' ', new Order($rule, [1999, 3500])->total(), "\n";Output
2498 5499
Each rule is testable alone, and Order never changes when one is added. Inherit when the child truly is a specialized parent; compose when combining behaviors (traits: Subsection 4.10.5).