継承よりも構成を好むのはなぜですか?各アプローチにはどのようなトレードオフがありますか?いつ合成より継承を選ぶべきですか?
継承よりも構成を好むのはなぜですか?各アプローチにはどのようなトレードオフがありますか?いつ合成より継承を選ぶべきですか?
回答:
継承よりも構成が優先されます。これは、より柔軟で、後で簡単に変更できるためです。ただし、常に作成する方法は使用しないでください。構成では、依存性注入/セッターを使用して動作をその場で簡単に変更できます。ほとんどの言語では複数の型からの派生を許可していないため、継承はより厳格です。TypeAから派生したガチョウは多かれ少なかれ調理されます。
上記の私の酸テストは:
TypeBは、TypeAが期待される場所でTypeBを使用できるように、TypeAの完全なインターフェイス(すべてのパブリックメソッド以上)を公開したいですか?継承を示します。
TypeBはTypeAによって公開される動作の一部または一部のみを必要としますか?構成の必要性を示します。
更新:ちょうど私の答えに戻ってきましたが、「このタイプから継承する必要がありますか?」のテストとして、Barbara LiskovのLiskov Substitution Principleの具体的な言及がなければ、不完全なようです。
封じ込めには関係があると考えてください。車にはエンジンがあり、人には名前があります。
継承は関係であると考えてください。車は乗り物であり、人は哺乳類である、など。
私はこのアプローチを信用していません。私は、Steve McConnellによるコードコンプリートの第2版、セクション6.3からそれを直接取り入れました。
違いを理解すれば説明しやすくなります。
この例として、クラスを使用しないPHP(特にPHP5より前)があります。すべてのロジックは、一連の関数にエンコードされます。ヘルパー関数などを含む他のファイルを含めて、関数内でデータを渡すことによってビジネスロジックを実行できます。これは、アプリケーションが成長するにつれて、管理が非常に困難になる可能性があります。PHP5は、よりオブジェクト指向のデザインを提供することでこれを改善しようとします。
これにより、クラスの使用が促進されます。継承は、OO設計の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は「Manager of Operations」または「Mr.」を返す必要がありますか?構成の下では、このあいまいさはより適切に処理されます。
Class Manager {
public string Title;
public Manager(Person p, Employee e)
{
this.Title = e.Title;
}
}
Managerオブジェクトは、従業員と個人として構成されます。タイトルの動作は従業員から取得されます。この明示的な構成により、あいまいさが取り除かれ、バグが少なくなります。
継承によって提供される紛れもない利点のすべてに、そのいくつかの欠点があります。
継承の短所:
一方、オブジェクトの構成は、他のオブジェクトへの参照を取得するオブジェクトを通じて、実行時に定義されます。そのような場合、これらのオブジェクトはお互いの保護されたデータに到達することができず(カプセル化の中断なし)、お互いのインターフェースを尊重することを強制されます。この場合も、継承の場合よりも実装の依存関係が大幅に少なくなります。
簡単に言えば、「継承よりも構成を優先する」に同意しますが、「コカコーラよりもジャガイモを優先する」ように聞こえることがよくあります。継承する場所と構成する場所があります。違いを理解する必要があります。そうすれば、この質問は消えます。私にとってそれが本当に意味することは、「継承を使用する場合、もう一度考えてみてください。おそらく、構成が必要なのです」です。
食べたいときはコカコーラよりもジャガイモを好み、飲みたいときはジャガイモよりコカコーラを好むべきです。
サブクラスを作成することは、スーパークラスのメソッドを呼び出すための単なる便利な方法以上のものを意味するはずです。サブクラス「is-a」スーパークラスを構造的および機能的にスーパークラスとして使用でき、それを使用する場合は、継承を使用する必要があります。そうでない場合-それは継承ではなく、何か他のものです。構成とは、オブジェクトが別のオブジェクトで構成されている場合、またはオブジェクトと何らかの関係がある場合です。
だから私にとっては、相続や作文が必要かどうかわからないように見えますが、本当の問題は、飲みたいのか食べたいのかわからないことです。問題のある領域についてもっと考え、よく理解してください。
InternalCombustionEngine
、派生クラスを持つ基本クラスを考えGasolineEngine
ます。後者は、基本クラスにはないスパークプラグなどを追加しますInternalCombustionEngine
が、スパークプラグを使用すると、スパークプラグが使用されるようになります。
継承は、特に手続き型の土地からのものでかなり魅力的であり、しばしば一見エレガントに見えます。つまり、この1ビットの機能を他のクラスに追加するだけです。まあ、問題の一つは
基本クラスは、保護されたメンバーの形でサブクラスに実装の詳細を公開することにより、カプセル化を解除します。これにより、システムが堅牢で壊れやすくなります。しかし、より悲劇的な欠陥は、新しいサブクラスが継承チェーンのすべての手荷物と意見をもたらすことです。
記事「継承は悪である:DataAnnotationsModelBinderの重大な失敗」では、C#でのこの例を紹介しています。これは、コンポジションが使用されるべきであった場合の継承の使用法と、リファクタリングの方法を示しています。
JavaまたはC#では、インスタンス化されたオブジェクトはその型を変更できません。
したがって、オブジェクトが別のオブジェクトとして表示される必要がある場合、またはオブジェクトの状態や条件に応じて異なる動作をする必要がある場合は、コンポジションを使用します。状態と戦略の設計パターンを参照してください。
オブジェクトを同じ型にする必要がある場合は、継承を使用するか、インターフェイスを実装します。
Client
。その後、新しいコンセプトのPreferredClient
ポップアップが後に登場します。PreferredClient
継承する必要がありClient
ますか?結局のところ、優先クライアントは「クライアント」です。まあ、それほど速くない...あなたが言ったようにオブジェクトは実行時にクラスを変えることができない。どのようにclient.makePreferred()
操作をモデル化しますか?おそらくその答えは、Account
おそらく欠落しているコンセプトのあるコンポジションを使用することでしょうか?
Client
クラスを、おそらくの概念をカプセル化1だけがありますAccount
することができたStandardAccount
かPreferredAccount
...
ここで満足のいく答えが見つからなかったので、私は新しいものを書きました。
「継承よりも構成を優先する」理由を理解するには、最初に、この短縮されたイディオムで省略された仮定を取り戻す必要があります。
継承には2つの利点があります。サブタイピングとサブクラス化です。
サブタイピングとは、タイプ(インターフェース)シグニチャー、つまりAPIのセットに準拠することを意味し、サブタイプのポリモーフィズムを実現するためにシグニチャーの一部をオーバーライドできます。
サブクラス化とは、メソッド実装の暗黙的な再利用を意味します。
2つの利点には、継承を行うための2つの異なる目的があります。サブタイプ指向とコード再利用指向です。
コードの再利用が唯一の目的である場合、サブクラス化は必要なものより1つ多く提供する可能性があります。つまり、親クラスのいくつかのパブリックメソッドは子クラスにはあまり意味がありません。この場合、継承よりも構成を優先する代わりに、構成が要求されます。これは「is-a」対「has-a」の概念の由来でもあります。
したがって、サブタイピングが目的の場合、つまり後で新しいクラスを多態的に使用する場合にのみ、継承または構成を選択するという問題に直面します。これは、議論中の短縮されたイディオムで省略される前提です。
サブタイプとは、タイプシグネチャに準拠することです。これは、コンポジションが常に同じタイプのAPIを公開する必要があることを意味します。ここでトレードオフが始まります:
継承は、オーバーライドされない場合、単純なコードの再利用を提供しますが、コンポジションは、単に委任の単純な仕事であっても、すべてのAPIを再コード化する必要があります。
継承は単純明快提供オープン再帰を内部多型部位を介してthis
、すなわちオーバーライドメソッド(あるいは呼び出すタイプ別のメンバ関数では)、のいずれか(ただし、パブリックまたはプライベート落胆)。オープン再帰は、compositionを介してシミュレートできますが、追加の作業が必要であり、常に実行できるとは限りません(?)。重複した質問に対するこの回答は、同様のことを語っています。
継承は保護されたメンバーを公開します。これにより、親クラスのカプセル化が解除され、サブクラスで使用された場合、子とその親の間に別の依存関係が導入されます。
コンポジションには、制御の反転の利点があり、その依存関係は、デコレーターパターンとプロキシパターンに示されているように、動的に注入できます。
構成は、インターフェイスへのプログラミングの直後に行われます。
構成には、多重継承が容易であるという利点があります。
上記のトレードオフを考慮して、継承よりも構成を優先します。しかし、密接に関連するクラス、つまり暗黙的なコードの再利用が本当にメリットをもたらす場合、またはオープンな再帰の魔法の力が望まれる場合、継承が選択されます。
個人的には、継承よりも常に構成を優先することを学びました。合成では解決できない継承で解決できるプログラム上の問題はありません。ただし、場合によっては、インターフェース(Java)またはプロトコル(Obj-C)を使用する必要があります。C ++はそのようなことを何も知らないため、抽象基本クラスを使用する必要があります。つまり、C ++で継承を完全に取り除くことはできません。
構成は多くの場合、より論理的であり、抽象化、カプセル化、コードの再利用が向上し(特に非常に大規模なプロジェクトで)、コード内のどこかで孤立した変更を行ったという理由だけで、離れたところにあるものを壊す可能性が低くなります。また、「単一の責任の原則」を支持することが容易になります。これは、「クラスが変更される理由は1つ以上あるべきではありません」とまとめられることが多く、すべてのクラスが特定の目的のために存在し、その目的に直接関連するメソッドのみを持っています。また、継承ツリーが非常に浅いため、プロジェクトが非常に大きくなり始めた場合でも、概要を簡単に維持できます。多くの人々は、継承は私たちの現実の世界を表すと考えています結構ですが、それは真実ではありません。現実の世界では、継承よりもはるかに多くの構成を使用しています。手に持つことができるほとんどすべての実世界のオブジェクトは、他のより小さな実世界のオブジェクトで構成されています。
ただし、構成には欠点があります。継承を完全にスキップして構成のみに焦点を合わせると、継承を使用した場合には不要だったいくつかの追加のコード行を記述する必要があることがよくあります。あなたは時々自分自身を繰り返すことを余儀なくされ、これはDRY原則に違反します(DRY =自分を繰り返さないでください)。また、合成では多くの場合委任が必要であり、メソッドは別のオブジェクトの別のメソッドを呼び出すだけで、この呼び出しを囲む他のコードはありません。このような「二重メソッド呼び出し」(これは、3倍または4倍のメソッド呼び出しに簡単に拡張でき、さらにそれよりも遠い)は、単純に親のメソッドを継承する継承よりもパフォーマンスがはるかに劣ります。継承されたメソッドの呼び出しは、継承されていないメソッドの呼び出しと同じくらい高速かもしれませんし、少し遅いかもしれませんが、通常は2つの連続したメソッド呼び出しよりも高速です。
ほとんどのオブジェクト指向言語は多重継承を許可していないことに気づいたかもしれません。多重継承が実際にあなたに何かを買うことができるいくつかのケースがありますが、それらは規則よりもむしろ例外です。「複数の継承がこの問題を解決するための本当に素晴らしい機能になるだろう」と思われる状況に遭遇したときはいつでも、通常、継承を完全に再考する必要があります。 、構成に基づくソリューションは、通常、はるかにエレガントで、柔軟性があり、将来を見据えたものになります。
継承は本当に素晴らしい機能ですが、ここ2、3年は使いすぎていると思います。人々は継承を、それが実際に釘であるか、ねじであるか、あるいは何か完全に異なるものであるかに関係なく、すべてを釘付けできる1つのハンマーとして扱いました。
TextFile
ですFile
。
私の一般的な経験則:継承を使用する前に、構成がより意味があるかどうかを検討します。
理由:サブクラス化は通常、より複雑で関連性があることを意味します。つまり、間違いを犯さずに変更、維持、拡張することが困難になります。
Tim Boudreau of Sun からのより完全で具体的な回答:
私が見る継承の使用に関する一般的な問題は次のとおりです。
- 無邪気な行為は予期しない結果をもたらす可能性があります -この典型的な例は、サブクラスのインスタンスフィールドが初期化される前の、スーパークラスコンストラクターからのオーバーライド可能なメソッドの呼び出しです。完璧な世界では、誰もそれをすることはありません。これは完璧な世界ではありません。
- これは、サブクラス作成者がメソッド呼び出しの順序などについて仮定を立てるというひねくれた誘惑を提供します。スーパークラスが時間とともに進化する可能性がある場合、そのような仮定は安定しない傾向があります。トースターとコーヒーポットの例えもご覧ください。
- クラスは重くなります。スーパークラスがコンストラクターで実行している作業や、使用するメモリの量が必ずしもわかるとは限りません。したがって、無害な軽量オブジェクトを構築することは、あなたが考えるよりもはるかに高価になる可能性があり、スーパークラスが進化する場合、これは時間とともに変化する可能性があります
- サブクラスの爆発的な増加を助長します。クラスローディングには時間がかかり、クラスが増えるとメモリが消費されます。NetBeansの規模でアプリを扱うまでは問題にならないかもしれませんが、メニューの最初の表示が大量のクラスの読み込みをトリガーしたためにメニューが遅くなるなど、実際の問題がありました。より宣言的な構文やその他の手法に移行することでこれを修正しましたが、修正にも時間がかかりました。
- 後で変更するのが難しくなります。クラスをパブリックにした場合、スーパークラスをスワップするとサブクラスが壊れます。コードをパブリックにすると、結婚することになります。したがって、実際の機能をスーパークラスに変更しない場合は、必要なものを拡張するよりも、後で使用するほうがはるかに自由に変更できます。たとえば、JPanelをサブクラス化するとします。これは通常間違っています。サブクラスがどこかで公開されている場合、その決定を再検討する機会はありません。JComponent getThePanel()としてアクセスされた場合でも、それを行うことができます(ヒント:内のコンポーネントのモデルをAPIとして公開します)。
- オブジェクト階層はスケーリングしません(または、後で階層化することは、事前に計画するよりもはるかに困難です)。これは、古典的な「層が多すぎる」問題です。以下でこれについて説明し、AskTheOracleパターンがそれをどのように解決できるかを示します(ただし、OOPの純粋主義者を怒らせる可能性があります)。
...
あなたが継承を許可する場合、何をすべきかについての私の見解は、塩の粒で取ることができます:
- 定数を除いて、フィールドを公開しない
- メソッドは抽象的または最終的でなければならない
- スーパークラスコンストラクターからメソッドを呼び出さない
...
これはすべて、大規模プロジェクトよりも小規模プロジェクトに適用され、パブリックプロジェクトよりもプライベートクラスには適用されません。
他の回答を参照してください。
次の文が当てはまる場合、Bar
クラスはクラスを継承できるとよく言われFoo
ます。
- バーはフーです
残念ながら、上記のテストだけでは信頼できません。代わりに以下を使用してください。
- バーはFOOある、と
- バーはfoosができることはすべてできる。
すべてのその最初のテスト性を保証ゲッターのFoo
中メイクセンスBar
(=共有特性)、第二の試験は確かにすべてのことをする間セッターのFoo
中メイクセンスBar
(=共有機能)。
例1:犬->動物
犬は動物であり、犬は動物ができるすべてのことを行うことができます(呼吸、死亡など)。したがって、クラスはクラスDog
を継承できAnimal
ます。
例2:円-/->楕円
円は楕円ですが、円は楕円ができることすべてを行うことはできません。たとえば、円は伸縮できませんが、楕円は伸縮できます。したがって、クラスはクラスCircle
を継承できませんEllipse
。
これはCircle-Ellipse問題と呼ばれ、実際には問題ではありません。最初のテストだけでは継承が可能であると結論付けるのに十分ではないことの明確な証拠です。特に、この例は、派生クラスが基本クラスの機能を拡張し、制限しないことを強調しています。それ以外の場合、基本クラスは多態的に使用できませんでした。
あなたがいてもすることができます継承を使用し、あなたが意味するものではありませんべき:組成物を用いて、常にオプションです。継承は、暗黙的なコードの再利用と動的なディスパッチを可能にする強力なツールですが、いくつかの欠点があるため、多くの場合、構成が優先されます。継承と構成の間のトレードオフは明らかではありません。私の意見では、lcnの回答で最もよく説明されています。
経験則として、私は、ポリモーフィックな使用が非常に一般的であると予想される場合は、コンポジションより継承を選択する傾向があります。たとえばWidget
、GUIフレームワークにポリモーフィッククラスを、またはNode
XMLライブラリにポリモーフィッククラスを使用すると、純粋に構成に基づいたソリューションよりもはるかに読みやすく、直感的に使用できるAPIを使用できます。
ご存知のように、継承が可能かどうかを判断するために使用される別の方法は、リスコフ代入原則と呼ばれます。
基本クラスへのポインターまたは参照を使用する関数は、それを知らなくても派生クラスのオブジェクトを使用できなければなりません
基本的に、これは、基本クラスを多態的に使用できる場合に継承が可能であることを意味します。これは、「バーはfooであり、バーはfooが実行できるすべてのことを実行できる」というテストに相当すると私は考えています。
computeArea(Circle* c) { return pi * square(c->radius()); }
。Ellipseを渡すと明らかに壊れます(radius()はどういう意味ですか?)。楕円は円ではないため、円から派生してはなりません。
computeArea(Circle *c) { return pi * width * height / 4.0; }
今では一般的です。
width()
とheight()
?ライブラリユーザーが「EggShape」と呼ばれる別のクラスを作成することにした場合はどうなりますか?「Circle」から派生する必要がありますか?もちろん違います。卵の形は円ではなく、楕円も円ではないため、LSPを壊すため、Circleから派生するものはありません。Circle *クラスで操作を実行するメソッドは、円が何であるかについて強力な仮定を行います。これらの仮定を破ると、ほぼ確実にバグが発生します。
継承は、サブクラスとスーパークラスの間に強い関係を作成します。サブクラスは、スーパークラスの実装の詳細を認識する必要があります。スーパークラスを拡張する方法を考える必要がある場合、スーパークラスの作成ははるかに困難です。クラスの不変条件を慎重に文書化し、オーバーライド可能なメソッドが内部で使用する他のメソッドを指定する必要があります。
階層が本当に関係を表す場合、継承が役立つことがあります。それはクラスが変更のために閉じられるべきであるが拡張に対して開かれるべきであると述べているOpen-Closed Principleに関連しています。そうすれば、ポリモーフィズムを持つことができます。スーパータイプとそのメソッドを処理するジェネリックメソッドを持ちますが、動的ディスパッチを介してサブクラスのメソッドが呼び出されます。これは柔軟性があり、間接的なものを作成するのに役立ちます。これは、ソフトウェアに不可欠です(実装の詳細についてあまり知らないため)。
ただし、継承は使いすぎやすく、クラス間の依存関係が複雑になるため、複雑さが増します。また、レイヤーとメソッド呼び出しの動的な選択により、プログラムの実行中に何が起こるかを理解することはかなり難しくなります。
デフォルトとして作曲を使用することをお勧めします。よりモジュール化されており、遅延バインディングの利点があります(コンポーネントを動的に変更できます)。また、個別にテストする方が簡単です。また、クラスのメソッドを使用する必要がある場合でも、特定の形式にする必要はありません(Liskov Substitution Principle)。
Inheritance is sometimes useful... That way you can have polymorphism
を、継承とポリモーフィズムの概念をハードリンクするものとして解釈しました(サブタイピングはコンテキストを前提として想定されています)。私のコメントは、あなたがコメントで明確にしていることを指摘することを意図していました。その継承は、ポリモーフィズムを実装する唯一の方法ではなく、実際には、構成と継承を決定する際の決定要因ではありません。
航空機にエンジンと翼の2つの部分しかないと仮定します。
次に、航空機クラスを設計するには2つの方法があります。
Class Aircraft extends Engine{
var wings;
}
これで、航空機は翼を固定した状態で開始し
、その場で回転翼に変更できます。それは本質的に
翼を持つエンジンです。しかし
、その場でエンジンを変更したい場合はどうなりますか?
基本クラスEngine
は、その
プロパティを変更するためのミューテーターを公開するか、次のように再設計Aircraft
します。
Class Aircraft {
var wings;
var engine;
}
これで、エンジンをその場で交換することもできます。
ボブおじさんのクラス設計のSOLID原則のリスコフ代替原則を確認する必要があります。:)
これらの2つの方法は、一緒にうまく生活し、実際にお互いをサポートすることができます。
構成は、それをモジュラーで再生するだけです。親クラスと同様のインターフェースを作成し、新しいオブジェクトを作成して、その呼び出しを委任します。これらのオブジェクトがお互いを知る必要がない場合は、コンポジションを使用するのは非常に安全で簡単です。ここには非常に多くの可能性があります。
ただし、何らかの理由で親クラスが「子クラス」が提供する関数にアクセスする必要がある場合、経験の浅いプログラマーは継承を使用するのに最適な場所に見えるかもしれません。親クラスは、サブクラスによって上書きされた独自の抽象「foo()」を呼び出すだけで、抽象ベースに値を渡すことができます。
それはいい考えのように見えますが、多くの場合、必要な基本クラスから新しいクラスを継承するよりも、foo()を実装するオブジェクトをクラスに与える(またはfoo()によって手動で提供される値を設定する)方が良いです指定する関数foo()
どうして?
継承は情報を移動するための貧弱な方法だからです。
構成には本当の意味があります。関係は逆にすることができます。「親クラス」または「抽象ワーカー」は、特定のインターフェースを実装する特定の「子」オブジェクトを集約できます。タイプです。オブジェクトはいくつでも存在できます。たとえば、MergeSortやQuickSortは、抽象的なCompareインターフェースを実装するオブジェクトのリストを並べ替えることができます。または別の言い方をすると、「foo()」を実装するオブジェクトのグループと、「foo()」を持つオブジェクトを利用できる他のオブジェクトのグループは、一緒に遊ぶことができます。
継承を使用する本当の3つの理由を考えることができます。
これらが真の場合、おそらく継承を使用する必要があります。
理由1を使用しても何も悪いことはありません。オブジェクトに堅固なインターフェースを持つことは非常に良いことです。これは、コンポジションを使用して、または継承を使用して行うことができます。このインターフェースがシンプルで変更されない場合は問題ありません。通常、継承は非常に効果的です。
理由が2番の場合、少し注意が必要です。本当に同じ基本クラスを使用する必要があるだけですか?一般に、同じ基本クラスを使用するだけでは十分ではありませんが、フレームワークの要件である可能性があり、これは回避できない設計上の考慮事項です。
ただし、プライベート変数(ケース3)を使用する場合は、問題が発生する可能性があります。グローバル変数を安全でないと考える場合は、継承を使用してプライベート変数へのアクセスも安全でないことを考慮する必要があります。大事なことは、グローバル変数がすべて悪いということではありません。データベースは本質的に大きなグローバル変数のセットです。しかし、それを処理できる場合は、それで問題ありません。
新しいプログラマーのために別の視点からこの質問に取り組むには:
継承は、オブジェクト指向プログラミングを学ぶときに早い段階で教えられることが多いため、一般的な問題の簡単な解決策と見なされています。
私には3つのクラスがあり、すべてにいくつかの共通機能が必要です。したがって、基本クラスを作成して、それらすべてを継承する場合、それらはすべてその機能を持ち、1か所で維持するだけで済みます。
それは素晴らしいように聞こえますが、実際には、いくつかの理由の1つのために、ほとんど機能しません。
結局のところ、コードをいくつかの難しい結び目で結び、「クールです。継承について学び、今はそれを使用しました」と言う以外は、コードから何の利益も得ません。私たち全員がそれを行ったので、それは非難することを意味しません。しかし、誰も私たちにしないように言わなかったので、私たちは皆それをやった。
誰かが「継承よりも構成を優先する」と説明するとすぐに、継承を使用してクラス間で機能を共有しようとするたびに思い出し、ほとんどの場合、それが実際にはうまく機能しないことに気付きました。
解毒剤は単一責任原則です。それを制約と考えてください。私のクラスは一つのことをしなければなりません。私はクラスに、その1つのことをどういうわけか説明する名前を付けることができなければなりません。(すべてに例外はありますが、学習している場合は絶対ルールの方が良い場合があります。)という名前の基本クラスを作成できないということになりObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
ます。私が必要とする個別の機能は、それ自体のクラスになければならず、その機能を必要とする他のクラスは、そのクラスから継承するのではなく、そのクラスに依存することができます。
単純化しすぎるリスクがありますが、それは構成です。複数のクラスを組み合わせて連携させます。そして、その習慣を形成すると、継承を使用するよりもはるかに柔軟で、保守可能で、テスト可能であることがわかります。
2つのクラスの間にis-a関係がある場合(例:dogは犬)、継承に進みます。
一方、あなたが持っている場合がある、またはいくつかの2クラス間の形容詞の関係(学生がコースを持っている)、または(教員研究コース)、あなたは、組成物を選びました。
これを理解する簡単な方法は、クラスのオブジェクトがその親クラスと同じインターフェースを持つ必要があるときに継承を使用して、それによって親クラスのオブジェクトとして処理できるようにすることです(アップキャスト) 。さらに、派生クラスオブジェクトの関数呼び出しはコード内のどこでも同じですが、呼び出す特定のメソッドは実行時に決定されます(つまり、低レベルの実装が異なり、高レベルのインターフェイスは同じです)。
新しいクラスが同じインターフェースを持つ必要がない場合、つまり、そのクラスのユーザーが知る必要のないクラスの実装の特定の側面を隠したい場合は、コンポジションを使用する必要があります。組成物は、より多くの支援の方法であるので、カプセル化、継承をサポートすることを意味している間(すなわち、隠蔽実装)抽象化(すなわち、この場合には、何かの簡略化された表現を提供同じ異なる内部構造を有するタイプの範囲に対するインターフェイス)。
サブタイピングは、不変条件を列挙できる場合に適切でより強力です。それ以外の場合は、拡張性のために関数構成を使用します。
@Pavelに同意します。彼が言ったとき、作曲の場所と継承の場所があります。
あなたの答えがこれらの質問のいずれかに対して肯定的である場合は、継承を使用する必要があると思います。
ただし、意図が純粋にコードの再利用である場合は、構成のほうがデザインを選択する可能性が高くなります。
継承は、コードを再利用するための非常に強力なメカニズムです。しかし、適切に使用する必要があります。サブクラスが親クラスのサブタイプでもある場合、継承は正しく使用されていると言えます。上述したように、ここではリスコフ代替原則が重要なポイントです。
サブクラスはサブタイプと同じではありません。サブタイプではないサブクラスを作成する場合があります(これは、コンポジションを使用する必要がある場合です)。サブタイプとは何かを理解するために、タイプとは何かを説明します。
数値5が整数型であると言うとき、5は可能な値のセットに属していることを示しています(例として、Javaプリミティブ型の可能な値を参照してください)。また、加算や減算などの値に対して実行できる有効なメソッドのセットがあることも述べています。最後に、常に満たされるプロパティのセットがあることを示します。たとえば、値3と5を追加すると、結果として8になります。
別の例として、抽象データ型、整数のセット、整数のリストについて考えてみましょう。それらが保持できる値は整数に制限されています。どちらもadd(newValue)やsize()などの一連のメソッドをサポートしています。そして、それらは両方とも異なるプロパティ(クラス不変)を持ち、Setsは重複を許可しませんが、Listは重複を許可します(もちろん、それらは両方とも満たす他のプロパティがあります)。
サブタイプもタイプであり、親タイプ(またはスーパータイプ)と呼ばれる別のタイプと関係があります。サブタイプは、親タイプの機能(値、メソッド、およびプロパティ)を満たしている必要があります。この関係は、スーパータイプが期待されるあらゆるコンテキストで、実行の動作に影響を与えることなく、サブタイプで置き換えることができることを意味します。私が言っていることを例示するためにいくつかのコードを見に行きましょう。整数のリストを(ある種の疑似言語で)作成するとします。
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」関係の場合は継承を使用し、「has-a」の場合は構成を使用する必要があります。それでも、多くの複雑さを排除するので、常に作曲に傾倒すべきだと感じます。
組成物が好まれていますが、私はプロの強調表示したいの継承との短所構成を。
継承の長所:
論理的な「IS A」関係を確立します。場合は車やトラックがの2種類があり車両(基底クラス)、子クラスは、ISの基本クラス。
すなわち
車は乗り物
トラックは乗り物
継承を使用すると、機能を定義/変更/拡張できます
構成の短所:
たとえば、 CarにVehicleが含まれていて、Vehicleで定義されているCarの価格を取得する必要がある場合、コードは次のようになります。
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
多くの人々が言ったように、私はまずチェックから始めます-「is-a」関係があるかどうか。それが存在する場合、私は通常以下をチェックします:
基本クラスをインスタンス化できるかどうか。つまり、基本クラスを非抽象にすることができるかどうか。それが非抽象的であることができるならば、私は通常作曲を好む
例1.会計士は従業員です。ただし、Employeeオブジェクトはインスタンス化できるため、継承は使用しません。
例2. Book は、 SellingItemです。SellingItemはインスタンス化できません-それは抽象的な概念です。したがって、私はinheritacneを使用します。SellingItemは、抽象基本クラス(または C#のインターフェース)です。
このアプローチについてどう思いますか?
継承を使用する主な理由は、構成の形式としてではありません-ポリモーフィックな動作を得ることができるようにするためです。ポリモーフィズムが必要ない場合は、継承を使用しないでください。
@MatthieuM。/software/12439/code-smell-inheritance-abuse/12448#comment303759_12448で言う
継承の問題は、2つの直交する目的に使用できることです。
インターフェース(ポリモーフィズム用)
実装(コードの再利用)
参照
継承で発生する可能性のあるダイヤモンドの問題について誰も言及していません。
一見すると、クラスBとCがAを継承し、両方がメソッドXをオーバーライドし、4番目のクラスDがBとCの両方を継承し、Xをオーバーライドしない場合、どのXDの実装が使用されることになっていますか?
ウィキペディアは、この質問で議論されているトピックの素晴らしい概要を提供しています。