There are two main ways to reuse code between classes.
- Inheritance: a class is a kind of another class (
Dog extends Animal). - Composition: a class has another object and uses it (
Carhas anEngine).
The problem with deep inheritance
Imagine a hierarchy built only with inheritance:
Bird
├── FlyingBird
│ ├── Sparrow
│ └── Eagle
└── NonFlyingBird
├── Penguin
└── Ostrich
Now add swimming birds, birds that cannot sing, birds that can talk… The tree explodes, and some classes do not fit anywhere.
The same idea with composition
Split the behaviours into small objects and give each bird the ones it needs.
<?php
interface MoveBehaviour
{
public function move(): string;
}
class Fly implements MoveBehaviour
{
public function move(): string { return "flies"; }
}
class Swim implements MoveBehaviour
{
public function move(): string { return "swims"; }
}
class Walk implements MoveBehaviour
{
public function move(): string { return "walks"; }
}
class Bird
{
public function __construct(private string $name, private MoveBehaviour $movement) {}
public function describe(): string
{
return $this->name . " " . $this->movement->move();
}
}
$birds = [
new Bird("Sparrow", new Fly()),
new Bird("Penguin", new Swim()),
new Bird("Ostrich", new Walk()),
];
foreach ($birds as $bird) {
echo $bird->describe() . "\n";
}
Sparrow flies
Penguin swims
Ostrich walks
There is one Bird class. A new way of moving is a new small class, and it can even change while the program runs (a bird can swap its behaviour).
A realistic example: notifications
<?php
interface Channel
{
public function send(string $to, string $message): string;
}
class EmailChannel implements Channel
{
public function send(string $to, string $message): string
{
return "Email to $to: $message";
}
}
class SmsChannel implements Channel
{
public function send(string $to, string $message): string
{
return "SMS to $to: $message";
}
}
class Notifier
{
/** @param Channel[] $channels */
public function __construct(private array $channels) {}
public function notify(string $to, string $message): array
{
return array_map(fn(Channel $c) => $c->send($to, $message), $this->channels);
}
}
$notifier = new Notifier([new EmailChannel(), new SmsChannel()]);
echo implode("\n", $notifier->notify("Ana", "Your order shipped"));
Email to Ana: Your order shipped
SMS to Ana: Your order shipped
Notifier has channels. It does not inherit from any of them.
How to choose
| Question | Choose |
|---|---|
| Can you say “X is a Y” and mean it everywhere Y is used? | Inheritance is possible |
| Do you only want to reuse some code from another class? | Composition |
| Is the behaviour something that might change or combine? | Composition |
| Is it a framework base class you must extend? | Inheritance |
A guideline
Prefer composition over inheritance. Use inheritance for a true “is a” relationship with a stable parent, and for classes that frameworks ask you to extend.
Interfaces and traits help
- Interfaces describe what a composed object must do.
- Traits can share small bits of code, but they are not a replacement for a well-designed object.
The "Liskov" check
Penguin that throws an error on fly()), the inheritance is wrong. That is a strong sign to use composition.