
オブジェクト指向プログラミング(OOP)における継承よりも合成(または合成再利用の原則) とは、クラスは基底クラスまたは親クラスからの継承よりも、合成(目的の機能を実装する他のクラスのインスタンスを含めることによる)による多態的な動作とコードの再利用を優先すべきであるという原則です。 [2]理想的には、すべての再利用は既存のコンポーネントを組み立てることによって達成できますが、実際には新しいコンポーネントを作成するために継承が必要になることがよくあります。したがって、継承とオブジェクト合成は通常、 Design Patterns (1994)で説明されているように、連携して機能します。[3]
基礎
継承よりも構成を優先する実装は、通常、システムが実行する必要がある動作を表すさまざまなインターフェイスの作成から始まります。インターフェイスは、ポリモーフィックな動作を可能にします。識別されたインターフェイスを実装するクラスは、必要に応じて構築され、ビジネス ドメイン クラスに追加されます。このようにして、システムの動作は継承なしで実現されます。
実際、ビジネス ドメイン クラスはすべて継承のない基本クラスである可能性があります。システム動作の代替実装は、必要な動作インターフェイスを実装する別のクラスを提供することで実現されます。インターフェイスへの参照を含むクラスは、インターフェイスの実装をサポートできます。この選択は、実行時まで延期できます。
例
継承
C++の例を次に示します。
クラスObject { public : virtual void update () { // 何もしない}
virtual void draw () { // 何もしない}
virtual void collide ( Object objects []) { // no-op } };
クラスVisible : public Object { Model * model ;
public :
virtual void draw () override { // このオブジェクトの位置にモデルを描画するコード} };
class Solid : public Object { public : virtual void collide ( Object objects []) override { // 他のオブジェクトとの衝突をチェックし、それに反応するコード} };
class Movable : public Object { public : virtual void update () override { // このオブジェクトの位置を更新するコード} };
次に、次のような具体的なクラスもあるとします。
- クラス-で
PlayerありSolid、MovableVisible - クラス
Cloud- でありMovable、VisibleではないSolid - クラス
Building- でありSolid、VisibleではないMovable - クラス
Trap- ですがSolid、VisibleMovable
多重継承は、慎重に実装しないとダイヤモンド問題VisibleAndSolidを引き起こす可能性があるため、危険であることに注意してください。この問題の解決方法の 1 つは、必要な組み合わせごとにVisibleAndMovable、、、などのクラスを作成することですが、これにより大量の繰り返しコードが発生します。C++ では、仮想継承をVisibleAndSolidAndMovable使用して多重継承のダイヤモンド問題を解決します。
構成とインターフェース
このセクションの C++ の例は、コードの再利用とポリモーフィズムを実現するためにコンポジションとインターフェースを使用する原則を示しています。C++ 言語にはインターフェースを宣言するための専用キーワードがないため、次の C++ の例では純粋な抽象基本クラスからの継承を使用しています。ほとんどの目的において、これは Java [4] : 87 や C#などの他の言語で提供されるインターフェースと機能的に同等です。[5] : 144
オブジェクトを描画する手段を提供する、
VisibilityDelegateサブクラスNotVisibleおよびを持つ という名前の抽象クラスを導入します。Visible
クラスVisibilityDelegate { public : virtual void draw () = 0 ; };
クラスNotVisible : public VisibilityDelegate { public : virtual void draw () override { // no-op } };
class Visible : public VisibilityDelegate { public : virtual void draw () override { // このオブジェクトの位置にモデルを描画するコード} };
オブジェクトを移動する手段を提供する、
UpdateDelegateサブクラスNotMovableおよびを持つ という名前の抽象クラスを導入します。Movable
クラスUpdateDelegate { public : virtual void update () = 0 ; };
クラスNotMovable : public UpdateDelegate { public : virtual void update () override { // no-op } };
class Movable : public UpdateDelegate { public : virtual void update () override { // このオブジェクトの位置を更新するコード} };
オブジェクトと衝突する手段を提供する、
CollisionDelegateサブクラスNotSolidおよびを持つ という名前の抽象クラスを導入します。Solid
クラスCollisionDelegate { public : virtual void collide ( Object objects []) = 0 ; };
クラスNotSolid : public CollisionDelegate { public : virtual void collide ( Object objects []) override { // no-op } };
class Solid : public CollisionDelegate { public : virtual void collide ( Object objects []) override { // 他のオブジェクトとの衝突をチェックし、それに反応するコード} };
Object最後に、メンバーを持つという名前のクラスを導入して、その可視性 ( を使用VisibilityDelegate)、移動可能性 ( を使用UpdateDelegate)、および堅牢性 ( を使用CollisionDelegate) を制御します。このクラスには、そのメンバーに委譲するメソッドがあります。たとえば、update()の メソッドを単に呼び出しますUpdateDelegate。
クラスObject { VisibilityDelegate * _v ; UpdateDelegate * _u ; CollisionDelegate * _c ;
パブリック:
Object ( VisibilityDelegate * v 、UpdateDelegate * u 、CollisionDelegate * c ) : _v ( v ) 、_u ( u ) 、_c ( c ) {}
void更新() { _u ->更新(); }
void描画() { _v ->描画(); }
void衝突( Objectオブジェクト[]) { _c ->衝突(オブジェクト); } };
すると、具体的なクラスは次のようになります。
クラスPlayer : public Object { public : Player () : Object (新しいVisible ()、新しいMovable ()、新しいSolid ()) {}
// ...
};
クラスSmoke :パブリックオブジェクト{パブリック: Smoke () :オブジェクト(新しいVisible ()、新しいMovable ()、新しいNotSolid ()) {}
// ...
};
利点
継承よりも構成を優先することは、設計に高い柔軟性を与える設計原則です。さまざまなコンポーネントからビジネスドメインクラスを構築する方が、それらの共通点を見つけて家系図を作成するよりも自然です。たとえば、アクセルペダルとハンドルには共通の特徴がほとんどありませんが、どちらも自動車の重要なコンポーネントです。それらが何ができるか、そしてどのように車に役立つかを簡単に定義できます。また、構成は、ファミリーメンバーの癖の影響を受けにくいため、長期的にはより安定したビジネスドメインを提供します。言い換えると、オブジェクトができること(has-a)を構成する方が、それが何であるかを拡張する(is-a)よりも優れています。[1]
階層関係を作成して継承を介してビジネス ドメイン クラス間で動作を分散する代わりに、システム オブジェクトの動作を個別のインターフェイスで識別することで、初期設計が簡素化されます。このアプローチにより、継承モデルでビジネス ドメイン クラスを完全に再構築する必要のある将来の要件変更に、より簡単に対応できます。さらに、複数の世代のクラスを含む継承ベースのモデルへの比較的小さな変更に伴う問題を回避できます。コンポジション関係は実行時に変更できるため、より柔軟ですが、サブタイプ関係は静的であり、多くの言語で再コンパイルが必要です。
Go [6]やRust [7]などの言語では、型合成のみが使用されます。
欠点
継承の代わりに合成を使用する場合の一般的な欠点の 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 ; } }
欠点を避ける
この欠点は、特性、ミックスイン、(型)埋め込み、またはプロトコル拡張を使用することで回避できます。
一部の言語では、これを軽減するための具体的な手段が提供されています。
- C#はバージョン8.0以降、インターフェースメンバーに本体を定義できるデフォルトのインターフェースメソッドを提供しています。[8] [5] : 28–29 [9] : 38 [10] : 466–468
- Dは、型内の明示的な「alias this」宣言を提供し、別の型のすべてのメソッドとメンバーをその中に転送することができます。[11]
- Dart は、共有可能なデフォルト実装を備えたミックスインを提供します。
- Goの型埋め込みは転送メソッドの必要性を回避します。[12]
- Javaはバージョン8以降でデフォルトのインターフェースメソッドを提供しています。[4] : 104 Project Lombok [13]は
@Delegate、委任されたフィールドからすべてのメソッドの名前と型をコピーして維持する代わりに、フィールドのアノテーションを使用して委任をサポートしています。 [14] - Juliaマクロは転送メソッドを生成するために使用できます。Lazy.jl [15]やTypedDelegation.jl [16] [17]などの実装がいくつか存在します。
- Kotlinは言語構文に委任パターンを組み込んでいます。[18]
- PHPはPHP 5.4以降で特性をサポートしています。 [19]
- Rakuは
handlesメソッド転送を容易にする特性を提供する。 [20] - Rust はデフォルトの実装を備えた特性を提供します。
- Scala(バージョン3以降)は、オブジェクトの選択されたメンバーのエイリアスを定義するための「export」句を提供します。[21]
- Swiftの拡張機能は、個々の型の実装内ではなく、プロトコル自体にプロトコルのデフォルト実装を定義するために使用できます。[22]
実証研究
2013 年に行われた 93 個のオープン ソース Java プログラム (さまざまなサイズ) の調査では、次のことがわかりました。
継承を合成に置き換える機会はそれほど多くありませんが、その機会は大きいです ([継承の] 使用の中央値 2% は内部再利用のみで、さらに 22% は外部または内部再利用のみです)。私たちの結果は、継承の乱用について懸念する必要はない (少なくともオープンソースの Java ソフトウェアでは) ことを示していますが、合成と継承の使用に関する疑問を浮き彫りにしています。合成を使用できる場合に継承を使用することで大きなコストが発生する場合、私たちの結果は懸念すべき理由があることを示唆しています。
— Tempero他「Javaにおける継承でプログラマーが行うこと」[23]
参照
参考文献
- ^ ab フリーマン、エリック、ロブソン、エリザベス、シエラ、キャシー、ベイツ、バート (2004)。Head First デザインパターン。オライリー。p. 23。ISBN 978-0-596-00712-6。
- ^ Knoernschild, Kirk (2002). Java 設計 - オブジェクト、UML、プロセス: 1.1.5 複合再利用原則 (CRP). Addison-Wesley Inc. ISBN 9780201750447. 2012年5月29日閲覧。
- ^ ガンマ、エリック、ヘルム、リチャード、ジョンソン、ラルフ、ブリサイドス、ジョン(1994)。デザインパターン: 再利用可能なオブジェクト指向ソフトウェアの要素。Addison -Wesley。p . 20。ISBN 0-201-63361-2. OCLC 31171684.
- ^ ab Bloch, Joshua (2018). 「Effective Java: プログラミング言語ガイド」(第3版). Addison-Wesley. ISBN 978-0134685991。
- ^ ab Price, Mark J. (2022). C# 8.0 および .NET Core 3.0 – 最新のクロスプラットフォーム開発: Visual Studio Code を使用して C#、.NET Core、Entity Framework Core、ASP.NET Core、ML.NET でアプリケーションを構築する. Packt. ISBN 978-1-098-12195-2。
- ^ Pike, Rob (2012-06-25). 「Less is exponentially more」。2016年10月1日閲覧。
- ^ 「オブジェクト指向言語の特徴 - Rustプログラミング言語」. doc.rust-lang.org . 2022年10月10日閲覧。
- ^ 「C# 8.0 の新機能」。Microsoft Docs。Microsoft。2019年 2 月 20 日閲覧。
- ^ Skeet, Jon (2019年3月23日). C# in Depth . Manning. ISBN 978-1617294532。
- ^ アルバハリ、ジョセフ (2022). C# 10 の概要。オライリー。ISBN 978-1-098-12195-2。
- ^ 「Alias This」。D言語リファレンス。2019年 6 月 15 日閲覧。
- ^ 「(Type) Embedding」。Goプログラミング言語ドキュメント。2019年5月10日閲覧。
- ^ https://projectlombok.org [裸のURL ]
- ^ "@Delegate". Project Lombok . 2018年7月11日閲覧。
- ^ 「MikeInnes/Lazy.jl」。GitHub。
- ^ 「JeffreySarnoff/TypedDelegation.jl」。GitHub。
- ^ 「メソッド転送マクロ」。JuliaLang。2019年4月20日。 2022年8月18日閲覧。
- ^ 「委任されたプロパティ」。Kotlinリファレンス。JetBrains。2018年 7 月 11 日閲覧。
- ^ 「PHP: Traits」. www.php.net . 2023年2月23日閲覧。
- ^ 「型システム」. docs.raku.org . 2022年8月18日閲覧。
- ^ 「エクスポート句」。Scalaドキュメント。2021年 10 月 6 日閲覧。
- ^ 「プロトコル」。Swiftプログラミング言語。Apple Inc. 2018年7月11日閲覧。
- ^ Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). 「プログラマーが Java で継承を使って行うこと」(PDF) . ECOOP 2013 – オブジェクト指向プログラミング. ECOOP 2013–オブジェクト指向プログラミング。コンピューターサイエンスの講義ノート。第 7920 巻。pp. 577–601。doi : 10.1007 / 978-3-642-39038-8_24。ISBN 978-3-642-39038-8。
