interface Channel { send(to: string, message: string): Promise<void>;}class SmsChannel implements Channel { async send(to: string, message: string) { /* API SMS */ }}class Notifier { constructor(private channels: Channel[]) {} async notify(to: string, message: string) { await Promise.all(this.channels.map((c) => c.send(to, message))); }}// On assemble les comportements au lieu d'empiler des sous-classesconst notifier = new Notifier([new SmsChannel(), new EmailChannel()]);Explanation
With inheritance, you quickly end up with classes like SmsAndEmailNotifierWithLogging. Composition means assembling small interchangeable objects that share the same contract (an interface).
Adding a channel (WhatsApp, push…) means writing a new class without touching
Notifier.Testing becomes easy: pass a fake channel to
Notifier.Changing behaviour at runtime is possible: the channel list can come from configuration.
Inheritance remains useful for a genuine, stable "is a" relationship (a Dog is an Animal). To share behaviour, prefer "has a".

