Composition over Inheritance は、「継承より合成を優先する」という設計原則です。
オブジェクト指向を学ぶと継承が便利に見えますが、使いすぎると変更に弱くなります。
一言でいうと
継承で親子関係を深くするより、小さな部品を組み合わせるほうが変更しやすい場面があります。
継承は「AはBの一種である」という関係を表します。合成は「AはBを持っている」という関係を表します。
継承の例
class EmailNotifier {
send(message: string) {
console.log(`email: ${message}`);
}
}
class WelcomeEmailNotifier extends EmailNotifier {
sendWelcome(userName: string) {
this.send(`Welcome ${userName}`);
}
}
単純な例では問題ありません。しかし、継承階層が深くなると、親クラスの変更が子クラスへ広く影響します。
合成の例
class WelcomeMessageService {
constructor(private readonly notifier: Notifier) {}
sendWelcome(userName: string) {
this.notifier.send(`Welcome ${userName}`);
}
}
通知方法を外から渡しています。メール通知、Slack通知、テスト用通知を差し替えやすくなります。
継承がつらくなるサイン
| サイン | 問題 |
|---|---|
| 親クラスが大きい | 子クラスが不要な機能まで持つ |
| 子クラスが増えすぎる | 変更影響が読みにくい |
| overrideが多い | 親の設計と合わなくなっている |
| 継承階層が深い | 実際の処理を追うのが大変 |
| テストしにくい | 親クラスの状態に依存する |
継承は便利ですが、親の都合に子が縛られます。
合成が向いている場面
- 処理の一部を差し替えたい
- テスト用の実装を渡したい
- 複数の振る舞いを組み合わせたい
- 親子関係より役割の組み合わせで表せる
- 将来の変更点が部品ごとに違う
たとえば、通知、保存、ログ、認証、支払いなどの外部処理は、合成やDIと相性がよいです。
継承を使ってよい場面
継承が悪いわけではありません。
「本当にAはBの一種」と言える場合、フレームワークの拡張ポイントとして決まっている場合、共通の契約を表す場合には使えます。
ただし、コード再利用だけを目的に継承するのは注意です。
まとめ
Composition over Inheritance は、継承より合成を優先して考える設計原則です。
親子関係で縛るより、小さな役割を組み合わせるほうが、差し替えやテストがしやすくなります。