
オブジェクト指向プログラミングでは、継承よりも合成(時にはフォワーディングまたはコンポジット再利用を伴う合成)は、継承を必要とせずにコードの再利用を実現しようとする一般的な設計パターンです。2 つの子クラスが共通の親から機能を継承する代わりに、合成は、子クラスが「擬似親」クラスのコピーをフィールドとして含めることで継承をシミュレートします。この擬似親は、2 つのクラスに共通する機能を実装します。そして、子クラスで呼び出されるはずだったメソッドは、代わりに擬似親で呼び出されます。これはメソッドフォワーディングと呼ばれる手法です。[ 2 ]
コンポジションは、継承が利用できない言語、または継承の実装が柔軟性に欠ける、不便、または不十分であると考えられる言語(たとえば、多重継承が欠けている言語)で一般的に使用されます。しかし、継承で簡単に解決できる問題の多くは、コンポジションだけでは解決が困難です。そのため、継承とオブジェクトコンポジションは通常、連携して機能します。これは、書籍『デザインパターン』(1994年)で説明されています。[ 3 ]
C++の例を以下に示します。
import std ;using std :: unique_ptr ; using std :: vector ;class GameObject { public : virtual void update () { // 何もしない}virtual void draw () const { // 何もしない}virtual void collide ( vector < GameObject > objects ) { // 何もしない} };class Visible : public GameObject { private : unique_ptr < Model > model ; public : virtual void draw () const override { // このオブジェクトの位置にモデルを描画するコード} };class Solid : public GameObject { public : virtual void collide ( vector < GameObject > objects ) override { // 他のオブジェクトとの衝突をチェックして反応するコード} };class Movable : public GameObject { public : virtual void update () override { // このオブジェクトの位置を更新するコード} };次に、次のような具体的なクラスがあると仮定します。
Player- それはSolid、MovableそしてVisibleCloud- これはMovableとですがVisible、 ではありませんSolidBuilding- これはSolidとですがVisible、 ではありませんMovableTrap- これは、ですSolidが、もでもVisibleありませんMovable多重継承は、慎重に実装しないとダイヤモンド問題を引き起こす可能性があるため、注意が必要です。この問題の解決策の一つは、必要な組み合わせごとに、、などのクラスを作成することですVisibleAndSolidがVisibleAndMovable、VisibleAndSolidAndMovableこれは大量の重複コードにつながります。C++ では、仮想継承を使用して多重継承のダイヤモンド問題を解決します。
このセクションの C++ の例は、コードの再利用とポリモーフィズムを実現するために、コンポジションとインターフェースを使用する原則を示しています。C++ 言語にはインターフェースを宣言するための専用のキーワードがないため、次の C++ の例では、純粋な抽象基底クラスからの継承を使用しています。ほとんどの場合、これはJava [ 4 ] : 87やC# [ 5 ] : 144などの他の言語で提供されているインターフェースと機能的に同等です。
オブジェクトを描画する手段を提供するVisibilityDelegate、サブクラスとをNotVisible持つ抽象クラスを導入します。Visible
class VisibilityDelegate { public : virtual void draw () const = 0 ; };class NotVisible : public VisibilityDelegate { public : virtual void draw () const override { // 何もしない} };class Visible : public VisibilityDelegate { public : virtual void draw () const override { // このオブジェクトの位置にモデルを描画するコード} };オブジェクトを移動するための手段を提供するUpdateDelegate、サブクラスとをNotMovable持つ抽象クラスを導入します。Movable
class UpdateDelegate { public : virtual void update () = 0 ; };class NotMovable : public UpdateDelegate { public : virtual void update () override { // 何もしない} };class Movable : public UpdateDelegate { public : virtual void update () override { // このオブジェクトの位置を更新するコード} };オブジェクトとの衝突判定手段を提供するCollisionDelegate、サブクラスとNotSolidを持つ抽象クラスを導入します。Solid
import std ;std :: vectorを使用します。class CollisionDelegate { public : virtual void collide ( vector < GameObject > objects ) = 0 ; };class NotSolid : public CollisionDelegate { public : virtual void collide ( vector < GameObject > objects ) override { // 何もしない} };class Solid : public CollisionDelegate { public : virtual void collide ( vector < GameObject > objects ) override { // 他のオブジェクトとの衝突をチェックして反応するコード} };GameObject最後に、可視性( を使用VisibilityDelegate)、移動性( を使用UpdateDelegate)、および堅牢性( を使用)を制御するメンバーを持つクラスを導入しますCollisionDelegate。このクラスには、メンバーに処理を委譲するメソッドがあります。たとえば、update()は のメソッドを単純に呼び出しますUpdateDelegate。
import std ;using std :: unique_ptr ; using std :: vector ;class GameObject { private : unique_ptr <VisibilityDelegate> visibilityDelegate ; unique_ptr <UpdateDelegate> updateDelegate ; unique_ptr <CollisionDelegate> collisionDelegate ; public : GameObject ( VisibilityDelegate * v , UpdateDelegate * u , CollisionDelegate * c ) : visibilityDelegate { std :: make_unique <VisibilityDelegate> ( v ) } , updateDelegate { std :: make_unique <UpdateDelegate> ( u ) } , collisionDelegate { std :: make_unique <CollisionDelegate> ( c ) } { }void update () { updateDelegate- > update (); }const void draw () { visibilityDelegate- > draw (); }void collide ( vector < GameObject > objects ) { collisionDelegate -> collide ( objects ); } };すると、具体的なクラスは次のようになります。
class Player : public GameObject { public : Player () : GameObject ( new Visible (), new Movable (), new Solid ()) {}// ... };class Smoke : public GameObject { public : Smoke () : GameObject ( new Visible (), new Movable (), new NotSolid ()) {}// ... };継承よりも構成を優先することは、設計の柔軟性を高める設計原則です。さまざまなコンポーネントからビジネスドメインクラスを構築する方が、それらの共通点を見つけて系統図を作成するよりも自然です。たとえば、アクセルペダルとステアリングホイールには共通点がほとんどありませんが、どちらも自動車の重要なコンポーネントです。それらが何ができるか、そしてどのように使用すれば自動車に役立つかは簡単に定義できます。構成は、ファミリーメンバーの癖の影響を受けにくいため、長期的にはより安定したビジネスドメインを提供します。言い換えれば、オブジェクトができること ( has-a ) を構成する方が、それが何であるか ( is-a ) を拡張するよりも良いのです。[ 1 ]
初期設計は、継承によってビジネスドメインクラス間で動作を分散させる階層的な関係を作成するのではなく、システムオブジェクトの動作を個別のインターフェースで識別することで簡素化されます。このアプローチは、継承モデルではビジネスドメインクラスの完全な再構築が必要となるような将来の要件変更にも容易に対応できます。さらに、複数の世代のクラスを含む継承ベースのモデルに対する比較的軽微な変更に伴う問題も回避できます。コンポジション関係は実行時に変更できるため柔軟性が高く、一方、サブタイピング関係は静的であり、多くの言語で再コンパイルが必要です。
継承の代わりに合成を使用する際の一般的な欠点の1つは、個々のコンポーネントによって提供されるメソッドが、転送メソッドであっても派生型で実装する必要がある場合があることです(これ はほとんどのプログラミング言語で当てはまりますが、すべてではありません。§欠点の回避 を参照してください)。対照的に、継承では、基底クラスのすべてのメソッドを派生クラス内で再実装する必要はありません。むしろ、派生クラスは、基底クラスのメソッドとは異なる動作をするメソッドのみを実装(オーバーライド)する必要があります。基底クラスにデフォルトの動作を提供するメソッドが多数含まれており、派生クラス内でオーバーライドする必要があるのがそのうちのごく一部である場合、これによりプログラミングの手間を大幅に削減できます。
したがって、「継承よりも構成を優先する」というパターンは、文字通りのマントラや法則として解釈されるべきではなく、適切な場面で選択的に適用できる提案として捉えるべきである。
例えば、以下の C# コードでは、基底クラスの変数とメソッドは、派生サブクラスEmployeeに継承されます。各派生サブクラスで実装(特殊化)する必要があるのは、メソッドのみです。その他のメソッドは基底クラス自体によって実装され、すべての派生サブクラスで共有されます。そのため、サブクラスの定義で再実装(オーバーライド)したり、言及したりする必要はありません。HourlyEmployeeSalariedEmployeePay()
![]()
// 基底クラスpublic abstract class Employee { // プロパティprotected string Name { get ; set ; } protected int ID { get ; set ; } protected decimal PayRate { get ; set ; } protected int HoursWorked { get ; }// 現在の給与期間の給与を取得するpublic abstract decimal Pay (); }// 派生サブクラスpublic class HourlyEmployee : Employee { // 現在の給与期間の給与を取得しますpublic override decimal Pay () { // 勤務時間は時間単位ですreturn HoursWorked * PayRate ; } }// 派生サブクラスpublic class SalariedEmployee : Employee { // 現在の給与期間の給与を取得しますpublic override decimal Pay () { // 給与レートは時給ではなく年俸ですreturn HoursWorked * PayRate / 2087 ; } }この欠点は、特性、ミックスイン、(型)埋め込み、またはプロトコル拡張を使用することで回避できます。
一部の言語では、この問題を軽減するための具体的な手段が提供されています。
virtualでは、メソッド(仮想関数を指定するためのメソッド)にデフォルトの実装を持たせることができます。[ 11 ]@Delegateは、委譲されたフィールドからすべてのメソッドの名前と型をコピーして維持する代わりに、フィールドの注釈を使用して委譲をサポートしています。 [ 15 ]handlesメソッド転送を容易にする特性を提供します。 [ 21 ]2013年に実施された、93個のオープンソースJavaプログラム(規模は様々)を対象とした調査では、以下のことが判明した。
継承をコンポジションに置き換える機会はそれほど大きくはないものの、その機会は大きい(継承の使用例の中央値で2%が内部再利用のみであり、さらに22%が外部または内部再利用のみである)。我々の結果は、継承の濫用について懸念する必要はないことを示唆している(少なくともオープンソースのJavaソフトウェアにおいては)が、コンポジションと継承の使用に関する疑問を浮き彫りにしている。コンポジションが使用できる場合に継承を使用することに伴うコストが大きいのであれば、我々の結果は懸念すべき理由があることを示唆している。
— Tempero et al.、「Java におけるプログラマーによる継承の活用」[ 24 ]