composition-over-inheritance について
クラスの振る舞いをどう構成するかを支援するソフトウェア設計スキルです。継承に頼らず、インターフェースを通じて機能を組み合わせる方針と、継承が適切な限定的な場面を示します。
主な機能
- 01継承よりコンポジションを優先
- 02インターフェースで機能を組み合わせ
- 03継承設計の警告サインを特定
- 04限定的な型階層で継承を使用
活用シーン
- 01複数の振る舞いを持つクラスの設計
- 02不自然または深いクラス階層の見直し
- 03テストしやすい振る舞いの分離
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill composition-over-inheritanceインストール後、Claude Code で/composition-over-inheritanceを入力して読み込まれたか確認します。
詳細
継承よりコンポジション
概要
クラス継承よりオブジェクトコンポジションを優先してください。
継承は密結合と硬直した階層を生みます。コンポジションは、自由に組み合わせられる柔軟で再利用可能なコンポーネントを生みます。
使用する場面
- クラス間の関係を設計するとき
extendsを使いたくなったとき- クラスが複数のソースから振る舞いを必要とするとき
- 「is-a」関係を作るとき
- クラス階層を構築するとき
絶対ルール
NEVER use inheritance when composition would work.
例外はありません:
- 「これがオブジェクト指向のやり方だから」
- 「is-a 関係だから」
- 「extends でコードを再利用するため」
- 「ポリモーフィズムのため」
デフォルトではコンポジションを選んでください。継承を使うのは、本当の型階層に限ってください。
見極め方:継承のにおい
継承に違和感がある、または無理があると感じたら、コンポジションを使ってください:
// ❌ VIOLATION: Inheritance hierarchy
class Animal {
eat(): void { console.log('Eating'); }
}
class FlyingAnimal extends Animal {
fly(): void { console.log('Flying'); }
}
class SwimmingAnimal extends Animal {
swim(): void { console.log('Swimming'); }
}
// Duck needs both fly AND swim - inheritance can't do this cleanly
class Duck extends FlyingAnimal {
swim(): void { console.log('Swimming'); } // Duplicated!
}
正しいパターン:インターフェースを使ったコンポジション
機能をインターフェースとして定義し、それらを組み合わせます:
// ✅ CORRECT: Composition
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
interface Eatable {
eat(): void;
}
// Reusable behaviors
const flyingBehavior: Flyable = {
fly() { console.log('Flying'); }
};
const swimmingBehavior: Swimmable = {
swim() { console.log('Swimming'); }
};
const eatingBehavior: Eatable = {
eat() { console.log('Eating'); }
};
// Compose what you need
class Duck implements Flyable, Swimmable, Eatable {
fly = flyingBehavior.fly;
swim = swimmingBehavior.swim;
eat = eatingBehavior.eat;
}
class Fish implements Swimmable, Eatable {
swim = swimmingBehavior.swim;
eat = eatingBehavior.eat;
}
class Bird implements Flyable, Eatable {
fly = flyingBehavior.fly;
eat = eatingBehavior.eat;
}
継承がうまくいかない理由
| 問題 | 例 |
|---|---|
| ダイヤモンド問題 | Duck には飛行と水泳の両方が必要 |
| 密結合 | 子クラスが親クラスの内部を知っている |
| 硬直した階層 | 親クラスを変えると子クラスが壊れる |
| 強制された継承 | 必要のないメソッドまで受け継ぐ |
| 脆弱な基底クラス | 親クラスの変更ですべての子クラスが壊れる |
コンポジションが優れている理由
| 利点 | 例 |
|---|---|
| 柔軟 | あらゆる振る舞いを組み合わせられる |
| 疎結合 | コンポーネント同士が互いを知らない |
| テストが簡単 | 個々の振る舞いをモックできる |
| 実行時の変更 | 振る舞いを動的に差し替えられる |
| 階層に縛られない | 新しい組み合わせを自由に追加できる |
圧力に対する対処法
1. 「これがオブジェクト指向のやり方」
圧力:「オブジェクト指向プログラミングでは継承を使う」
**回答:**現代のオブジェクト指向プログラミングではコンポジションが優先されます。継承は使われすぎています。
**対応:**インターフェースとコンポジションを使ってください。それでもオブジェクト指向です。
2. 「これは Is-A 関係」
圧力:「Duck は Bird なので、Bird を拡張するべきだ」
**回答:**要件が変わると、「is-a」は「has-a」になることがよくあります。コンポジションならどちらにも対応できます。
対応:「ある型である」ではなく、「振る舞いを持つ」としてモデル化してください。
3. 「extends でコードを再利用」
圧力:「親クラスのメソッドが必要だ」
**回答:**コンポジションなら、結合させずにコードをよりうまく再利用できます。
**対応:**共有する振る舞いを、組み合わせ可能な単位として切り出してください。
4. 「ポリモーフィズムには継承が必要」
圧力:「異なる型を一様に扱う必要がある」
**回答:**インターフェースを使えば、継承なしでポリモーフィズムを実現できます。
**対応:**インターフェースを定義し、クラスに実装させてください。
危険信号 — 作業を止めて再検討してください
次のいずれかに気づいたら、代わりにコンポジションを使ってください:
- コードに
extendsキーワードがある - クラス階層が 2 階層を超えている
- 子クラスが親クラスのメソッドをオーバーライドしている
- 「ダイヤモンド問題」がある — 複数の親クラスが必要
- サブクラスが親クラスのすべてのメソッドを使っていない
- 親クラスを変更すると子クラスが壊れる
- 親クラスをインスタンス化しないとテストが難しい
これらはすべて、コンポジションにリファクタリングすべきという意味です。
継承が適切な場合
次の場合に限り、継承を使ってください:
- 本当の型階層がある(まれです)
- フレームワークが必要とする(React のクラスコンポーネントなど)
- 自分で制御できないライブラリのクラスを拡張する
その場合でも、階層は浅く保ってください(最大 2 階層)。
クイックリファレンス
| 継承 | コンポジション |
|---|---|
class Dog extends Animal | class Dog implements Animal + 振る舞いの注入 |
| 硬直した階層 | 柔軟なコンポジション |
| 親クラスは 1 つだけ | 複数の振る舞い |
| 密結合 | 疎結合 |
| 変更が連鎖する | 変更の影響を局所化できる |
よくある合理化(すべて誤り)
| 言い訳 | 実情 |
|---|---|
| 「これがオブジェクト指向のやり方」 | 現代のオブジェクト指向プログラミングではコンポジションが優先されます。 |
| 「これは is-a 関係」 | 「振る舞いを持つ」と考えるほうが柔軟です。 |
| 「親クラスのメソッドが必要」 | 代わりに、その振る舞いを組み合わせてください。 |
| 「ポリモーフィズムに必要」 | インターフェースでポリモーフィズムを実現できます。 |
| 「extends のほうがコードが少ない」 | コンポジションのほうが柔軟性に優れます。 |
| 「問題があるとメモした」 | 問題があるなら使わないでください。 |
要点
振る舞いは合成しましょう。継承するのではありません。
クラスを設計するときは、能力ごとにインターフェースを定義し、合成可能な振る舞いを作成して、各クラスに必要なものを注入してください。extends は最後の手段としてのみ使用してください。