ソフトウェア設計において、「継承を使うべきか、コンポジションを使うべきか」という問いは、多くのエンジニアが直面する重要なテーマです。
継承はオブジェクト指向の基本概念のひとつですが、誤った使い方をすると結合度が高まり、保守性を著しく損なう原因となります。
一方、コンポジションは柔軟性と再利用性に優れた設計手法として、現代のソフトウェア開発で広く推奨されています。
本記事では、継承とコンポジションの違い、is-a関係とhas-a関係の考え方、委譲・集約・結合度・凝集度・保守性の観点から、両者の使い分けと設計指針をわかりやすく解説していきます。
継承とコンポジションの違い:結論として「コンポジションを優先」が基本指針
それではまず、継承とコンポジションの違いについて、結論から解説していきます。
GoF(Gang of Four)のデザインパターン本でも提唱されているように、「継承よりコンポジションを優先せよ(Favor Composition over Inheritance)」という指針が現代の設計の基本です。
これは継承が悪いということではなく、コンポジションの方が多くの場面で柔軟性と保守性に優れているという意味です。
継承は「is-a関係」、コンポジションは「has-a関係」を表現するものとして使い分けるのが設計の基本といえるでしょう。
継承とコンポジションの基本的な違いをまとめると以下のとおりです。
継承:親クラスの性質をすべて引き継ぎ、サブクラスを作成します。is-a関係(犬は動物である)を表現します。
コンポジション:別のオブジェクトを内部に保持し、その機能を利用します。has-a関係(車はエンジンを持つ)を表現します。
設計の方向性を誤ると、後になってから大規模なリファクタリングが必要になることも少なくありません。
最初から正しい関係性を見極めることが、長期的な保守性の向上につながるでしょう。
is-a関係とhas-a関係とは何か
is-a関係とは、「AはBの一種である」という関係のことです。
たとえば「犬は動物の一種である」という関係は、継承で表現するのが自然でしょう。
is-a関係が成立する場合にのみ継承を使用するのが、正しい設計の基本原則です。
has-a関係は「AはBを持っている」という関係であり、たとえば「車はエンジンを持っている」という関係はコンポジションで表現します。
日本語でこの2つを区別するだけで、設計の方向性が大きく明確になるでしょう。
委譲とは何か:コンポジションの実現手段
委譲(デリゲーション)とは、あるオブジェクトが受け取った処理を、内部に保持する別のオブジェクトに任せる仕組みのことです。
コンポジションは委譲によって実現されることが多く、「使う側」と「使われる側」の関係が明確になります。
たとえば、「LogService」クラスが「FileWriter」オブジェクトを内部に保持し、ログ書き込みの処理を委譲する設計がその典型例です。
委譲を活用することで、クラスの責任範囲を小さく保ちつつ、柔軟に機能を組み合わせられます。
継承と比較して、実装の入れ替えが容易になるため、テストのしやすさにも大きく貢献するでしょう。
集約とコンポジションの違い
コンポジションと似た概念に「集約(アグリゲーション)」があります。
コンポジションは「全体が消えれば部品も消える」強い所有関係を表し、集約は「全体が消えても部品は存在し続ける」弱い関係を表します。
たとえば、「部署」と「社員」の関係は集約であり、部署が解散しても社員は他の部署に移れます。
一方、「人間」と「心臓」の関係はコンポジションであり、人間が存在しなければ心臓も意味をなしません。
UMLクラス図では、コンポジションは塗りつぶしひし形、集約は空白ひし形で表現します。
継承の問題点と過剰継承が生む設計の歪み
続いては、継承の問題点と、過剰に継承を使うことで生じる設計上の歪みを確認していきます。
継承は強力な機能ですが、その力を正しく扱わないと深刻な問題を引き起こします。
結合度の増加という問題
継承を使うと、子クラスは親クラスの実装に強く依存することになります。
これを「脆弱な基底クラス問題(Fragile Base Class Problem)」と呼び、親クラスの変更が予期しない形で子クラスに影響を与えるリスクが生じます。
結合度が高まると、コードの変更が連鎖的に広がり、保守コストが増大するでしょう。
特に、継承の階層が深くなるほどこの問題は顕著になります。
設計の初期段階から継承の深さに意識を向けることが、後々の問題を防ぐ重要なポイントです。
リスコフの置換原則との関係
リスコフの置換原則(LSP)とは、「子クラスは親クラスの代わりに使えるべきである」という原則です。
この原則に違反する継承関係は、バグの温床になりやすい問題のある設計といえます。
たとえば、「正方形は長方形の一種」という数学的な関係をそのまま継承で表現しようとすると、LSPに違反することが知られています。
正方形の幅と高さは常に等しいという制約が、長方形の振る舞い(幅と高さを独立して変更できる)を壊すためです。
このような場合には継承を避け、インターフェースやコンポジションで関係を表現するのが適切でしょう。
多重継承の問題と回避策
PythonやC++では多重継承が可能ですが、「ダイヤモンド問題」と呼ばれる曖昧性の問題が生じることがあります。
JavaやTypeScriptなどでは多重継承を禁止し、代わりにインターフェースで複数の型を実装できる設計が採用されているでしょう。
多重継承が必要と感じる場面の多くは、コンポジションや委譲で代替できます。
設計の複雑さを増やさないためにも、多重継承への安易な依存は避けるべきです。
インターフェースを活用してポリモーフィズムを実現しつつ、具体的な実装はコンポジションで組み合わせるのが現代的なアプローチといえます。
コンポジションの実践的な活用方法と設計指針
続いては、コンポジションを実際の設計で活かすための具体的な方法と設計指針を確認していきます。
コンポジションを正しく活用することで、柔軟で保守しやすいシステムが構築できるでしょう。
StrategyパターンによるコンポジションのDesign活用
Strategyパターンは、コンポジションを活用した代表的なデザインパターンです。
アルゴリズムをカプセル化し、実行時に差し替えられるようにすることで、継承を使わずに柔軟な振る舞いの切り替えを実現します。
たとえば、「ソート戦略」インターフェースを定義し、「バブルソート」「クイックソート」などの実装を切り替える設計がその典型例です。
これにより、コードを修正せずに新しいアルゴリズムを追加できる拡張性の高い設計が生まれます。
Strategyパターンは開放閉鎖の原則(OCP)を実現する代表的な手法でもあります。
Decoratorパターンによる機能の動的追加
Decoratorパターンも、コンポジションを活用した重要なデザインパターンのひとつです。
オブジェクトを別のオブジェクトで包むことで、クラスを変更せずに動的に機能を追加できるという特徴があります。
たとえば、テキスト表示機能に「太字」「斜体」「下線」などの装飾を動的に組み合わせる設計がその例です。
継承を使って同じことを実現しようとすると、組み合わせの数だけクラスが必要になり、クラス爆発という問題が発生します。
Decoratorパターンはそのような問題をコンポジションで解決する、エレガントな設計手法といえるでしょう。
凝集度を高める設計のコツ
コンポジションを活用する際にも、凝集度の高い設計を意識することが重要です。
各クラスが1つの明確な責任を持ち、その責任に関連する処理だけをまとめることで、凝集度の高い設計が実現されます。
コンポジションによって複数のクラスを組み合わせる場合、各クラスの責任範囲を明確に定義することが設計品質の鍵です。
「このクラスは何のためにあるか」を一言で説明できる設計が、凝集度の高い設計の目安になるでしょう。
凝集度を意識することで、自然と保守しやすく、テストしやすいコードが生まれます。
継承とコンポジションの使い分け:判断基準の整理
続いては、継承とコンポジションをどのような基準で使い分けるかを整理していきます。
継承を使うべき場面
継承を使うべき場面は、is-a関係が明確に成立しており、リスコフの置換原則を満たせる場合に限定するのが基本です。
「犬は動物の一種である」「管理者はユーザーの一種である」といった関係が典型的な例です。
また、フレームワークのテンプレートメソッドパターンのように、親クラスが共通の処理フローを定義し、具体的な処理を子クラスに委ねる設計も継承が有効な場面です。
継承の階層は浅く保ち、2〜3レベルを超えないように意識するのが望ましいでしょう。
コンポジションを使うべき場面
コンポジションは、has-a関係を表現する場合、実装を柔軟に切り替えたい場合、継承階層が深くなりそうな場合に使うのが適切です。
「車はエンジンを持つ」「注文は商品リストを持つ」といった関係はコンポジションで表現します。
また、実行時に振る舞いを変更したい場合も、コンポジションの方が圧倒的に柔軟です。
テストのしやすさを重視する場合にも、依存するオブジェクトを差し替えやすいコンポジションが優れています。
設計レビューでのチェックポイント
設計レビューの場で継承とコンポジションの使い分けを確認する際には、以下のような観点でチェックするとよいでしょう。
「子クラスは親クラスのすべての機能を適切に引き継いでいるか」「is-a関係が本当に成立しているか」を必ず確認することが重要です。
「親クラスの変更が子クラスに予期しない影響を与えないか」もチェックポイントのひとつです。
コンポジションを使っている場合は、「内部に保持するオブジェクトの責任が明確か」「不要な依存関係が生じていないか」を確認します。
| 項目 | 継承 | コンポジション |
|---|---|---|
| 関係性 | is-a関係 | has-a関係 |
| 結合度 | 高い | 低い |
| 柔軟性 | 低い | 高い |
| 実装の切り替え | 困難 | 容易 |
| テストのしやすさ | やや低い | 高い |
| 保守性 | 階層が深いと低下 | 比較的高い |
まとめ
本記事では、継承とコンポジションの違い、is-a関係とhas-a関係の考え方、委譲・集約・結合度・凝集度・保守性の観点から、両者の使い分けと設計指針について解説しました。
「継承よりコンポジションを優先せよ」という指針は、現代のオブジェクト指向設計における重要な基本原則です。
継承はis-a関係が明確な場面に限定し、それ以外の多くの場面ではコンポジションを選択することで、保守性と拡張性の高い設計が実現できるでしょう。
設計の段階でis-a関係とhas-a関係を意識的に区別することが、長期的に優れたコードベースを作り上げる第一歩となります。