Inheritance and composition
There are two ways to relate classes to each other: inheritance and composition. This chapter looks at how to choose between them.
Inheritance (an is-a relationship)
Inheritance is a mechanism by which one class takes on the characteristics of another.
In PHP you use extends.
abstract class PaymentMethod
{
abstract public function pay(int $amount): void;
public function logPayment(int $amount): void
{
error_log("Payment: {$amount} yen");
}
}
class CreditCardPayment extends PaymentMethod
{
public function pay(int $amount): void
{
// Credit card payment processing (omitted)
}
}
Because CreditCardPayment inherits from PaymentMethod, it can use the logPayment method as is.
You use inheritance when the relationship "a CreditCardPayment is a PaymentMethod" holds naturally.
This is called an is-a relationship.
Composition (a has-a relationship)
Composition is a mechanism by which one class holds another as a property.
class Order
{
public function __construct(
private readonly PaymentMethod $paymentMethod,
) {
}
public function checkout(int $amount): void
{
$this->paymentMethod->pay($amount);
}
}
An Order is not a payment method itself; it merely has one.
The relationship "an Order has a PaymentMethod" is called a has-a relationship.
Having Order inherit from PaymentMethod (Order extends PaymentMethod) would be unnatural.
An order is not a kind of payment method.
Why composition is preferred over inheritance
Even where an is-a relationship holds, inheritance has a weakness. Take notifications as an example. Say there are two axes: the delivery channel (email, SMS) and the urgency (normal, urgent).
Expressing that with inheritance alone looks like this.
abstract class Notification
{
abstract public function send(string $message): void;
}
class EmailNotification extends Notification
{
public function send(string $message): void { /* ... */ }
}
class SmsNotification extends Notification
{
public function send(string $message): void { /* ... */ }
}
class UrgentEmailNotification extends EmailNotification
{
public function send(string $message): void { /* send email as urgent */ }
}
class UrgentSmsNotification extends SmsNotification
{
public function send(string $message): void { /* send SMS as urgent */ }
}
With two axes (channel and urgency) four classes are enough, but each additional axis multiplies the class count. This "combinatorial explosion" is pointed out as a weakness of inheritance in Design Patterns by the Gang of Four1.
With composition you can separate the axes into parts.
enum Urgency
{
case Normal;
case Urgent;
}
interface NotificationChannel
{
public function send(string $message): void;
}
class EmailChannel implements NotificationChannel
{
public function send(string $message): void { /* ... */ }
}
class SmsChannel implements NotificationChannel
{
public function send(string $message): void { /* ... */ }
}
class Notification
{
public function __construct(
private readonly NotificationChannel $channel,
private readonly Urgency $urgency,
) {
}
public function send(string $message): void
{
$prefix = $this->urgency === Urgency::Urgent ? '[URGENT] ' : '';
$this->channel->send($prefix . $message);
}
}
The delivery channel and the urgency are held as independent parts.
Adding one more channel means adding one class that implements NotificationChannel, and the class count never multiplies.
Summary
- Use inheritance when "an A is a B" holds naturally (an is-a relationship)
- Use composition when the relationship is "an A has a B" (a has-a relationship)
- Even where an is-a relationship holds, several axes make inheritance prone to a combinatorial explosion in class count
- Separating the axes into parts with composition keeps the class count additive as axes are added
What to read next
The next chapter covers interfaces and depending on abstractions. We look at designing against contracts, as the entry point to dependency inversion.
Exercises
Rewrite the following classes from inheritance to a design that uses composition
abstract class Discount
{
abstract public function apply(int $price): int;
}
class MemberDiscount extends Discount
{
public function apply(int $price): int
{
return (int) ($price * 0.9);
}
}
class SeasonalMemberDiscount extends MemberDiscount
{
public function apply(int $price): int
{
return (int) ($price * 0.9 * 0.8);
}
}
Assume you want to combine the member discount (10% off) and the seasonal discount (a further 20% off) independently.
Sample answer
interface DiscountRate
{
public function rate(): float;
}
class MemberDiscountRate implements DiscountRate
{
public function rate(): float
{
return 0.9;
}
}
class SeasonalDiscountRate implements DiscountRate
{
public function rate(): float
{
return 0.8;
}
}
class Discount
{
private array $rates = [];
public function addRate(DiscountRate $rate): void
{
$this->rates[] = $rate;
}
public function apply(int $price): int
{
foreach ($this->rates as $rate) {
$price = (int) ($price * $rate->rate());
}
return $price;
}
}
Without a dedicated combination class like SeasonalMemberDiscount, addRate lets you combine discount types freely.
Adding a new kind of discount means adding one class that implements DiscountRate.