class Notifier { send(msg: string) { /* email */ } }
class SmsNotifier extends Notifier { /* overrides send */ }
class LoggedSmsNotifier extends SmsNotifier { /* adds logging */ }
class RetryingLoggedSmsNotifier extends LoggedSmsNotifier { /* adds retry */ }
// Now we need logging + retry for email too...
It means: build behaviour by combining small objects, instead of by extending classes. The phrase comes from the Gang of Four book (Design Patterns).
Why inheritance causes problems:
- It is fixed at compile time. You cannot change it while the program runs.
- It is a white-box relationship: the child depends on the parent's internal code.
- Each new combination of features needs a new subclass.
The hierarchy above explodes as soon as every channel needs logging and retry.
With composition, an object gets behaviour by holding other objects behind interfaces. Steps:
- Define
interface Notifier { send(msg: string): Promise<void> }. - Implement
EmailNotifierandSmsNotifier. - Write
LoggingNotifierandRetryingNotifier. Each one wraps anyNotifier.
Now new RetryingNotifier(new LoggingNotifier(new SmsNotifier())) builds any combination at runtime. Each class has one job, and you can test each class alone.
Inheritance is still fine for a true, stable is-a relationship, where the child keeps the parent's contract (its promised behaviour).
- Inheritance couples the child to the parent's code, and you need a new subclass for every combination.
- Composition builds behaviour from small objects behind an interface.
- You can combine wrappers at runtime in any order.
- Inheritance is still fine for real, stable is-a relationships.