なぜ継承よりも構成を好むのですか?各アプローチにはどのようなトレードオフがありますか?いつ、構成よりも継承を選択する必要がありますか?
32 に答える
順応性が高く、後で変更しやすいため、継承よりも構成を優先しますが、常に構成するアプローチは使用しないでください。コンポジションを使用すると、依存性注入/セッターを使用してその場で動作を簡単に変更できます。ほとんどの言語では複数の型から派生させることができないため、継承はより厳格です。したがって、TypeA から派生すると、ガチョウは多かれ少なかれ調理されます。
上記の私の酸テストは次のとおりです。
TypeB は、TypeA が期待される場所で TypeB を使用できるように、TypeA の完全なインターフェイス (すべてのパブリック メソッド) を公開したいですか? 継承を示します。
- たとえば、セスナ複葉機は、飛行機の完全なインターフェースを公開します。そのため、Airplane から派生するのに適しています。
TypeB は、TypeA によって公開された動作の一部/一部のみを必要としますか? 構成の必要性を示します。
- たとえば、鳥は飛行機の飛行動作のみを必要とする場合があります。この場合、インターフェイス/クラス/両方として抽出し、両方のクラスのメンバーにすることが理にかなっています。
更新:私の答えに戻ってきたところですが、「このタイプから継承する必要がありますか?」のテストとして、 Barbara Liskov のLiskov Substitution Principleについて具体的に言及していないため、不完全なようです。
封じ込めは関係があると考えてください。車は「エンジンを持っている」、人は「名前を持っている」など。
継承は関係であると考えてください。車は「乗り物」、人は「哺乳類」などです。
私はこのアプローチを信用していません。スティーブマコネルによるコードコンプリートの第2版、セクション6.3から直接引用しました。
違いを理解すれば、説明しやすくなります。
手続き型コード
この例は、クラスを使用しないPHPです(特にPHP5より前)。すべてのロジックは、一連の関数にエンコードされています。ヘルパー関数などを含む他のファイルを含め、関数内でデータを渡すことによってビジネスロジックを実行できます。これは、アプリケーションの成長に伴って管理するのが非常に難しい場合があります。PHP5は、よりオブジェクト指向の設計を提供することにより、これを改善しようとします。
継承
これにより、クラスの使用が促進されます。継承は、オブジェクト指向設計の3つの信条(継承、ポリモーフィズム、カプセル化)の1つです。
class Person {
String Title;
String Name;
Int Age
}
class Employee : Person {
Int Salary;
String Title;
}
これは仕事での継承です。従業員は「人」であるか、人から継承します。すべての継承関係は「is-a」関係です。Employeeは、PersonからTitleプロパティもシャドウします。つまり、Employee.Titleは、PersonではなくEmployeeのTitleを返します。
構成
継承よりも構成が優先されます。簡単に言えば、次のようになります。
class Person {
String Title;
String Name;
Int Age;
public Person(String title, String name, String age) {
this.Title = title;
this.Name = name;
this.Age = age;
}
}
class Employee {
Int Salary;
private Person person;
public Employee(Person p, Int salary) {
this.person = p;
this.Salary = salary;
}
}
Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);
構成は通常、「持つ」または「使用する」関係です。ここで、EmployeeクラスにはPersonがあります。Personから継承するのではなく、Personオブジェクトを渡します。これが、Personを「持っている」理由です。
継承をめぐる構成
ここで、Managerタイプを作成して、最終的に次のようになるとします。
class Manager : Person, Employee {
...
}
この例は正常に機能しますが、PersonとEmployeeの両方が宣言した場合はどうなりTitle
ますか?Manager.Titleは「ManagerofOperations」または「Mr.」を返す必要がありますか?構成の下では、このあいまいさはより適切に処理されます。
Class Manager {
public string Title;
public Manager(Person p, Employee e)
{
this.Title = e.Title;
}
}
Managerオブジェクトは、EmployeeとPersonとして構成されます。タイトルの動作は従業員から取得されます。この明示的な構成により、特にあいまいさがなくなり、発生するバグが少なくなります。
継承によって提供されるすべての否定できない利点とともに、その欠点のいくつかを次に示します。
継承の欠点:
- 実行時にスーパー クラスから継承された実装を変更することはできません (継承はコンパイル時に定義されるため)。
- 継承は、サブクラスをその親クラスの実装の詳細に公開します。これが、継承がカプセル化を破るとよく言われる理由です (ある意味では、実装ではなくインターフェイスのみに集中する必要があるため、サブクラスによる再利用は常に優先されるとは限りません)。
- 継承によって提供される密結合により、サブクラスの実装はスーパークラスの実装と密接に結びついており、親の実装が変更されるとサブクラスも強制的に変更されます。
- サブクラス化による過度の再利用は、継承スタックを非常に深くし、非常に混乱させる可能性があります。
一方、オブジェクト構成は、他のオブジェクトへの参照を取得するオブジェクトを通じて実行時に定義されます。このような場合、これらのオブジェクトはお互いの保護されたデータに到達することができず (カプセル化の中断はありません)、お互いのインターフェースを尊重するように強制されます。この場合も、実装の依存関係は継承の場合よりもはるかに少なくなります。
継承よりもコンポジションを優先するもう1つの非常に実用的な理由は、ドメインモデルと関係があり、それをリレーショナルデータベースにマッピングする必要があります。継承をSQLモデルにマッピングするのは非常に困難です(常に使用されるとは限らない列の作成、ビューの使用など、あらゆる種類のハッキーな回避策が発生します)。一部のORMLはこれに対処しようとしますが、常にすぐに複雑になります。構成は、2つのテーブル間の外部キー関係を介して簡単にモデル化できますが、継承ははるかに困難です。
一言で言えば、「継承よりも構成を好む」には同意しますが、私にとっては「コカ・コーラよりもジャガイモを好む」ように聞こえることがよくあります。継承する場所と合成する場所があります。違いを理解する必要があります。そうすれば、この質問は消えます。私にとってそれが本当に意味することは、「継承を使用する場合、もう一度考えてみてください。合成が必要になる可能性があります」ということです。
食べたいときはコカ・コーラよりもポテトを、飲みたいときはポテトよりもコカ・コーラを選ぶべきです。
サブクラスを作成するということは、スーパークラスのメソッドを呼び出す便利な方法以上のものを意味するはずです。サブクラスが構造的にも機能的にも「スーパークラス」であり、スーパークラスとして使用でき、それを使用する場合は、継承を使用する必要があります。そうでない場合は、継承ではなく、別のものです。コンポジションとは、オブジェクトが別のオブジェクトで構成されているか、オブジェクトと何らかの関係がある場合です。
したがって、私にとっては、誰かが継承または構成が必要かどうかを知らない場合、本当の問題は、彼が飲みたいか食べたいかがわからないことです。問題のドメインについてもっと考え、理解を深めましょう。
継承は、特にプロシージャルランドから来ると非常に魅力的であり、一見エレガントに見えることがよくあります。つまり、この 1 つの機能を他のクラスに追加するだけです。さて、問題の1つは、
継承は、おそらく最悪の結合形態です
基本クラスは、保護されたメンバーの形式で実装の詳細をサブクラスに公開することにより、カプセル化を破ります。これにより、システムが硬直して壊れやすくなります。しかし、より悲劇的な欠陥は、新しいサブクラスが継承チェーンのすべての荷物と意見を持ち込むことです。
記事Inheritance is Evil: The Epic Fail of the DataAnnotationsModelBinderでは、C# でのこの例について説明しています。コンポジションを使用する必要がある場合の継承の使用と、それをリファクタリングする方法を示します。
ここで満足のいく答えが見つからなかったので、新しいものを書きました。
「継承より合成を好む」理由を理解するには、まず、この短縮されたイディオムで省略されている前提を取り戻す必要があります。
継承には、サブタイピングとサブクラス化という 2 つの利点があります。
サブタイピングとは、型 (インターフェース) シグネチャー、つまり一連の API に準拠することを意味し、シグネチャーの一部をオーバーライドしてサブタイピング ポリモーフィズムを実現できます。
サブクラス化とは、メソッド実装の暗黙的な再利用を意味します。
この 2 つの利点には、継承を行うための 2 つの異なる目的があります。サブタイプ指向とコード再利用指向です。
コードの再利用が唯一の目的である場合、サブクラス化は彼が必要とする以上のものを与える可能性があります。つまり、親クラスのいくつかの public メソッドは、子クラスにとってあまり意味がありません。この場合、継承よりも構成を優先するのではなく、構成が要求されます。これは、「is-a」対「has-a」の概念の由来でもあります。
したがって、サブタイプ化が意図されている場合、つまり後でポリモーフィックな方法で新しいクラスを使用する場合にのみ、継承または合成の選択の問題に直面します。これは、議論中の短縮イディオムでは省略される仮定です。
サブタイプとは、型シグネチャに準拠することです。これは、コンポジションが常にその型の API を少なからず公開する必要があることを意味します。ここでトレードオフが発生します。
継承は、オーバーライドされていない場合、単純なコードの再利用を提供しますが、構成は、委任の単純な仕事であっても、すべての API を再コーディングする必要があります。
継承は、内部ポリモーフィック サイト を介して単純なオープン再帰を提供します。つまり、パブリックまたはプライベートのいずれかの別のメンバー関数で
this
オーバーライド メソッド (または型) を呼び出します (推奨されませんが)。オープン再帰は、合成を介してシミュレートできますが、余分な労力が必要であり、常に実行できるとは限りません(?)。重複した質問に対するこの回答は、似たようなことを話します。継承により、保護されたメンバーが公開されます。これにより、親クラスのカプセル化が中断され、サブクラスで使用された場合、子とその親の間に別の依存関係が導入されます。
コンポジションには制御の反転という利点があり、デコレータ パターンとプロキシ パターンで示されているように、その依存関係を動的に注入できます。
コンポジションには、コンビネータ指向プログラミングの利点があります。つまり、コンポジット パターンのような方法で動作します。
コンポジションは、インターフェイスへのプログラミングの直後に行われます。
コンポジションには、多重継承が容易であるという利点があります。
上記のトレードオフを念頭に置いて、継承よりも構成を優先します。ただし、密接に関連するクラスの場合、つまり、暗黙的なコードの再利用が実際にメリットをもたらす場合、またはオープン再帰の魔法の力が必要な場合は、継承が選択されます。
コンポジションはいつ使用できますか?
いつでもコンポジションを使用できます。場合によっては、継承も可能であり、より強力で直感的な API につながる可能性がありますが、構成は常にオプションです。
いつ継承を使用できますか?
Bar
「バーが foo である」場合、クラスはクラスを継承できるとよく言われますFoo
。残念ながら、このテストだけでは信頼できません。代わりに以下を使用してください。
- バーは foo であり、かつ
- bars は、foos ができるすべてのことを実行できます。
最初のテストでは、(= 共有プロパティ) のすべてのgetterが意味を持つFoo
ことを確認します。Bar
Foo
Bar
例: 犬/動物
犬は動物であり、犬は動物ができることはすべて行うことができます (呼吸、移動など)。したがって、クラスはクラスを継承Dog
できAnimal
ます。
反例: 円/楕円
円は楕円ですが、円は楕円にできることすべてを行うことはできません。たとえば、円は伸縮できませんが、楕円は伸縮できます。したがって、クラスはクラスを継承Circle
できませんEllipse
。
これはCircle-Ellipse problemと呼ばれ、実際には問題ではありませんが、「バーは foo である」ということ自体は信頼できるテストではないことを示しています。特に、この例は、派生クラスが基本クラスの機能を拡張する必要があり、決して制限しないことを強調しています。そうしないと、基本クラスをポリモーフィックに使用できません。テスト「bars can do everything that foos can do」を追加すると、ポリモーフィックな使用が可能になり、Liskov Substitution Principleと同等になります。
基本クラスへのポインターまたは参照を使用する関数は、派生クラスのオブジェクトを知らなくても使用できる必要があります。
いつ継承を使用する必要がありますか?
継承を使用できるからといって、継承を使用する必要があるわけではありません。コンポジションの使用は常にオプションです。継承は、暗黙的なコードの再利用と動的なディスパッチを可能にする強力なツールですが、いくつかの欠点もあるため、合成が好まれることがよくあります。継承と構成の間のトレードオフは明らかではありません。私の意見では、lcn's answerで最もよく説明されています。
経験則として、ポリモーフィックな使用が非常に一般的であると予想される場合は、構成よりも継承を選択する傾向があります。その場合、動的ディスパッチの力により、はるかに読みやすく洗練された API を実現できます。たとえばWidget
、GUI フレームワークでポリモーフィック クラスを使用したり、Node
XML ライブラリでポリモーフィック クラスを使用したりすると、純粋にコンポジションに基づくソリューションよりもはるかに読みやすく、直感的に使用できる API を使用できます。
個人的には、常に継承よりも構成を好むことを学びました。継承で解決でき、構成で解決できないプログラム上の問題はありません。ただし、場合によっては、Interfaces(Java) または Protocols(Obj-C) を使用する必要がある場合があります。C++ はそのようなことを認識しないため、抽象基底クラスを使用する必要があります。つまり、C++ で継承を完全に取り除くことはできません。
多くの場合、構成はより論理的であり、より優れた抽象化、より優れたカプセル化、より優れたコードの再利用 (特に非常に大規模なプロジェクトで) を提供し、コードのどこかで孤立した変更を行ったという理由だけで、離れた場所で何かを壊す可能性が低くなります。また、「単一の責任の原則」を支持することも容易になります。これは、「クラスが変更される理由は 1 つ以上であってはならない」としばしば要約されます。これは、すべてのクラスが特定の目的のために存在し、それが必要であることを意味します。その目的に直接関連するメソッドのみを持っています。また、非常に浅い継承ツリーを使用すると、プロジェクトが非常に大きくなり始めた場合でも、概要を維持することがはるかに簡単になります。多くの人は、継承は私たちの現実の世界を表していると考えていますかなり良いですが、それは真実ではありません。現実の世界では、継承よりもはるかに合成が使用されます。手に持つことができるほとんどすべての現実世界のオブジェクトは、他の小さな現実世界のオブジェクトで構成されています.
ただし、構成の欠点があります。継承を完全にスキップして構成のみに焦点を当てると、継承を使用していた場合には必要のなかった余分なコード行をいくつか書かなければならないことがよくあることに気付くでしょう。また、同じことを繰り返さなければならないこともあり、これはDRY の原則に違反しています。(DRY = 繰り返さないでください)。また、コンポジションには委任が必要になることが多く、メソッドは別のオブジェクトの別のメソッドを呼び出すだけで、この呼び出しを囲む他のコードはありません。このような「2 重メソッド呼び出し」 (3 重または 4 重メソッド呼び出し、さらにはそれ以上のメソッド呼び出しに容易に拡張される可能性があります) は、親のメソッドを単純に継承する継承よりもはるかにパフォーマンスが低下します。継承されたメソッドの呼び出しは、継承されていないメソッドの呼び出しと同じくらい高速である場合もあれば、わずかに遅くなる場合もありますが、通常は 2 つの連続したメソッド呼び出しよりも高速です。
ほとんどの OO 言語では多重継承が許可されていないことに気付いたかもしれません。多重継承によって実際に何かが得られるケースがいくつかありますが、それらはルールというよりはむしろ例外です。「多重継承はこの問題を解決するための非常にクールな機能だろう」と考える状況に遭遇したときはいつでも、通常、継承を完全に再考する必要がある時点にいます。 、構成に基づくソリューションは、通常、はるかにエレガントで柔軟で、将来性のあることが判明します。
継承は実に優れた機能ですが、ここ数年は使いすぎているのではないかと思います。人々は継承を、それが実際に釘、ネジ、またはまったく異なるものであるかどうかに関係なく、すべてを釘付けにすることができる 1 つのハンマーとして扱いました。
私の一般的な経験則:継承を使用する前に、構成がより理にかなっているかどうかを検討してください。
理由:サブクラス化とは、通常、より複雑で接続性が高いことを意味します。つまり、間違いを犯さずに変更、維持、拡張することが難しくなります。
SunのTimBoudreauからのはるかに完全で具体的な回答:
私が見ているように、継承の使用に関する一般的な問題は次のとおりです。
- 無実の行為は予期しない結果をもたらす可能性があります-これの典型的な例は、サブクラスのインスタンスフィールドが初期化される前に、スーパークラスコンストラクターからオーバーライド可能なメソッドを呼び出すことです。完璧な世界では、誰もそれをしません。これは完璧な世界ではありません。
- これは、サブクラッサーがメソッド呼び出しの順序などについて仮定するというひねくれた誘惑を提供します。スーパークラスが時間の経過とともに進化する可能性がある場合、そのような仮定は安定しない傾向があります。私のトースターとコーヒーポットの例えも参照してください。
- クラスは重くなります-スーパークラスがコンストラクターでどのような作業を行っているか、またはスーパークラスが使用するメモリの量を必ずしも把握しているとは限りません。したがって、無実の軽量オブジェクトを構築すると、思ったよりもはるかにコストがかかる可能性があります。これは、スーパークラスが進化した場合、時間の経過とともに変化する可能性があります。
- それはサブクラスの爆発を助長します。クラスローディングには時間がかかり、クラスが増えるとメモリがかかります。これは、NetBeansの規模のアプリを扱うまでは問題にならないかもしれませんが、メニューの最初の表示が大量のクラスの読み込みをトリガーしたため、メニューが遅いなどの実際の問題がありました。より宣言的な構文やその他の手法に移行することでこれを修正しましたが、修正にも時間がかかりました。
- 後で変更するのが難しくなります-クラスを公開した場合、スーパークラスを交換するとサブクラスが壊れます-コードを公開すると、結婚するという選択になります。したがって、実際の機能をスーパークラスに変更しない場合は、必要なものを拡張するのではなく、後で使用する場合に変更する自由が大幅に広がります。たとえば、JPanelをサブクラス化するとします。これは通常間違っています。サブクラスがどこかで公開されている場合、その決定を再検討する機会はありません。JComponent getThePanel()としてアクセスする場合でも、それを行うことができます(ヒント:APIとして内部のコンポーネントのモデルを公開します)。
- オブジェクト階層はスケーリングされません(または、後でスケーリングすることは、事前に計画するよりもはるかに困難です)。これは、古典的な「レイヤーが多すぎる」問題です。これについては、以下で説明し、AskTheOracleパターンがそれをどのように解決できるかを説明します(ただし、OOPの純粋主義者を怒らせる可能性があります)。
..。
あなたが継承を許可するならば、あなたが一粒の塩で取るかもしれない何をすべきかについての私の見解は次のとおりです:
- 定数を除いて、フィールドを公開しないでください
- メソッドは抽象的または最終的でなければなりません
- スーパークラスコンストラクターからメソッドを呼び出さない
..。
これらはすべて、大規模なプロジェクトよりも小規模なプロジェクトには適用されず、公開プロジェクトよりもプライベートクラスには適用されません。
継承は非常に強力ですが、強制することはできません(円-楕円問題を参照)。真の「is-a」サブタイプの関係を完全に確信できない場合は、構成を使用するのが最善です。
航空機がエンジンと翼の 2 つの部分だけで構成されているとします。
次に、航空機クラスを設計するには 2 つの方法があります。
Class Aircraft extends Engine{
var wings;
}
これで、航空機は固定翼から始めて
、その場で回転翼に変更できます。それは本質的
に翼のあるエンジンです。
しかし、その場でエンジンも 交換したい場合はどうすればよいでしょうか?
基本クラスEngine
がミューテーターを公開してその
プロパティを変更するか、次のように再設計Aircraft
します。
Class Aircraft {
var wings;
var engine;
}
これで、エンジンもその場で交換できます。
継承によって、サブクラスとスーパー クラスの間に強い関係が生まれます。サブクラスは、スーパークラスの実装の詳細を認識している必要があります。スーパークラスを作成するのは、それを拡張する方法を考えなければならない場合、はるかに困難です。クラスの不変条件を注意深く文書化し、オーバーライド可能なメソッドが内部で使用する他のメソッドを説明する必要があります。
階層が実際に is-a-relationship を表している場合、継承が役立つことがあります。これは、クラスは変更に対して閉じられているが、拡張に対して開かれているべきであると述べているオープンクローズド原則に関連しています。そうすれば、ポリモーフィズムを持つことができます。スーパータイプとそのメソッドを扱うジェネリックメソッドを持ちますが、動的ディスパッチを介してサブクラスのメソッドが呼び出されます。これは柔軟であり、ソフトウェアに不可欠な間接化を作成するのに役立ちます (実装の詳細についてはあまり知りません)。
ただし、継承は簡単に過剰に使用され、クラス間の強い依存関係により複雑さが増します。また、プログラムの実行中に何が起こるかを理解することは、レイヤーとメソッド呼び出しの動的選択のためにかなり難しくなります。
構成をデフォルトとして使用することをお勧めします。よりモジュール化されており、遅延バインディングの利点があります (コンポーネントを動的に変更できます)。また、物事を別々にテストする方が簡単です。また、クラスのメソッドを使用する必要がある場合、特定の形式である必要はありません (Liskov Substitution Principle)。
Uncle Bob のクラス設計のSOLID原則のThe Liskov Substitution Principleを参照する必要があります。:)
基本クラスの API を「コピー」/公開する場合は、継承を使用します。機能を「コピー」するだけの場合は、委任を使用します。
この一例: List から Stack を作成したいとします。スタックには、ポップ、プッシュ、ピークのみがあります。スタックに push_back、push_front、removeAt などの種類の機能が必要ない場合は、継承を使用しないでください。
これら 2 つの方法はうまく共存でき、実際にお互いをサポートします。
コンポジションは、モジュール化されているだけです。親クラスに似たインターフェイスを作成し、新しいオブジェクトを作成して、呼び出しを委任します。これらのオブジェクトがお互いを認識する必要がない場合、構成は非常に安全で使いやすいものになります。ここには非常に多くの可能性があります。
ただし、何らかの理由で、経験の浅いプログラマーのために「子クラス」が提供する関数に親クラスがアクセスする必要がある場合は、継承を使用するのに最適な場所のように見えるかもしれません。親クラスは、サブクラスによって上書きされる独自の抽象「foo()」を呼び出すだけで、抽象ベースに値を渡すことができます。
良いアイデアのように見えますが、多くの場合、クラスに foo() を実装するオブジェクトを与える (または foo() に提供された値を手動で設定する) 方が、新しいクラスを必要とするいくつかの基本クラスから継承するよりも優れています。指定する関数 foo()。
なんで?
継承は情報を移動する方法としては不十分だからです。
構成にはここで真の優位性があります: 関係を逆にすることができます: 「親クラス」または「抽象ワーカー」は、特定のインターフェースを実装する特定の「子」オブジェクトを集約できます +任意の子を他のタイプの親内に設定できます。タイプです。また、オブジェクトの数に制限はありません。たとえば、MergeSort や QuickSort は、抽象的な Compare インターフェイスを実装するオブジェクトのリストを並べ替えることができます。別の言い方をすれば、「foo()」を実装するオブジェクトのグループと、「foo()」を持つオブジェクトを利用できる他のオブジェクトのグループは、一緒に遊ぶことができます。
継承を使用する本当の理由は 3 つあります。
- 同じインターフェースを持つ多くのクラスがあり、それらを書く時間を節約したい
- 各オブジェクトに同じ基本クラスを使用する必要があります
- どのような場合でも公開できないプライベート変数を変更する必要があります
これらが当てはまる場合、おそらく継承を使用する必要があります。
理由 1 を使用することは何も悪いことではありません。オブジェクトにしっかりしたインターフェースを持たせることは非常に良いことです。これは、合成または継承を使用して行うことができますが、このインターフェイスが単純で変更されない場合は問題ありません。通常、ここでは継承が非常に効果的です。
理由が 2 の場合、少しトリッキーになります。本当に同じ基本クラスを使用するだけでよいのでしょうか? 一般に、同じ基本クラスを使用するだけでは十分ではありませんが、それはフレームワークの要件であり、避けられない設計上の考慮事項である可能性があります。
ただし、プライベート変数を使用する場合 (ケース 3)、問題が発生する可能性があります。グローバル変数が安全でないと考える場合は、継承を使用してプライベート変数へのアクセスも安全でないことを検討する必要があります。注意してください、グローバル変数はすべて悪いわけではありません.データベースは本質的にグローバル変数の大きなセットです. でも、我慢できれば全然大丈夫です。
考慮事項があることは別として、オブジェクトが通過しなければならない継承の「深さ」も考慮する必要があります。継承のレベルが5または6を超えると、予期しないキャストやボクシング/アンボクシングの問題が発生する可能性があります。そのような場合は、代わりにオブジェクトを作成することをお勧めします。
2 つのクラス間にis-a関係がある場合(たとえば、犬は犬です)、継承に進みます。
一方、2 つのクラスの間にhas-aまたは何らかの形容詞関係がある (学生はコースを持っている) または (教師はコースを勉強している) 場合は、構成を選択しました。
これを理解する簡単な方法は、クラスのオブジェクトがその親クラスと同じインターフェイスを持つ必要がある場合に継承を使用して、親クラスのオブジェクトとして扱うことができるようにすることです (アップキャスト)。 . さらに、派生クラス オブジェクトの関数呼び出しはコード内のどこでも同じままですが、呼び出す特定のメソッドは実行時に決定されます (つまり、低レベルの実装が異なり、高レベルのインターフェイスは同じままです)。
新しいクラスに同じインターフェイスを持たせる必要がない場合、つまり、そのクラスのユーザーが知る必要のないクラスの実装の特定の側面を隠したい場合は、合成を使用する必要があります。したがって、合成はカプセル化をサポートする(つまり、実装を隠蔽する) 方法であり、継承は抽象化をサポートすることを目的としています(つまり、何かの単純化された表現を提供します。この場合、さまざまな内部を持つ型の範囲に対して同じインターフェイスを提供します)。
私は@Pavelに同意します。彼が言うとき、構成の場所があり、継承の場所があります。
これらの質問のいずれかに対する答えが肯定的である場合は、継承を使用する必要があると思います。
- あなたのクラスは、ポリモーフィズムの恩恵を受ける構造の一部ですか? たとえば、draw() というメソッドを宣言する Shape クラスがある場合、Circle クラスと Square クラスを Shape のサブクラスにする必要があるのは明らかです。これにより、クライアント クラスは特定のサブクラスではなく Shape に依存するようになります。
- あなたのクラスは、別のクラスで定義された高レベルの相互作用を再利用する必要がありますか? テンプレート メソッドのデザイン パターンは、継承なしでは実装できません。すべての拡張可能なフレームワークがこのパターンを使用していると思います。
ただし、純粋にコードの再利用を目的としている場合は、構成の方が優れた設計の選択肢となる可能性が高くなります。
不変条件を列挙できる場合は、サブタイプ化が適切であり、より強力です。それ以外の場合は、拡張性のために関数構成を使用します。
継承は、コードを再利用するための非常に強力なメカニズムです。しかし、適切に使用する必要があります。サブクラスが親クラスのサブタイプでもある場合、継承は正しく使用されていると言えます。前述したように、リスコフの置換原理はここで重要なポイントです。
サブクラスはサブタイプと同じではありません。サブタイプではないサブクラスを作成する場合があります (このような場合は合成を使用する必要があります)。サブタイプとは何かを理解するために、タイプとは何かの説明を始めましょう。
数値 5 が整数型であると言うとき、5 が可能な値のセットに属していることを示しています (例として、Java プリミティブ型の可能な値を参照してください)。また、加算や減算など、値に対して実行できる有効なメソッドのセットがあることも述べています。最後に、常に満たされる一連のプロパティがあることを示しています。たとえば、値 3 と 5 を加算すると、結果として 8 が得られます。
別の例を挙げると、抽象データ型、整数のセットと整数のリストについて考えてください。それらが保持できる値は整数に制限されています。どちらも、add(newValue) や size() などの一連のメソッドをサポートしています。そして、それらは両方とも異なるプロパティ (クラス不変) を持ちます。セットは重複を許可しませんが、リストは重複を許可します (もちろん、両方が満たす他のプロパティがあります)。
サブタイプは、親タイプ (またはスーパータイプ) と呼ばれる別のタイプとの関係を持つタイプでもあります。サブタイプは、親タイプの機能 (値、メソッド、およびプロパティ) を満たす必要があります。この関係は、スーパータイプが期待されるコンテキストでは、実行の動作に影響を与えることなく、サブタイプで代用できることを意味します。私が言っていることを例証するために、いくつかのコードを見に行きましょう。整数のリストを(ある種の疑似言語で)書いたとします。
class List {
data = new Array();
Integer size() {
return data.length;
}
add(Integer anInteger) {
data[data.length] = anInteger;
}
}
次に、整数のセットを整数のリストのサブクラスとして記述します。
class Set, inheriting from: List {
add(Integer anInteger) {
if (data.notContains(anInteger)) {
super.add(anInteger);
}
}
}
Set of integers クラスは List of Integers のサブクラスですが、List クラスのすべての機能を満たしていないため、サブタイプではありません。メソッドの値と署名は満たされていますが、プロパティは満たされていません。add(Integer) メソッドの動作は明らかに変更されており、親型のプロパティは保持されていません。クラスのクライアントの観点から考えてください。整数のリストが期待されるところに、整数のセットを受け取る場合があります。クライアントは、値を追加し、その値がリストに既に存在する場合でも、その値をリストに追加することを希望する場合があります。しかし、値が存在する場合、彼女はその動作を取得しません。彼女にとって大きな驚きです!
これは、継承の不適切な使用の典型的な例です。この場合はコンポジションを使用してください。
(からのフラグメント:継承を適切に使用します)。
コンポジションが優先されますが、継承の長所とコンポジションの短所を強調したいと思います。
継承の長所:
論理的な「IS A」関係を確立します。CarとTruckが 2 種類のVehicle (基本クラス) の場合、子クラスは基本クラスです。
すなわち
車は乗り物です
トラックは乗り物です
継承により、機能を定義/変更/拡張できます
- 基本クラスは実装を提供せず、サブクラスは完全なメソッド(抽象)をオーバーライドする必要があります=>コントラクトを実装できます
- 基本クラスはデフォルトの実装を提供し、サブクラスは動作を変更できます =>コントラクトを再定義できます
- サブクラスは、最初のステートメントとして super.methodName() を呼び出すことにより、基本クラスの実装に拡張機能を追加します =>コントラクトを拡張できます
- 基本クラスはアルゴリズムの構造を定義し、サブクラスはアルゴリズムの一部をオーバーライドします =>基本クラスのスケルトンを変更せずにTemplate_methodを実装できます
構成の短所:
- 継承では、 IS A関係により、サブクラスは基底クラスのメソッドを実装していなくても、基底クラスのメソッドを直接呼び出すことができます。コンポジションを使用する場合、コンテナ クラスにメソッドを追加して、含まれているクラス API を公開する必要があります。
たとえば 、 CarにVehicleが含まれていて、 Vehicleで定義されているCarの価格を取得する必要がある場合、コードは次のようになります。
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
私が聞いた経験則では、継承は「is-a」の関係の場合に使用し、構成は「has-a」の場合に使用する必要があります。それでも、複雑さが大幅に軽減されるので、常に作曲に傾倒する必要があると思います。
多くの人が言ったように、私はまずチェックから始めます - 「is-a」関係が存在するかどうか。存在する場合は、通常、次のことを確認します。
基本クラスをインスタンス化できるかどうか。つまり、基本クラスを非抽象化できるかどうかです。抽象的でないことができる場合、私は通常、構成を好みます
例 1. 会計士は従業員です。ただし、Employee オブジェクトはインスタンス化できるため、継承は使用しません。
例 2. BookはSellingItem です。SellingItem はインスタンス化できません。これは抽象的な概念です。したがって、継承を使用します。SellingItem は抽象基本クラス (またはC# のインターフェース) です。
このアプローチについてどう思いますか?
また、なぜ継承を使用するのですか? で@anon の回答をサポートします。
継承を使用する主な理由は、構成の形式としてではなく、ポリモーフィックな動作を取得できるようにするためです。ポリモーフィズムが必要ない場合は、おそらく継承を使用しないでください。
@マシューM。https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448で言う
継承の問題は、次の 2 つの直交する目的に使用できることです。
インターフェイス (ポリモーフィズム用)
実装 (コードの再利用のため)
参照
このルールは完全にナンセンスです。なんで?
その理由は、すべての場合において、構成を使用するか継承を使用するかを判断できるからです。これは、「IS something A something else」または「HAS something A something else」という質問への回答によって決定されます。
何かを別のものにしたり、別のものを持ったりすることを「好む」ことはできません。厳密な論理規則が適用されます。
また、あらゆる状況でこの質問に対する答えを与えることができるため、「不自然な例」はありません。
この質問に答えられない場合は、何か他の問題があります。これには、通常はインターフェースの誤った使用の結果であるクラスの責任の重複が含まれますが、異なるクラスで同じコードを書き直すことはあまりありません。
このような状況を回避するために、クラスの責任に完全に似た適切な名前をクラスに使用することもお勧めします。
自分自身 (または別のプログラマー) に何を強制したいのか、いつ自分自身 (または別のプログラマー) にもっと自由を与えたいのか。継承は、誰かに特定の問題を処理/解決する方法を強制して、間違った方向に向かうことができないようにする場合に役立つと主張されてきました。
Is-a
とHas-a
役立つ経験則です。