オブジェクト指向システムでコードを最適に拡張、強化、再利用する方法については、2つの考え方があります。
継承:サブクラスを作成して、クラスの機能を拡張します。サブクラスのスーパークラスメンバーをオーバーライドして、新しい機能を提供します。スーパークラスが特定のインターフェースを必要とするが、その実装については不可知である場合、メソッドを抽象/仮想にして、サブクラスを「空欄に埋める」ようにします。
集約:他のクラスを取得し、それらを新しいクラスに結合することにより、新しい機能を作成します。他のコードとの相互運用性のために、この新しいクラスに共通のインターフェースを接続します。
それぞれのメリット、コスト、および結果は何ですか?他の選択肢はありますか?
この議論は定期的に行われますが、Stack Overflowでまだ質問されているとは思いません(関連する議論はありますが)。また、Googleの優れた結果が驚くほど不足しています。
どちらが最善かは問題ではなく、いつ何を使用するかです。
「通常」の場合、継承または集約が必要かどうかを確認するには、簡単な質問で十分です。
ただし、大きな灰色の領域があります。したがって、他のいくつかのトリックが必要です。
それを短くする。インターフェイスの一部が使用されていないか、非論理的な状況を回避するために変更する必要がある場合は、集計を使用する必要があります。主要な変更なしでほとんどすべての機能が必要な場合にのみ、継承を使用する必要があります。疑わしい場合は、集計を使用します。
他の可能性として、元のクラスの機能の一部を必要とするクラスがある場合、元のクラスをルートクラスとサブクラスに分割します。そして、新しいクラスにルートクラスを継承させます。ただし、非論理的な分離を作成するのではなく、これに注意する必要があります。
例を追加しましょう。 「Eat」、「Walk」、「Bark」、「Play」というメソッドを持つクラス「Dog」があります。
class Dog
Eat;
Walk;
Bark;
Play;
end;
ここで、クラス「Cat」が必要です。これには、「Eat」、「Walk」、「Purr」、および「Play」が必要です。だから、最初に犬からそれを拡張してみてください。
class Cat is Dog
Purr;
end;
見た目は大丈夫ですが、待ってください。この猫はBarえることができます(猫愛好家は私を殺します)。そして、barえている猫は宇宙の原則に違反しています。したがって、何もしないようにBarkメソッドをオーバーライドする必要があります。
class Cat is Dog
Purr;
Bark = null;
end;
OK、これは機能しますが、悪臭がします。集計を試してみましょう:
class Cat
has Dog;
Eat = Dog.Eat;
Walk = Dog.Walk;
Play = Dog.Play;
Purr;
end;
はい、これはいいです。この猫はもうbarえず、静かでもありません。しかし、まだそれは外に望んでいる内部犬を持っています。解決策3番を試してみましょう。
class Pet
Eat;
Walk;
Play;
end;
class Dog is Pet
Bark;
end;
class Cat is Pet
Purr;
end;
これはずっときれいです。飼い犬はいません。猫と犬は同じレベルです。他のペットを導入して、モデルを拡張することもできます。魚、または歩かないものでない限り。その場合、再度リファクタリングする必要があります。しかし、それはまた別のことです。
差は通常、「is a」と「has a」の差として表されます。継承、 "is a"関係は Liskov Substitution Principle でうまくまとめられています。 "has a"関係である集約とは、集約オブジェクトが集約オブジェクトの1つを持っていることを示しています。
さらに区別が存在します。C++のプライベート継承は、「の観点から実装されている」関係を示し、(非公開の)メンバーオブジェクトの集約によってもモデル化できます。
私の最も一般的な議論は次のとおりです。
オブジェクト指向システムでは、クラスには2つの部分があります。
そのinterface:オブジェクトの「公開顔」。これは、世界中に発表する一連の機能です。多くの言語では、セットは「クラス」に適切に定義されています。通常、これらはオブジェクトのメソッドシグネチャですが、言語によって多少異なります。
その実装:「舞台裏」は、オブジェクトがインターフェイスを満たし機能を提供するために行うことです。これは通常、オブジェクトのコードとメンバーデータです。
OOPの基本原則の1つは、実装がクラス内でカプセル化(つまり:隠し)であるということです。部外者が見るべき唯一のものはインターフェースです。
サブクラスがサブクラスから継承する場合、通常はboth実装とインターフェイスを継承します。これは、つまり、両方をクラスの制約として受け入れるためにforcedであることを意味します。
集約では、実装またはインターフェースのいずれか、または両方を選択できますが、どちらかに強制されることはありません。オブジェクトの機能は、オブジェクト自体に任されています。それは好きなように他のオブジェクトに延期することができますが、それ自体の最終的な責任があります。私の経験では、これはより柔軟なシステムにつながります。つまり、変更が簡単なシステムです。
そのため、オブジェクト指向ソフトウェアを開発するときは常に、継承よりも集約を好んでいます。
"Is a" vs "Has a":どちらが良いですか? に答えました。
基本的に、私は他の人々に同意します:継承を使用するのは、派生クラスが本当にがである場合だけであり、それが単に同じデータが含まれています。継承とは、サブクラスがメソッドとデータを取得することを意味することに注意してください。
派生クラスがスーパークラスのすべてのメソッドを持っているのは理にかなっていますか?または、これらのメソッドを派生クラスで無視することを静かに約束しますか?または、スーパークラスのメソッドをオーバーライドして、何も操作せず、だれも誤ってそれらを呼び出さないようにしますか?または、APIドキュメント生成ツールにヒントを与えて、ドキュメントからメソッドを省略しますか?
その場合、集約がより良い選択であるという強力な手がかりです。
この質問と関連する質問に対する「is-a vs. has-a。概念的に異なる」という回答がたくさんあります。
私の経験で私が見つけた1つのことは、関係が「is-a」であるか「has-a」であるかを判断しようとすると失敗することです。オブジェクトのその決定を今すぐ正しく行うことができたとしても、要件の変更は、将来のある時点でおそらく間違っていることを意味します。
私が見つけたもう一つのことは、継承階層の周りにたくさんのコードが書かれていると、継承から集約に変換するのが難しいということです非常にです。スーパークラスからインターフェイスに切り替えるだけで、システム内のほぼすべてのサブクラスを変更することになります。
そして、この投稿の他の場所で述べたように、集約は継承よりも柔軟性が低い傾向があります。
そのため、どちらかを選択する必要がある場合は常に、継承に対する議論の完璧な嵐があります。
したがって、強いis-a関係があるように見える場合でも、集約を選択する傾向があります。
私はこれを元の質問に対するコメントにしたかったのですが、300文字のバイト[; <)です。
私たちは注意する必要があると思います。まず、質問で作成された2つのかなり具体的な例よりも多くのフレーバーがあります。
また、目的を機器と混同しないことが重要であることをお勧めします。選択した手法または方法論が主な目的の達成をサポートすることを確認したいのですが、文脈から外れて、どの技術が最良かという議論は非常に有用ではありません。さまざまなアプローチの落とし穴と、それらの明確なスイートスポットを知るのに役立ちます。
たとえば、何を達成するために何を始め、何から始めることができますか、そして制約は何ですか?
コンポーネントフレームワークを作成していますか?インターフェイスはプログラミングシステムの実装から分離可能ですか、それとも異なる種類のテクノロジを使用した実践によって実現されていますか?インターフェースの継承構造(ある場合)を、それらを実装するクラスの継承構造から分離できますか?実装が提供するインターフェースに依存するコードから実装のクラス構造を隠すことは重要ですか?同時に使用可能な複数の実装がありますか?または、メンテナンスとエンハンストメモリの結果として、時間の経過とともに変動が増えますか?ツールや方法論に固執する前に、これ以上のことを考慮する必要があります。
最後に、抽象化の区別とその考え方を(is-aとhas-aのように)OOテクノロジーの異なる機能にロックすることは重要ですか?概念構造を一貫性のあるものにし、管理しやすいようにしますが、そのことや、最終的にはゆがみに屈しないようにするのが賢明です。ナレーション(他の人が何が起きているのかわかるように))[プログラムの特定の部分を説明できるようにするものを探しますが、大きな勝利があったときに優雅さを求めることがあります。常に最良のアイデアとは限りません。]
私はインターフェイスの純粋主義者であり、Javaフレームワークを構築するか、いくつかのCOM実装を編成するかどうかにかかわらず、インターフェイスの純粋主義が適切な種類の問題とアプローチに魅了されます。私はそれを誓っていますが、すべてに近いものではなく、すべてに適しています(インターフェイスの純粋主義に対する深刻な反例を提供するように見えるプロジェクトがいくつかあるので、どう対処するかを見るのは興味深いでしょう)。
質問は通常 Composition vs. Inheritance と表現され、以前にここで質問されました。
それはどちらかまたは両方の議論ではないと思います。それだけです:
両方にそれぞれの場所がありますが、継承は危険です。
もちろん、クラスのShape 'having-a' PointクラスとSquareクラスを持つことは意味がありません。ここでは、継承が原因です。
拡張性のあるものを設計しようとするとき、人々は最初に継承について考える傾向があります。それが問題です。
両方の候補者が資格を得ると好意が生じます。 AとBはオプションであり、Aを支持します。その理由は、合成が一般化よりも拡張/柔軟性の可能性を高めるためです。この拡張性/柔軟性は、主にランタイム/動的な柔軟性を指します。
利点はすぐにはわかりません。メリットを確認するには、次の予期しない変更要求を待つ必要があります。そのため、ほとんどの場合、一般化にこだわる人は、構図を採用する人と比較すると失敗します(後述の明らかなケースを除く)。したがって、ルール。学習の観点から、依存性注入を正常に実装できる場合は、どの注入を優先するか、いつ行うかを知っておく必要があります。ルールは、意思決定にも役立ちます。よくわからない場合は、構成を選択してください。
概要:構成:いくつかの小さなものを大きなものに差し込むだけで結合が減少し、大きなオブジェクトは小さなオブジェクトをコールバックします。一般化:APIの観点から、メソッドをオーバーライドできることを定義することは、メソッドを呼び出すことができることを定義することよりも強力です。 (一般化が勝ったときのごく少数の機会)。そして、コンポジションでは、大きなクラスの代わりにインターフェイスから継承も使用していることを決して忘れないでください
これらが適用される可能性のある部分について説明します。ゲームシナリオでの両方の例を次に示します。さまざまな種類の兵士がいるゲームがあるとします。各兵士は、さまざまなものを収納できるナップサックを持つことができます。
ここに継承?マリン、グリーンベレー帽、狙撃兵がいます。これらは兵士のタイプです。そのため、派生クラスとして海兵隊員、グリーンベレー帽、狙撃兵を持つ基本クラスの兵士がいます。
アグリゲーションはここですか?ナップザックには手(弾、銃(異なるタイプ)、ナイフ、メディキットなどを入れることができます。防弾チョッキは攻撃されたときに鎧として機能し、彼の負傷は一定の割合まで減少します。兵士クラスには、防弾チョッキクラスのオブジェクトと、これらのアイテムへの参照を含むナップザッククラスが含まれています。
両方のアプローチは、さまざまな問題を解決するために使用されます。 1つのクラスから継承する場合、2つ以上のクラスを集約する必要は必ずしもありません。
そのクラスは封印されているか、そうでなければインターセプトする必要がある非仮想メンバーがあるため、単一のクラスを集約する必要がある場合があります。これにサブスクライブできるインターフェイスがあり、かなりうまく機能します。