Composition over Inheritance(継承よりコンポジション)とは?柔軟で保守性の高い設計を実現する原則を徹底解説
オブジェクト指向設計では、コード再利用や共通機能の整理を目的として、継承が長く利用されてきました。親クラスに共通処理を定義し、子クラスがそれを引き継ぐことで、似たようなクラスを効率よく実装できるためです。特に、明確な親子関係があるモデルでは、継承は分かりやすく強力な仕組みとして機能します。
しかし、継承を多用すると、クラス階層が複雑になり、親クラスの変更が多くの子クラスへ影響する問題が発生しやすくなります。また、コード再利用だけを目的に不自然な継承関係を作ると、リスコフの置換原則に違反したり、不要な機能まで子クラスが引き継いだりすることがあります。こうした問題を避ける考え方として重視されるのが、Composition over Inheritance(継承よりコンポジション)です。
Composition over Inheritanceは、継承を完全に否定する原則ではありません。むしろ、継承を使う前に、オブジェクトを組み合わせるコンポジションで実現できないかを検討する設計方針です。コンポジションを使えば、機能を部品として分離し、必要な振る舞いを柔軟に組み合わせられるため、保守性や拡張性の高い設計を作りやすくなります。
本記事では、Composition over Inheritanceの基本概念、継承とコンポジションの違い、is-a関係とhas-a関係、委譲、Strategy・Decorator・Adapterパターンとの関係、SOLID原則とのつながり、ゲーム開発・Web開発・フロントエンド開発での活用、実務でのベストプラクティスまで体系的に解説します。
1. Composition over Inheritance(継承よりコンポジション)とは?
Composition over Inheritance(継承よりコンポジション)とは、コード再利用や機能拡張を行う際に、継承よりもコンポジションを優先して検討する設計原則です。コンポジションとは、あるオブジェクトが別のオブジェクトを部品として持ち、その機能を利用する設計方法です。親クラスから機能を受け継ぐのではなく、必要な機能を持つオブジェクトを組み合わせて振る舞いを構成します。
この考え方が重視される理由は、継承が強い結合を生みやすいからです。継承では、子クラスが親クラスの構造や振る舞いに強く依存します。一方、コンポジションでは、必要な機能を外部オブジェクトとして持たせるため、機能の差し替えや追加がしやすくなります。変化しやすいシステムでは、継承よりもコンポジションの方が柔軟な設計になりやすいです。
主な特徴
| 項目 | 内容 |
|---|---|
| 英語名 | Composition over Inheritance |
| 日本語 | 継承よりコンポジション |
| 中心思想 | 継承よりもオブジェクトの組み合わせを優先する |
| 主な目的 | 柔軟性・保守性・拡張性の向上 |
| 関連概念 | 委譲、has-a関係、Strategyパターン、Decoratorパターン |
1.1 継承との違い
継承とコンポジションの大きな違いは、機能を「受け継ぐ」のか「持つ」のかという点です。継承では、子クラスが親クラスの性質や振る舞いを引き継ぎます。コンポジションでは、クラスが別のオブジェクトを内部に持ち、そのオブジェクトに処理を委譲します。
| 比較項目 | 継承 | コンポジション |
|---|---|---|
| 関係性 | is-a関係 | has-a関係 |
| 再利用方法 | 親クラスの機能を引き継ぐ | 部品オブジェクトを組み合わせる |
| 結合度 | 高くなりやすい | 低く保ちやすい |
| 変更への強さ | 親クラス変更の影響を受けやすい | 部品の差し替えで対応しやすい |
| 適した場面 | 明確で安定した親子関係 | 柔軟な機能追加や差し替え |
継承は、親子関係が明確で安定している場合には有効です。一方で、単にコードを再利用したいだけで継承を使うと、不自然なクラス階層が生まれやすくなります。コンポジションは、機能を小さな部品として分離できるため、変更や拡張に強い設計を実現しやすくなります。
1.2 なぜ重要なのか
Composition over Inheritanceが重要なのは、現代のソフトウェア開発では仕様変更や機能追加が継続的に発生するからです。継承を中心に設計すると、最初はシンプルでも、要件が増えるにつれてクラス階層が複雑化し、変更時の影響範囲が広がりやすくなります。
コンポジションを優先すれば、機能を独立した部品として扱えるため、必要な機能だけを組み合わせたり、後から差し替えたりしやすくなります。これは、Webアプリケーション、ゲーム開発、フロントエンド設計、マイクロサービス、ライブラリ設計など、多くの領域で有効な考え方です。
2. オブジェクト指向設計における再利用
オブジェクト指向設計では、コード再利用は重要なテーマです。同じ処理を何度も書かず、共通化できる部分を整理することで、開発効率や保守性を高められます。ただし、再利用の方法を誤ると、かえって変更に弱い設計になることがあります。
継承もコンポジションも、どちらも再利用の手段です。継承は親クラスの機能を子クラスが利用する方法であり、コンポジションは別オブジェクトの機能を組み合わせて利用する方法です。重要なのは、再利用したいからすぐ継承するのではなく、どちらが変更に強いかを考えて選択することです。
2.1 コード再利用の考え方
コード再利用の目的は、同じ処理を重複して書かないことだけではありません。再利用によって、変更箇所を限定し、品質を安定させ、システム全体の理解を容易にすることも重要です。そのため、再利用する単位や責務の分け方が大切になります。
安易な再利用は、逆に保守性を下げることがあります。見た目が似ている処理を無理に共通化すると、将来的に片方だけ仕様が変わったときに対応しづらくなります。再利用では、同じ責務を持っているか、同じ変更理由を持っているかを確認する必要があります。
2.2 継承による再利用
継承による再利用では、親クラスに共通処理を定義し、子クラスがそれを引き継ぎます。たとえば、Animalクラスに共通の属性やメソッドを持たせ、DogやCatがそれを継承するような設計です。親子関係が自然であれば、継承は分かりやすい再利用方法になります。
しかし、継承は親クラスと子クラスの結合を強めます。親クラスの変更がすべての子クラスに影響する可能性があり、子クラスが不要な機能まで引き継ぐこともあります。コード再利用だけを目的に継承を使うと、設計が硬直化しやすくなります。
2.3 コンポジションによる再利用
コンポジションによる再利用では、必要な機能を持つオブジェクトを内部に持ち、その処理を利用します。たとえば、キャラクターが移動機能、攻撃機能、描画機能をそれぞれコンポーネントとして持つような設計です。機能を部品として組み合わせるため、柔軟性が高くなります。
コンポジションでは、機能の追加や差し替えがしやすくなります。移動方法を変更したい場合は、MoveBehaviorを差し替えればよく、キャラクター全体の継承階層を変更する必要はありません。これにより、変化に強い設計を作れます。
3. 継承(Inheritance)の仕組み
継承とは、あるクラスが別のクラスの属性やメソッドを引き継ぐ仕組みです。親クラスに共通処理を定義し、子クラスがそれを再利用したり、必要に応じて上書きしたりできます。オブジェクト指向プログラミングにおける基本機能の一つです。
継承は、明確な親子関係がある場合には有効です。たとえば、Vehicleを親クラスとしてCarやBikeを定義するような設計は理解しやすい場合があります。ただし、関係性が曖昧なまま継承を使うと、不自然なクラス階層やLSP違反を招くことがあります。
継承の主な特徴
| 項目 | 内容 |
|---|---|
| 英語名 | Inheritance |
| 日本語 | 継承 |
| 関係性 | is-a関係 |
| 主な目的 | 共通機能の継承・親子関係の表現 |
| 注意点 | 親クラスへの強い依存が発生しやすい |
3.1 is-a関係とは
is-a関係とは、「AはBの一種である」と自然に言える関係です。たとえば、Dog is an Animal、Car is a Vehicleのように、子クラスが親クラスの一種として扱える場合、継承が適している可能性があります。
| 項目 | 内容 |
|---|---|
| 意味 | あるクラスが別のクラスの一種である関係 |
| 例 | Dog is an Animal |
| 適した設計 | 安定した分類階層 |
| 注意点 | 単なる機能共有だけでは使わない |
| 関連原則 | LSP |
is-a関係では、子クラスが親クラスとして安全に扱える必要があります。親クラスの利用者が子クラスを渡されても問題なく動作するなら、継承関係は自然です。逆に、子クラスが親クラスの振る舞いを満たせない場合は、継承ではなくコンポジションを検討するべきです。
3.2 継承のメリット
継承のメリットは、共通処理を親クラスにまとめられることです。複数の子クラスで同じ属性やメソッドを使う場合、親クラスに定義することで重複を減らせます。また、親クラスを通じて共通インターフェースを提供できる点も便利です。
さらに、継承は分類構造を表現しやすいという利点があります。ドメイン上、明確な親子関係が存在し、その構造が長期的に安定している場合、継承は自然で分かりやすい設計になります。
3.3 継承が利用される場面
継承は、フレームワークの拡張や抽象クラスの実装、共通仕様を持つオブジェクトの分類などで利用されます。たとえば、BaseControllerを継承して各Controllerを作る、AbstractRepositoryを継承して具体Repositoryを作るといったケースがあります。
ただし、継承を使う場合は、親子関係が本当に自然かを確認する必要があります。コード再利用だけが目的なら、継承ではなくコンポジションの方が適している場合が多くあります。
4. コンポジション(Composition)の仕組み
コンポジションとは、あるオブジェクトが別のオブジェクトを内部に持ち、その機能を利用する設計方法です。継承のように親クラスから機能を受け継ぐのではなく、必要な機能を持つ部品を組み合わせて振る舞いを作ります。
コンポジションでは、has-a関係が中心になります。たとえば、Car has an Engine、Character has a Weapon、UserService has a UserRepositoryのように、あるオブジェクトが別のオブジェクトを持つ関係です。これにより、機能を柔軟に差し替えられます。
コンポジションの主な特徴
| 項目 | 内容 |
|---|---|
| 英語名 | Composition |
| 日本語 | コンポジション |
| 関係性 | has-a関係 |
| 主な目的 | オブジェクトを組み合わせて機能を構成する |
| 効果 | 柔軟な拡張・疎結合・再利用性向上 |
4.1 has-a関係とは
has-a関係とは、「AはBを持っている」と表現できる関係です。たとえば、車はエンジンを持つ、ユーザーサービスはリポジトリを持つ、キャラクターは移動機能を持つ、といった関係です。
| 項目 | 内容 |
|---|---|
| 意味 | あるオブジェクトが別のオブジェクトを部品として持つ関係 |
| 例 | Car has an Engine |
| 適した設計 | 機能の組み合わせ・差し替え |
| メリット | 結合度を下げやすい |
| 関連概念 | 委譲・依存注入 |
has-a関係では、機能を持つ部品を組み替えることで振る舞いを変えられます。これにより、継承階層を増やさずに機能追加や変更を行いやすくなります。特に変更が多いシステムでは、has-a関係を中心に設計する方が柔軟です。
4.2 オブジェクトの組み合わせ
コンポジションでは、複数のオブジェクトを組み合わせて機能を構成します。たとえば、ゲームキャラクターを移動コンポーネント、攻撃コンポーネント、描画コンポーネント、AIコンポーネントの組み合わせとして設計できます。
この設計では、キャラクターごとに必要な機能だけを持たせられます。空を飛ぶ敵にはFlightComponent、地上を歩く敵にはWalkComponentを持たせるようにすれば、継承階層を複雑にせずに多様な振る舞いを表現できます。
4.3 委譲による実装
コンポジションでは、内部に持つオブジェクトへ処理を委譲することが多くあります。たとえば、UserServiceがデータ取得をUserRepositoryに委譲するような設計です。UserService自身がDB操作を行うのではなく、Repositoryに任せます。
委譲により、各オブジェクトは自分の責務に集中できます。Serviceは業務処理、Repositoryはデータアクセス、Notifierは通知処理というように分離できるため、保守性とテスト容易性が向上します。
5. 継承の問題点
継承は強力な仕組みですが、使い方を誤ると多くの問題を引き起こします。特に、コード再利用だけを目的に継承を使うと、クラス間の結合が強くなり、変更に弱い設計になりやすくなります。
継承の問題は、最初は見えにくいことがあります。小さなクラス階層では便利に見えても、機能追加や仕様変更が増えるにつれて、親クラスの変更が多くの子クラスへ影響したり、継承階層が理解しにくくなったりします。
5.1 強い結合が発生する
継承では、子クラスが親クラスの実装に強く依存します。親クラスのメソッドやフィールドを前提に子クラスが作られるため、親クラスを変更すると子クラスに影響する可能性があります。これは保守性を下げる原因になります。
特に、親クラスが多くの責務を持っている場合、子クラスは不要な機能まで引き継ぐことがあります。これにより、子クラスの振る舞いが不自然になり、設計の意図が分かりにくくなります。
5.2 クラス階層が複雑化する
継承を多用すると、クラス階層が複雑化します。親クラス、子クラス、孫クラスが増えると、あるメソッドがどこで定義され、どこで上書きされているのかを追うのが難しくなります。
クラス階層が深くなるほど、コード理解の負担が大きくなります。修正時には、親クラスとすべての子クラスの関係を確認する必要があり、影響範囲を把握しづらくなります。
5.3 変更の影響範囲が広がる
継承では、親クラスの変更が複数の子クラスに影響することがあります。共通処理を修正したつもりでも、一部の子クラスでは想定外の動作になる可能性があります。これは継承の大きなリスクです。
変更影響を抑えるには、安定した共通仕様だけを親クラスに持たせることが重要です。変化しやすい振る舞いは、継承ではなくコンポジションで差し替え可能にする方が安全です。
6. コンポジションが注目される理由
コンポジションが注目される理由は、柔軟性、疎結合、保守性を高めやすいからです。変化の多い現代の開発では、最初に決めたクラス階層がずっと適切であり続けるとは限りません。機能追加や仕様変更に合わせて、振る舞いを組み替えられる設計が求められます。
コンポジションでは、機能を部品として分け、必要に応じて組み合わせます。これにより、継承階層を増やさずに多様な振る舞いを表現でき、変更時の影響も抑えやすくなります。
6.1 柔軟な機能追加
コンポジションを使うと、新しい機能を部品として追加できます。既存クラスを継承して新しい子クラスを増やすのではなく、新しいコンポーネントやStrategyを追加し、それを組み合わせることで振る舞いを拡張できます。
この方法では、既存コードへの変更を最小限にできます。たとえば、通知方法を追加する場合、新しいNotificationSenderを作成し、既存の通知処理に組み込めばよい設計にできます。
6.2 疎結合な設計
コンポジションでは、オブジェクト同士をインターフェースや抽象を通じて接続しやすくなります。これにより、具体実装への依存を減らし、疎結合な設計を実現できます。
疎結合な設計では、部品の差し替えが容易になります。たとえば、メール通知からSlack通知へ変更する場合でも、共通インターフェースに従った実装を差し替えるだけで対応しやすくなります。
6.3 保守性の向上
コンポジションは、保守性の向上にも役立ちます。機能ごとに責務が分離されるため、修正箇所を特定しやすくなります。移動処理、保存処理、通知処理、認証処理などを独立した部品として扱えば、変更時の影響を限定できます。
また、部品ごとに単体テストを行いやすくなります。継承階層全体を確認する必要がなく、個別のコンポーネントやStrategyをテストできるため、品質を保ちやすくなります。
7. is-a関係とhas-a関係の違い
is-a関係とhas-a関係は、継承とコンポジションを判断するうえで重要な考え方です。is-a関係は「AはBの一種である」という関係で、継承に向いています。has-a関係は「AはBを持っている」という関係で、コンポジションに向いています。
| 比較項目 | is-a関係 | has-a関係 |
|---|---|---|
| 意味 | AはBの一種である | AはBを持っている |
| 主な設計手法 | 継承 | コンポジション |
| 例 | Dog is an Animal | Car has an Engine |
| 適した場面 | 安定した分類構造 | 機能の組み合わせ |
| 注意点 | 不自然な継承に注意 | 部品数が増えすぎないよう注意 |
この違いを正しく理解すると、継承を使うべきか、コンポジションを使うべきか判断しやすくなります。重要なのは、コード再利用だけで判断しないことです。関係性が自然か、変更に強いか、責務が明確かを基準に考える必要があります。
7.1 継承が適切なケース
| 項目 | 内容 |
|---|---|
| 関係性 | 明確なis-a関係がある |
| 階層 | 親子関係が安定している |
| 変更頻度 | 親クラスの仕様が頻繁に変わらない |
| 利用条件 | 子クラスが親クラスとして安全に扱える |
| 注意点 | コード再利用だけを目的にしない |
継承が適切なのは、親子関係が明確で、子クラスが親クラスの一種として自然に扱える場合です。たとえば、抽象的なShapeに対してCircleやRectangleを定義するようなケースでは、継承が有効な場合があります。
ただし、子クラスが親クラスの振る舞いを一部拒否する場合や、親クラスのメソッドを無理に上書きする場合は注意が必要です。そのような設計はLSP違反につながる可能性があります。
7.2 コンポジションが適切なケース
| 項目 | 内容 |
|---|---|
| 関係性 | has-a関係が自然 |
| 機能 | 機能を組み合わせたい |
| 変更頻度 | 振る舞いの追加・変更が多い |
| 利用条件 | 部品を差し替えたい |
| 注意点 | 依存関係を整理する必要がある |
コンポジションが適切なのは、機能を柔軟に組み合わせたい場合です。たとえば、キャラクターに移動方法や攻撃方法を持たせる、ServiceにRepositoryを持たせる、ControllerにUseCaseを持たせるといった設計です。
コンポジションを使うと、部品の差し替えや追加がしやすくなります。変更が多い領域や、機能の組み合わせが増える領域では、継承よりもコンポジションの方が適していることが多いです。
7.3 設計判断の基準
| 判断基準 | 継承を検討する場合 | コンポジションを検討する場合 |
|---|---|---|
| 関係性 | is-a関係が自然 | has-a関係が自然 |
| 変更頻度 | 階層が安定している | 振る舞いが変化しやすい |
| 再利用目的 | 共通仕様を共有したい | 機能を部品化したい |
| 拡張方法 | 子クラスを追加する | 部品を追加・差し替える |
| リスク | 親クラス変更の影響 | 部品構成の複雑化 |
設計判断では、「本当にis-a関係か」を最初に確認することが重要です。単に同じメソッドを使いたいだけなら、継承よりもコンポジションが適している可能性が高いです。
また、将来的に振る舞いを変更したり、組み合わせを変えたりする可能性がある場合は、コンポジションを優先して検討する方が安全です。継承は安定した分類、コンポジションは柔軟な構成に向いています。
8. コンポジションによる機能拡張
コンポジションによる機能拡張では、既存クラスを継承して新しい子クラスを増やすのではなく、機能を持つオブジェクトを追加・差し替えすることで振る舞いを拡張します。この方法は、変更に強く、保守しやすい設計を実現しやすくします。
たとえば、通知機能をメール、SMS、チャット通知として分け、それぞれを共通インターフェースで扱う設計にすれば、通知方法を柔軟に追加できます。利用側は通知手段の詳細を知らず、共通の通知処理だけを呼び出せます。
8.1 動的な機能追加
コンポジションでは、実行時に機能を差し替えたり追加したりする設計が可能です。たとえば、設定やユーザー種別に応じて異なるStrategyやComponentを利用することができます。
これは、継承では実現しづらい柔軟性です。継承ではクラス階層が固定されやすい一方、コンポジションでは部品の組み合わせによって振る舞いを変えられます。
8.2 機能の差し替え
コンポジションでは、同じインターフェースを持つ別実装へ差し替えることができます。たとえば、開発環境ではMockPaymentClient、本番環境ではRealPaymentClientを使うように切り替えられます。
この差し替えやすさは、テストや環境ごとの設定にも役立ちます。外部APIやデータベースのように変化しやすい依存先は、コンポジションとインターフェースで扱うと柔軟性が高まります。
8.3 再利用性向上
コンポジションでは、機能を小さな部品として分離するため、再利用性が向上します。特定のクラス階層に依存しない部品であれば、複数の場所で利用しやすくなります。
たとえば、ログ機能、バリデーション機能、通知機能、キャッシュ機能などは、コンポジションによってさまざまなServiceに組み込めます。これにより、重複を減らしながら柔軟な設計を実現できます。
9. 委譲(Delegation)とは?
委譲とは、あるオブジェクトが自分で処理を行うのではなく、内部に持っている別のオブジェクトへ処理を任せることです。コンポジションでは、委譲がよく使われます。オブジェクトを組み合わせ、その部品に具体的な処理を任せることで、責務を分離します。
委譲を使うと、クラスは自分の役割に集中できます。たとえば、UserServiceはユーザー登録の業務フローを管理し、データ保存はUserRepositoryに委譲し、通知送信はNotificationServiceに委譲するような設計です。
主な特徴
| 項目 | 内容 |
|---|---|
| 英語名 | Delegation |
| 日本語 | 委譲 |
| 目的 | 別オブジェクトに処理を任せる |
| 関係 | コンポジションと組み合わせて使われる |
| 効果 | 責務分離・疎結合化・テスト容易性向上 |
9.1 メソッド呼び出しの仕組み
| 流れ | 内容 |
|---|---|
| 1 | 呼び出し元オブジェクトがメソッドを受け取る |
| 2 | 自分で処理せず、内部の部品オブジェクトを呼び出す |
| 3 | 部品オブジェクトが具体処理を実行する |
| 4 | 結果を呼び出し元へ返す |
| 5 | 呼び出し元が必要に応じて後続処理を行う |
委譲では、呼び出し元が全ての処理を抱え込む必要がありません。処理の専門性に応じて、適切なオブジェクトに任せます。これにより、1つのクラスが巨大化することを防げます。
また、委譲先をインターフェースとして扱えば、実装を差し替えることもできます。これはStrategyパターンやRepositoryパターンでもよく使われる考え方です。
9.2 コンポジションとの関係
コンポジションは、オブジェクトを部品として持つ設計です。委譲は、その部品に処理を任せる動作です。つまり、コンポジションで構成し、委譲で振る舞いを実行するという関係があります。
この組み合わせにより、機能ごとに責務を分けられます。継承によって親クラスのメソッドを受け継ぐのではなく、必要な処理を担当する部品に任せるため、柔軟で変更に強い設計になります。
10. Strategyパターンとの関係
Strategyパターンは、Composition over Inheritanceを実践する代表的なデザインパターンです。処理方針やアルゴリズムをStrategyオブジェクトとして分離し、利用側がそれを保持して実行します。つまり、継承で振る舞いを変えるのではなく、コンポジションで振る舞いを差し替えます。
このパターンは、条件分岐が増えやすい処理に有効です。決済方法、認証方式、割引計算、通知方法など、複数の処理方針を切り替える場面でよく使われます。
10.1 振る舞いの切り替え
Strategyパターンでは、振る舞いを外部オブジェクトとして分離します。たとえば、PaymentServiceがPaymentStrategyを持ち、クレジットカード決済やQR決済などの具体処理をStrategyに委譲します。
これにより、PaymentService自体を継承して種類を増やす必要がありません。必要なStrategyを差し替えるだけで振る舞いを変更できます。これはコンポジションの典型的な活用例です。
10.2 条件分岐の削減
Strategyパターンを使うと、if文やswitch文による条件分岐を減らせます。処理方針ごとにStrategyクラスを分ければ、利用側は共通インターフェースを呼び出すだけで済みます。
条件分岐が減ることで、新しい処理を追加しやすくなります。既存の巨大なswitch文を修正するのではなく、新しいStrategyを追加する形にできるため、OCPにも合った設計になります。
10.3 コンポジション活用例
たとえば、通知処理を設計する場合、EmailNotification、SmsNotification、PushNotificationをそれぞれStrategyとして実装できます。NotificationServiceはNotificationStrategyを持ち、実行時に適切な通知方法を利用します。
この設計では、通知方法を増やしてもNotificationServiceを大きく変更する必要がありません。コンポジションによって機能を組み合わせることで、柔軟な拡張が可能になります。
11. Decoratorパターンとの関係
Decoratorパターンも、Composition over Inheritanceを実践する代表例です。Decoratorパターンでは、既存オブジェクトを別のオブジェクトで包み込むことで、機能を追加します。継承で機能を増やすのではなく、コンポジションで機能を重ねます。
このパターンは、複数の追加機能を柔軟に組み合わせたい場合に有効です。たとえば、ログ追加、キャッシュ追加、認証チェック、圧縮処理などをDecoratorとして重ねることができます。
11.1 機能追加の仕組み
Decoratorパターンでは、元のオブジェクトと同じインターフェースを持つDecoratorを作成します。Decoratorは内部に元のオブジェクトを持ち、処理の前後に追加処理を行います。
これにより、既存オブジェクトを変更せずに機能を追加できます。たとえば、DataSourceに対してCompressionDecoratorやEncryptionDecoratorを重ねることで、圧縮や暗号化の機能を追加できます。
11.2 継承との違い
継承で機能を追加する場合、機能の組み合わせごとに子クラスが増える可能性があります。たとえば、圧縮あり、暗号化あり、ログありの組み合わせを継承で表現すると、クラス数が増えやすくなります。
Decoratorでは、機能を個別のDecoratorとして分け、必要に応じて組み合わせます。これにより、継承階層を増やさずに柔軟な機能追加が可能になります。
11.3 柔軟な拡張方法
Decoratorパターンは、既存クラスを変更せずに機能を追加したい場合に有効です。これはOCPにも合っています。新しいDecoratorを追加すれば、新しい機能を組み込めます。
また、実行時にDecoratorの組み合わせを変えられる点もメリットです。設定や利用状況に応じて、ログあり、キャッシュあり、暗号化ありといった構成を柔軟に作れます。
12. Adapterパターンとの関係
Adapterパターンは、既存のインターフェースを利用側が期待する形に変換するパターンです。内部に変換対象のオブジェクトを持ち、その処理を委譲することが多いため、コンポジションと深く関係しています。
外部APIやレガシーシステムを利用する場合、内部設計と外部仕様が一致しないことがあります。Adapterを使えば、外部仕様を内部で扱いやすい形式に変換し、依存を局所化できます。
12.1 インターフェース変換
Adapterパターンでは、外部オブジェクトや既存クラスのインターフェースを、利用側が期待するインターフェースに変換します。利用側はAdapterを通じて処理を呼び出すため、外部仕様を直接意識しなくて済みます。
この設計では、Adapterが外部オブジェクトを内部に持ち、その処理を呼び出します。つまり、コンポジションと委譲を使ってインターフェース変換を実現しています。
12.2 既存システム連携
レガシーシステムや外部サービスと連携する場合、既存の仕様をそのまま内部に広げると保守性が低下します。Adapterを用意すれば、古い仕様や外部仕様をAdapter内に閉じ込められます。
これにより、内部システムは一貫したインターフェースで外部機能を利用できます。外部仕様が変わった場合も、Adapterを中心に修正すればよく、影響範囲を抑えられます。
12.3 コンポジションによる実装
Adapterは、変換対象のオブジェクトを内部に持つ形で実装されることが多いです。たとえば、OldPaymentClientをNewPaymentAdapterが内部に持ち、内部の呼び出し形式へ変換します。
この構造は、継承ではなくコンポジションによる拡張です。既存クラスを変更せず、外側にAdapterを追加することで、新しいインターフェースに対応できます。
13. SOLID原則との関係
Composition over Inheritanceは、SOLID原則と深く関係しています。特に、単一責任の原則、オープン・クローズドの原則、依存性逆転の原則と相性が良い考え方です。コンポジションを使うことで、責務を分離し、抽象に依存し、変更に強い構造を作りやすくなります。
SOLID原則は抽象的な設計原則ですが、コンポジションはそれを実装レベルで支える手段になります。機能を部品として分け、必要に応じて組み合わせる設計は、保守性の高いコードを書くうえで重要です。
13.1 SRPとの関係
SRPは、1つのクラスが1つの責任だけを持つべきという原則です。コンポジションを使うと、責務ごとに機能を分けやすくなります。たとえば、Serviceが通知、保存、計算をすべて行うのではなく、それぞれを専用オブジェクトへ委譲できます。
これにより、各クラスの責務が明確になります。変更理由も分離されるため、保守性が高まります。コンポジションはSRPを実現するための実践的な手段です。
13.2 OCPとの関係
OCPは、拡張には開き、修正には閉じるべきという原則です。コンポジションを使うと、新しい機能を追加する際に、既存クラスを直接変更せず、新しい部品を追加して対応しやすくなります。
StrategyやDecoratorは、その代表例です。新しいStrategyやDecoratorを追加することで、既存の利用側コードを大きく変更せずに機能拡張できます。これはOCPに沿った設計です。
13.3 DIPとの関係
DIPは、具象ではなく抽象に依存するべきという原則です。コンポジションでは、内部に持つ部品をインターフェースとして扱うことで、具体実装への依存を減らせます。
たとえば、UserServiceがUserRepositoryインターフェースに依存し、具体実装を外部から注入する設計にすれば、DB実装を差し替えやすくなります。コンポジションとDIPを組み合わせることで、疎結合な設計を実現できます。
14. LSPとの関係
LSPは、派生クラスが親クラスの代わりとして利用できなければならないという原則です。継承を使う場合、子クラスが親クラスの契約を守る必要があります。これが守れない場合、継承設計に問題があります。
Composition over Inheritanceは、LSP違反を避けるためにも有効です。不自然な継承関係を作る代わりに、コンポジションで必要な機能を持たせれば、置換可能性の問題を回避しやすくなります。
14.1 継承設計の制約
継承設計では、子クラスが親クラスの振る舞いを満たす必要があります。親クラスのメソッドを子クラスで無効化したり、例外を投げるようにしたりすると、利用者の期待を裏切ることになります。
この制約は、継承を安全に使うために重要です。しかし、現実の要件では、すべての子クラスが親クラスの振る舞いを自然に満たせるとは限りません。その場合は、継承ではなくコンポジションを検討するべきです。
14.2 置換可能性
置換可能性とは、親クラス型として扱っても子クラスが問題なく動作することです。たとえば、Animal型としてDogを扱っても自然に動くなら、継承関係は適切です。
しかし、子クラスが親クラスの一部機能を使えない場合、置換可能性が崩れます。これはLSP違反です。コンポジションを使えば、必要な機能だけを持たせられるため、不自然な置換関係を作らずに済みます。
14.3 継承の落とし穴
継承の落とし穴は、コード再利用のために不自然な親子関係を作ってしまうことです。共通メソッドを使いたいだけで継承すると、子クラスが親クラスの契約を満たせない場合があります。
このような場合、共通機能を部品として切り出し、コンポジションで利用する方が安全です。継承は分類を表すために使い、機能再利用にはコンポジションを優先することが重要です。
15. ゲーム開発での活用
ゲーム開発では、Composition over Inheritanceが非常に重要です。キャラクター、敵、アイテム、弾、罠、UIなど、多様なオブジェクトが存在し、それぞれが異なる機能の組み合わせを持つためです。継承だけで表現しようとすると、クラス階層が爆発的に増えることがあります。
コンポジションを使えば、移動、攻撃、描画、当たり判定、AI、ステータス、エフェクトなどをコンポーネントとして分離できます。各ゲームオブジェクトは必要なコンポーネントを持つことで、多様な振る舞いを柔軟に構成できます。
15.1 Entity Component System
Entity Component System、いわゆるECSは、コンポジションを強く活用した設計です。Entityは識別子として存在し、Componentがデータを持ち、Systemが処理を行います。継承によるキャラクター分類ではなく、機能の組み合わせでオブジェクトを表現します。
ECSでは、空を飛ぶ敵、地上を歩く敵、弾を撃つ敵、爆発する敵などを、コンポーネントの組み合わせで表現できます。これにより、継承階層を増やさずに多様なゲーム要素を作れます。
15.2 キャラクター機能管理
ゲームキャラクターは、多くの機能を持ちます。移動、攻撃、防御、アニメーション、入力、AI、サウンド、当たり判定などを1つの継承階層で管理すると、非常に複雑になります。
コンポジションを使えば、各機能を独立したコンポーネントとして管理できます。プレイヤーにはInputComponent、敵にはAIComponentを持たせるようにすれば、共通部分と差異を柔軟に扱えます。
15.3 コンポーネント設計
ゲーム開発におけるコンポーネント設計では、1つのコンポーネントが明確な責務を持つことが重要です。MoveComponentは移動、HealthComponentは体力、AttackComponentは攻撃、RenderComponentは描画を担当するように分けます。
この設計により、機能の追加や調整がしやすくなります。新しい敵を作る場合も、既存コンポーネントを組み合わせるだけで基本的な振る舞いを構成できるため、開発効率が向上します。
16. Web開発での活用
Web開発でも、Composition over Inheritanceは広く活用されます。Service、Repository、Middleware、Controller、Validator、Clientなどを組み合わせることで、責務を分離し、変更に強い設計を作れます。
継承ベースのBaseServiceやBaseControllerを多用すると、親クラスが肥大化しやすくなります。代わりに、必要な機能を小さな部品として分け、各クラスに注入する設計にすると柔軟性が高まります。
16.1 サービス設計
WebアプリケーションのService設計では、業務処理、データアクセス、通知、認証、ログなどを分離することが重要です。Serviceがすべてを直接実装するのではなく、RepositoryやNotifier、Validatorなどへ処理を委譲します。
これにより、Serviceは業務フローに集中できます。データ保存方法や通知方法が変わっても、委譲先を差し替えることで対応しやすくなります。
16.2 ミドルウェア構成
ミドルウェアは、コンポジション的な設計と相性が良いです。認証、ログ、エラーハンドリング、レート制限、CORS設定などを個別のミドルウェアとして分け、必要に応じて組み合わせます。
この構成では、各ミドルウェアが小さな責務を持ちます。機能を追加したい場合は新しいミドルウェアを追加し、不要なら外すことができます。継承よりも柔軟に処理パイプラインを構成できます。
16.3 APIアーキテクチャ
APIアーキテクチャでも、コンポジションは重要です。ControllerがUseCaseを呼び、UseCaseがRepositoryやGatewayを使い、Gatewayが外部API Adapterを使うような構造は、コンポジションによって成り立っています。
この設計により、各層の責務を分けられます。外部APIが変わってもAdapterを修正すればよく、業務ロジックやControllerへの影響を抑えられます。
17. フロントエンド開発での活用
フロントエンド開発では、コンポーネント指向設計そのものがコンポジションと深く関係しています。小さなUIコンポーネントを組み合わせて画面を構成する考え方は、Composition over Inheritanceの実践例です。
ReactやVueなどのモダンフロントエンドでは、継承よりもコンポジションが重視されます。UI、状態管理、ロジック、API通信を小さな部品として分離し、必要に応じて組み合わせることで、保守しやすい画面を作れます。
17.1 コンポーネント指向設計
コンポーネント指向設計では、ボタン、フォーム、カード、モーダル、リストなどのUI部品を組み合わせて画面を作ります。各コンポーネントは小さな責務を持ち、再利用しやすい形で設計されます。
継承によってUI部品を増やすよりも、propsやslots、childrenなどを使ってコンポジションする方が柔軟です。これにより、同じ部品をさまざまな画面で使いやすくなります。
17.2 UI機能の分離
フロントエンドでは、表示、状態管理、入力検証、API通信、イベント処理などを分けることが重要です。1つのコンポーネントにすべてを詰め込むと、巨大コンポーネントになり、保守が難しくなります。
コンポジションを使えば、UIコンポーネント、カスタムフック、状態管理ストア、APIクライアントを分離できます。表示は表示部品に、ロジックはHookやServiceに任せることで、見通しの良い設計になります。
17.3 状態管理との連携
状態管理でもコンポジションは有効です。UIコンポーネントが状態管理ロジックを直接持つのではなく、カスタムフックやStoreを組み合わせて利用する設計にすれば、状態管理を再利用しやすくなります。
たとえば、検索条件管理、フォーム状態管理、モーダル表示管理などを個別のHookとして分離すれば、複数の画面で同じロジックを再利用できます。これは継承よりも柔軟な再利用方法です。
18. マイクロサービスとの関係
マイクロサービスは、システム全体を小さな独立サービスの組み合わせとして設計するアーキテクチャです。この考え方は、広い意味でComposition over Inheritanceに近い発想を持っています。大きな一枚岩の継承構造ではなく、独立した部品を組み合わせてシステムを構成します。
各サービスは独立した責務を持ち、APIやイベントを通じて連携します。これにより、機能追加や変更をサービス単位で行いやすくなります。ただし、サービス分割や依存関係の設計を誤ると、システム全体が複雑化する点には注意が必要です。
18.1 サービス分離
マイクロサービスでは、注文、決済、在庫、通知、ユーザー管理などを独立したサービスとして分けることがあります。これは、責務ごとに機能を分離する設計です。
サービス分離により、各サービスは自分の責務に集中できます。決済仕様が変わった場合は決済サービスを中心に変更し、通知方法が変わった場合は通知サービスを変更するように、影響範囲を限定しやすくなります。
18.2 疎結合アーキテクチャ
マイクロサービスでは、サービス同士を疎結合に保つことが重要です。各サービスが他サービスの内部実装に依存すると、変更に弱い構造になります。API契約やイベントを通じて連携することで、独立性を高めます。
これはコンポジションの考え方と似ています。各部品が独立し、明確なインターフェースを通じて組み合わされることで、システム全体の柔軟性が高まります。
18.3 独立した機能設計
マイクロサービスでは、各サービスが独立して開発・デプロイ・運用できることが理想です。これにより、特定機能の変更が他の機能へ与える影響を小さくできます。
独立した機能設計を実現するには、サービス境界を適切に定義する必要があります。責務が曖昧なまま分割すると、サービス間通信が複雑になり、保守性が下がる可能性があります。
19. コンポジションのメリット
コンポジションのメリットは、柔軟性、拡張性、テスト容易性を高められることです。機能を小さな部品として分け、必要に応じて組み合わせることで、変更に強い設計を作れます。
また、継承階層を深くしなくても多様な振る舞いを表現できます。これは、機能の種類や組み合わせが増えやすいシステムで特に有効です。
19.1 柔軟性向上
コンポジションでは、必要な部品を組み合わせることで振る舞いを構成します。これにより、クラス階層を固定せずに、実行時や設定に応じて機能を変えられます。
柔軟性が高い設計では、仕様変更に対応しやすくなります。新しい要件が発生しても、既存クラスを大きく変更するのではなく、新しい部品を追加・差し替えすることで対応できます。
19.2 拡張性向上
コンポジションは、拡張性を高めます。新しい機能を追加する場合、既存のクラス階層を変更せずに、部品を追加する形にできます。これはOCPにも合っています。
たとえば、新しい通知方法や新しい決済方法を追加するとき、共通インターフェースに従った部品を追加するだけで対応できる設計にできます。これにより、既存処理への影響を抑えられます。
19.3 テスト容易性向上
コンポジションでは、部品ごとにテストしやすくなります。ServiceがRepositoryやNotifierに依存している場合、それらをモックに差し替えてServiceの単体テストを行えます。
継承階層が複雑な場合、親クラスの振る舞いや状態に依存してテストが難しくなることがあります。コンポジションでは依存を明示しやすいため、テスト設計もしやすくなります。
20. コンポジションのデメリット
コンポジションには多くのメリットがありますが、デメリットもあります。部品を分けることでクラスやファイルの数が増え、設計の理解に時間がかかる場合があります。また、依存関係を適切に管理しないと、構造が複雑になることもあります。
そのため、コンポジションは何でも細かく分ければよいというものではありません。責務、変更頻度、再利用性、テスト容易性を考慮しながら、適切な粒度で分離することが重要です。
20.1 実装量の増加
コンポジションを使うと、部品となるクラスやインターフェースが増えることがあります。単純な処理でも、抽象化や委譲を導入すると、コード量が増える場合があります。
小規模な処理や変更可能性が低い処理にまでコンポジションを使うと、過剰設計になることがあります。実装量と得られる柔軟性のバランスを考える必要があります。
20.2 設計難易度の上昇
コンポジションでは、どの責務をどの部品に分けるか、どのインターフェースを用意するか、依存関係をどう管理するかを考える必要があります。継承よりも設計判断が増える場合があります。
特に経験が浅いチームでは、部品の分け方が不統一になり、構造が分かりにくくなることがあります。命名規則や設計方針をチームで共有することが重要です。
20.3 構造の理解が必要
コンポジションを多用した設計では、処理が複数の部品に分散します。そのため、全体の処理フローを理解するには、部品同士の関係を把握する必要があります。
この問題を防ぐには、依存関係を明確にし、適切な名前を付け、ディレクトリ構成を整理することが大切です。コンポジションは強力ですが、構造を分かりやすく保つ工夫が必要です。
21. 継承が適しているケース
Composition over Inheritanceは、継承を完全に否定するものではありません。継承が適しているケースも存在します。特に、親子関係が明確で、階層が安定しており、子クラスが親クラスとして自然に扱える場合、継承は有効です。
重要なのは、継承を使う理由を明確にすることです。単なるコード再利用ではなく、ドメイン上の自然な分類や共通仕様の共有が目的であれば、継承が適切な選択になる場合があります。
21.1 安定した階層構造
継承は、階層構造が安定している場合に向いています。親クラスと子クラスの関係が将来的にも大きく変わらないなら、継承によって自然なモデルを表現できます。
たとえば、抽象的なShapeに対してCircleやRectangleを定義するようなケースでは、関係性が分かりやすく、継承が適している場合があります。ただし、振る舞いが大きく異なる場合は注意が必要です。
21.2 明確な親子関係
継承は、明確なis-a関係がある場合に使うべきです。子クラスが親クラスの一種として自然に説明でき、親クラスの契約を守れる場合、継承は理解しやすい設計になります。
逆に、「同じメソッドを使いたいから」という理由だけで継承するのは危険です。親子関係が不自然な場合は、コンポジションで共通機能を持たせる方が保守しやすくなります。
21.3 共通仕様の共有
継承は、共通仕様を共有する場合にも有効です。抽象クラスや基底クラスに共通インターフェースや基本処理を定義し、子クラスがそれを実装・拡張する設計です。
ただし、親クラスに多くの処理を詰め込みすぎると、子クラスが不要な機能まで引き継ぎます。共通仕様は安定した最小限の内容に留めることが重要です。
22. コンポジションが適しているケース
コンポジションは、機能変更が多い場合、柔軟な拡張が必要な場合、疎結合が求められる場合に適しています。特に、複数の機能を組み合わせてさまざまな振る舞いを作るシステムでは、コンポジションの方が自然です。
Web開発、ゲーム開発、フロントエンド開発、外部API連携、マイクロサービスなどでは、変更や差し替えが頻繁に発生します。こうした領域では、継承よりもコンポジションを優先することで保守性を高められます。
22.1 頻繁な機能変更
機能変更が頻繁に発生する領域では、コンポジションが有効です。継承で設計すると、変更のたびにクラス階層を見直す必要が出ることがあります。一方、コンポジションでは部品を差し替えることで対応しやすくなります。
たとえば、料金計算、通知方法、認証方式、UI表示などは変更されやすい処理です。これらを独立した部品として設計すれば、変更時の影響を小さくできます。
22.2 柔軟な拡張が必要な場合
機能の組み合わせが多い場合も、コンポジションが向いています。複数のオプションや振る舞いを継承で表現すると、組み合わせごとにクラスが増える可能性があります。
コンポジションでは、機能を部品として追加し、必要に応じて組み合わせられます。DecoratorパターンやStrategyパターンは、このような柔軟な拡張に適した設計です。
22.3 疎結合が求められる場合
外部API、データベース、クラウドサービス、通知基盤など、差し替えやすさが求められる依存先では、コンポジションが有効です。インターフェースを通じて依存し、具体実装を外部から注入することで、疎結合な設計にできます。
疎結合な設計では、テストもしやすくなります。実装をモックに差し替えられるため、外部環境に依存しない単体テストを行いやすくなります。
23. 継承からコンポジションへのリファクタリング
既存コードが継承に強く依存している場合でも、段階的にコンポジションへ移行できます。重要なのは、一度にすべてを作り直すのではなく、問題が大きい箇所から少しずつ責務を分離することです。
継承階層が複雑になっている場合、まず親クラスが持っている責務を洗い出します。その中で、子クラスごとに差し替えたい振る舞いや、不要な機能として引き継がれている処理を部品として切り出します。
23.1 設計改善の流れ
リファクタリングでは、最初に現在の継承階層を確認します。どのクラスがどの親クラスに依存しているか、どのメソッドが上書きされているか、どの機能が共通化されているかを把握します。
次に、共通機能と可変機能を分けます。共通仕様として安定しているものは残し、変更されやすい振る舞いはStrategyやComponentとして切り出します。これにより、継承階層を浅くしながら柔軟性を高められます。
23.2 責務の分離
継承からコンポジションへ移行する際は、責務の分離が重要です。親クラスが持っている処理を、移動処理、通知処理、保存処理、計算処理などの責務に分け、それぞれ専用オブジェクトへ切り出します。
責務を分離すると、子クラスが不要な処理を引き継がなくなります。必要な機能だけを持たせられるため、クラスの意味が明確になり、保守しやすくなります。
23.3 段階的な移行方法
段階的に移行するには、まずテストを整備することが重要です。既存の動作を守りながら内部構造を変更するため、テストがあると安全にリファクタリングできます。
次に、親クラスの一部機能を小さな部品に切り出し、子クラスがそれを持つ形へ変更します。すべてを一度に変更するのではなく、変更頻度が高い処理や問題が起きやすい処理から移行すると安全です。
24. 実務でのベストプラクティス
実務では、まずコンポジションを検討し、それでも継承が自然な場合に継承を使うという判断が有効です。継承は強力ですが、変更に弱い構造を作りやすいため、慎重に使う必要があります。
コンポジションを活用する際は、部品の責務を明確にし、依存関係を整理し、過剰な抽象化を避けることが重要です。設計原則は、コードを複雑にするためではなく、変更しやすくするために使うものです。
24.1 まずコンポジションを検討する
新しい設計を考えるときは、最初にコンポジションで実現できないかを検討するのが有効です。必要な機能を部品として分け、組み合わせで表現できるなら、継承よりも柔軟な設計になる可能性があります。
特に、振る舞いが変わりやすい領域ではコンポジションを優先すべきです。決済、通知、認証、外部連携、UI機能、ゲームオブジェクトなどは、機能の差し替えや追加が発生しやすいためです。
24.2 継承は必要最小限にする
継承を使う場合は、必要最小限に留めることが重要です。親クラスには安定した共通仕様だけを持たせ、変化しやすい振る舞いはコンポジションで扱う方が安全です。
また、継承階層を深くしすぎないことも大切です。階層が深くなると、処理の流れや影響範囲を理解しづらくなります。継承は明確なis-a関係がある場合に限定して使うべきです。
24.3 責務ごとに機能を分離する
コンポジションを成功させるには、責務ごとに機能を分離することが重要です。1つの部品が多くの責務を持つと、結局巨大クラスになってしまいます。部品ごとに明確な役割を持たせる必要があります。
たとえば、通知、保存、検証、変換、認証、ログ出力などを別々の部品として設計します。各部品が小さく明確な責務を持てば、再利用しやすく、テストしやすく、変更にも強い設計になります。
おわりに
Composition over Inheritance(継承よりコンポジション)は、継承を安易に使うのではなく、まずオブジェクトの組み合わせによって機能を構成できないかを検討する設計原則です。継承は親子関係を表現する強力な仕組みですが、多用すると強い結合や複雑なクラス階層を生みやすくなります。
コンポジションを活用すると、機能を小さな部品として分離し、必要に応じて組み合わせたり差し替えたりできます。これにより、柔軟性、保守性、拡張性、テスト容易性が向上します。Strategyパターン、Decoratorパターン、Adapterパターンなど、多くのデザインパターンもコンポジションを基盤としています。
ただし、継承が不要になるわけではありません。明確なis-a関係があり、親子関係が安定している場合には、継承が自然な選択になることもあります。重要なのは、コード再利用だけを目的に継承を使わず、関係性や変更可能性を見極めることです。
現代のソフトウェア開発では、仕様変更や機能追加に対応しやすい設計が求められます。継承とコンポジションを適切に使い分け、特に変化しやすい領域ではコンポジションを優先することで、長期的に保守しやすく、拡張しやすいシステムを構築できるでしょう。
EN
JP
KR