web-dev-qa-db-ja.com

「継承よりも構成を優先する」-署名の変更を防ぐ唯一の理由は何ですか?

このページ 次の引数を使用して、継承よりも構成を推奨します(私の言葉で言い換えます):

スーパークラス(サブクラスでオーバーライドされていない)のメソッドのシグネチャを変更すると、継承を使用するときに多くの場所で追加の変更が発生します。ただし、Compositionを使用する場合、必要な追加の変更は1つの場所(サブクラス)のみです。

それが本当に継承よりも構成を支持する唯一の理由ですか?その場合は、サブクラスが実装を変更しない(つまり、サブクラスにダミーのオーバーライドを配置する)場合でも、スーパークラスのすべてのメソッドをオーバーライドすることを推奨するコーディングスタイルを適用することで、この問題を簡単に軽減できます。ここで何か不足していますか?

13
Utku

私は類推が好きなので、ここに1つあります。VCRプレーヤーが組み込まれているテレビを見たことがありますか。 VCRとDVDプレーヤーのあるものはどうですか?または、Blu-ray、DVD、VCRを搭載しているもの。ああ、今はみんながストリーミングしているので、新しいテレビを設計する必要があります...

ほとんどの場合、テレビを所有している場合、上記のようなテレビはありません。おそらくそれらのほとんどは存在さえしていません。新しいタイプの入力デバイスがあるたびに新しいセットを必要としないように、多くの入力を持つモニターまたはセットを持っている可能性があります。

継承は、VCRが組み込まれたテレビのようなものです。一緒に使用したい3つの異なるタイプの動作があり、それぞれに実装のための2つのオプションがある場合、それらすべてを表すには8つの異なるクラスが必要です。オプションを追加すると、数字が爆発します。代わりにコンポジションを使用すると、その組み合わせの問題が回避され、デザインの拡張性が高まる傾向があります。

27
JimmyJames

いいえそうではありません。私の経験では、継承に関する構成は、ソフトウェアをobjectsの束として考えることの結果です。behaviours互いに話し合う/objects提供するservicesclassesおよびdataの代わりに相互に。

このようにして、サービスを提供するいくつかのオブジェクトがあります。それらをビルディングブロックとして使用して(実際にはレゴに非常によく似ています)、他の動作を有効にします(たとえば、UserRegistrationServiceによって提供されるサービスを使用しますEmailerServiceおよびUserRepository)。

このアプローチでより大きなシステムを構築するとき、私は自然に継承を使用することはほとんどありません。結局のところ、継承はveryの強力なツールであり、注意して適切な場所でのみ使用する必要があります。

4
marstato

「継承よりも構成を優先する」はjust良いヒューリスティックです

これは普遍的な規則ではないため、コンテキストを考慮する必要があります。構成を行うことができるときに継承を使用しないことを意味するものと解釈しないでください。その場合は、継承を禁止して修正します。

この点については、この記事で明らかにしたいと思います。

作曲のメリットを自ら弁護しようとはしません。トピックから外れていると思います。代わりに、コンポジションを使用したほうがよい継承を開発者が検討する可能性があるいくつかの状況について説明します。その点、継承にはそれなりのメリットがありますが、これについても取り上げません。


車両例

説明の目的で、ばかげた方法で物事を試みる開発者について書いています

いくつかのOOPコースで使用されている古典的な例の変形を見てみましょう... Vehicleクラスがあり、次にCarAirplaneBalloonおよびShip

:この例を根拠づける必要がある場合は、これらがビデオゲームのオブジェクトの一種であると考えてください。

その場合、CarAirplaneには共通のコードが含まれる可能性があります。どちらも陸上で回転するためです。開発者は、継承チェーンに中間クラスを作成することを検討する場合があります。しかし、実際にはAirplaneBalloonの間にもいくつかの共有コードがあります。そのために、継承チェーンに別の中間クラスを作成することを検討できます。

したがって、開発者は多重継承を探します。開発者が多重継承を求めている時点で、設計はすでに間違っています。

この動作をインターフェースと構成としてモデル化することをお勧めします。これにより、複数のクラス継承に遭遇することなく再利用できます。たとえば、開発者がFlyingVehiculeクラスを作成した場合。彼らはAirplaneFlyingVehicule(クラス継承)であると言っていますが、代わりにAirplaneFlyingコンポーネント(構成)とAirplaneIFlyingVehicule(インターフェース継承)です。

必要に応じて、インターフェースを使用して、(インターフェースの)多重継承を行うことができます。さらに、特定の実装に結合しているわけではありません。コードの再利用性とテスト性の向上。

継承はポリモーフィズムのツールであることを忘れないでください。さらに、ポリモーフィズムは再利用性のためのツールです。コンポジションを使用してコードの再利用性を向上させることができる場合は、そうします。コンポジションが再利用性に優れているかどうかわからない場合は、「継承よりコンポジションを優先する」が適切なヒューリスティックになります。

Amphibiousに言及せずにすべて。

実際、私たちは地面から離れる必要がないかもしれません。スティーブンハーンは、彼の記事「継承よりも好意的な構成」 パート1 および パート2のより雄弁な例を挙げています。


代替可能性とカプセル化

ABを継承または作成しますか?

ABの特殊化であり、 Liskov置換の原則 を満たす必要がある場合、継承は実行可能であり、さらに望ましいです。 ABの有効な置換ではない状況がある場合は、継承を使用しないでください。

防御的なプログラミングの形式としての合成に興味があるかもしれません、派生クラスを防御するため。特に、他のさまざまな目的でBを使い始めると、その目的に適するように変更または拡張する必要が生じる場合があります。 BAで無効な状態になる可能性のあるメソッドを公開するリスクがある場合は、継承ではなくコンポジションを使用する必要があります。私たちがBAの両方の作成者である場合でも、心配する必要は1つ少ないため、合成によりBの再利用が容易になります。

Bに機能がある場合、Aには不要であると主張することもできます(これらの機能がAの無効な状態を引き起こす可能性があるかどうかもわかりません)現在の実装または将来的には、継承の代わりに合成を使用することをお勧めします。

コンポジションには、切り替えの実装を可能にし、モックを緩和するという利点もあります。

:置換が有効であるにもかかわらず、コンポジションを使用したい場合があります。substitutabilityは、インターフェイスまたは抽象クラス(別のトピックの場合に使用するクラス)を使用してアーカイブし、実際の実装の依存性注入を伴う構成を使用します。

最後に、もちろん、継承によって親クラスのカプセル化が解除されるため、合成親クラスを保護するためにを使用する必要があるという議論があります。

継承はサブクラスをその親の実装の詳細に公開します。「継承はカプセル化を壊す」としばしば言われます

-設計パターン:再利用可能なオブジェクト指向ソフトウェアの要素、4つのギャング

まあ、それは不十分に設計された親クラスです。そのため、次のことを行う必要があります。

継承のための設計、またはそれを禁止します。

-効果的なJava、Josh Bloch


ヨーヨー問題

合成が役立つもう1つのケースは Yo-Yo問題 です。これはウィキペディアからの引用です:

ソフトウェア開発において、ヨーヨー問題は、継承グラフが非常に長く複雑であり、プログラマーが多くの異なるクラス定義間を切り替え続けなければならないプログラムをプログラマが読み取って理解する必要があるときに発生するアンチパターンです。プログラムの制御フロー。

たとえば、次のように解決できます。クラスCはクラスBを継承しません。代わりに、クラスCにはタイプAのメンバーがあり、タイプBのオブジェクトである場合とそうでない場合があります。この方法では、Bの実装の詳細に対してプログラミングするのではなく、Aのインターフェイスが提供する規約に対してプログラミングします。


カウンターの例

多くのフレームワークは、コンポジションより継承を優先します(これは、これまで議論してきたものとは逆です)。開発者がこれを行うのは、基本クラスに多くの作業を加えたためです。構成で実装するとクライアントコードのサイズが大きくなるためです。時々これは言語の制限が原因です。

たとえば、PHP ORMフレームワークは、マジックメソッドを使用して、オブジェクトに実際のプロパティがあるかのようにコードを記述できるようにする基本クラスを作成します。代わりに、マジックメソッドによって処理されるコードは、データベース、特定の要求されたフィールドをクエリし(おそらく、将来の要求のためにキャッシュします)、それをコンポジションで実行します。クライアントが各フィールドのプロパティを作成するか、マジックメソッドコードのいくつかのバージョンを書く必要があります。

補遺:ORMオブジェクトを拡張できる他の方法がいくつかあります。したがって、この場合は継承が必要だとは思いません。もっと安いです。

別の例として、ビデオゲームエンジンは、ターゲットプラットフォームに応じてネイティブコードを使用して3Dレンダリングとイベント処理を行う基本クラスを作成する場合があります。このコードは複雑でプラットフォーム固有です。エンジンの開発者ユーザーがこのコードを処理することは、費用がかかり、エラーが発生しやすくなります。実際、それがエンジンを使用する理由の一部です。

さらに、3Dレンダリング部分がなければ、これがいくつのウィジェットフレームワークが機能するかです。これにより、OSメッセージの処理について心配する必要がなくなります。実際、多くの言語では、なんらかの形式のネイティブビッディングなしにそのようなコードを書くことはできません。さらに、これを行うと、移植性が低下します。代わりに、継承を使用して、開発者が互換性を(過度に)壊さない限り、将来サポートする新しいプラットフォームにコードを簡単に移植できるようになります。

さらに、多くの場合、いくつかのメソッドをオーバーライドし、その他はすべてデフォルトの実装のままにしておきたいことを考慮します。コンポジションを使用している場合は、ラップされたオブジェクトに委譲するだけの場合でも、これらすべてのメソッドを作成する必要があります。

この議論によって、継承よりも保守性が最も悪くなる可能性がある点があります(基本クラスが複雑すぎる場合)。しかし、継承の保守性は、構成のそれ(継承ツリーが複雑すぎる場合)よりも劣ることがあることを覚えておいてください。これは、私がヨーヨー問題で言及していることです。

提示された例では、開発者が継承によって生成されたコードを他のプロジェクトで再利用することはめったにありません。これにより、合成ではなく継承を使用することによる再利用性の低下が緩和されます。さらに、継承を使用することにより、フレームワーク開発者は、使いやすく、コードを簡単に発見できる多くの機能を提供できます。


最終的な考え

ご覧のように、コンポジションは、常にではなく、状況によっては継承よりも優れています。決定を下すには、コンテキストとそれに関連するさまざまな要因(再利用性、保守性、テスト容易性など)を考慮することが重要です。最初のポイントに戻ります:「継承よりも構成を優先する」はjust優れたヒューリスティックです。

また、私が説明している状況の多くは、特性またはミックスインである程度解決できることに気付くかもしれません。残念なことに、これらは優れた言語のリストでは一般的な機能ではなく、通常はパフォーマンスコストが伴います。ありがたいことに、彼らの人気のいとこ拡張メソッドとデフォルト実装はいくつかの状況を緩和します。

最近の投稿で、インターフェイスの利点のいくつかについて C#でUI、ビジネス、データアクセス間のインターフェイスが必要な理由 について説明します。それは分離に役立ち、再利用性とテスト容易性を容易にします。

4
Theraot

通常、Composition-Over-Inheritanceの原動力となるアイデアは、設計の柔軟性を高めることであり、変更を伝播するために変更する必要のあるコードの量を削減することではありません(疑問です)。

wikipedia の例は、彼のアプローチの力のより良い例を示しています。

「構成要素」ごとに基本インターフェースを定義すると、継承だけでは到達するのが難しいさまざまな「構成オブジェクト」を簡単に作成できます。

3
lbenini

「継承よりも構成を優先する」という行は、正しい方向へのナッジに他なりません。それはあなたをあなた自身の愚かさから救うつもりはなく、実際にあなたに物事をさらに悪化させる機会を与えます。それは、ここには多くの力があるからです。

これさえ言われる必要がある主な理由は、プログラマーが怠惰だからです。とても怠惰な彼らは、キーボード入力を減らすことを意味するあらゆる解決策を取ります。それが相続の主なセールスポイントです。今日はタイプを少なくし、明日は他の誰かが台無しになっている。それで、私は家に帰りたいです。

翌日、ボスは私が作文を使用すると主張します。理由は説明しませんが、主張して​​います。そこで、継承元のインスタンスを取得し、継承元と同じインターフェイスを公開し、すべての作業を継承元のものに委任することで、そのインターフェイスを実装します。リファクタリングツールを使用して実行できたのは頭が痛いタイプの入力であり、私には無意味に思えますが、上司がそれを望んでいたので、私はそれを行いました。

翌日、これが欲しかったかどうか尋ねます。上司は何と言いますか?

継承元の状態を(実行時に)動的に変更できるようにする必要がある場合を除き(状態パターンを参照)、これは非常に時間の浪費でした。上司はどのようにしてそれを継承し、継承よりも構成に賛成することができますか?

これは、メソッドシグネチャの変更による破損を防ぐための処理を行わないことに加えて、構成の最大の利点を完全に逃します。継承とは異なり、異なるインターフェースを自由に作成できます。このレベルでの使用方法に適したもう1つ。あなたが知っている、抽象化!

継承を合成と委任に自動的に置き換えることにはいくつかの小さな利点があります(そしていくつかの欠点)が、このプロセス中に脳をオフにしておくことは大きな機会を逃します。

インダイレクションは非常に強力です。賢く使ってください。

2
candied_orange

ここに別の理由があります:多くのOO言語(私はC++、C#またはJavaここにあります)のようなものを考えています)では、クラスを変更する方法はありません作成したオブジェクトの.

従業員データベースを作成するとします。抽象ベースのEmployeeクラスがあり、エンジニア、セキュリティガード、トラックドライバーなど、特定の役割の新しいクラスの派生を開始します。各クラスには、その役割に特化した動作があります。すべてが良いです。

その後、ある日エンジニアがエンジニアリングマネージャーに昇格します。既存のエンジニアを再分類することはできません。代わりに、エンジニアリングマネージャーを作成し、エンジニアからすべてのデータをコピーし、データベースからエンジニアを削除して、新しいエンジニアリングマネージャーを追加する関数を作成する必要があります。そして、考えられる役割の変更ごとにこのような関数を作成する必要があります。

さらに悪いことに、トラック運転手が長期の病気休暇を取り、警備員がトラック運転のパートタイムでの記入を申し出たとします。今、彼らのためだけに新しい警備員とトラック運転手クラスを発明する必要があります。

これにより、具象Employeeクラスを作成し、ジョブロールなどのプロパティを追加できるコンテナーとして扱うことが非常に簡単になります。

0
Simon B

継承は次の2つを提供します。

1)コードの再利用:構成によって簡単に実行できます。そして実際には、カプセル化をよりよく保存するので、構成によってよりよく達成されます。

2)ポリモーフィズム:「インターフェース」(すべてのメソッドが純粋仮想であるクラス)を通じて実現できます。

非インターフェース継承を使用することに対する強い議論は、必要なメソッドの数が多く、サブクラスがそれらの小さなサブセットのみを変更したい場合です。ただし、一般に "single-responsibility" の原則(必要)をサブスクライブすると、インターフェースのフットプリントが非常に小さくなるため、これらのケースはまれです。

すべての親クラスのメソッドをオーバーライドする必要があるコーディングスタイルがあっても、大量のパススルーメソッドを作成することは避けられないので、合成を通じてコードを再利用し、カプセル化の追加のメリットを得ませんか?さらに、別のメソッドを追加してスーパークラスを拡張する場合、コーディングスタイルでは、そのメソッドをすべてのサブクラスに追加する必要があります(そして、違反する可能性が高くなります "interface segregation" =)。この問題は、継承に複数のレベルがあるとすぐに大きくなります。

最後に、多くの言語では、「構成」ベースのオブジェクトのユニットテストは、メンバーオブジェクトをモック/テスト/ダミーオブジェクトで置き換えることにより、「継承」ベースのオブジェクトのユニットテストよりもはるかに簡単です。

0
dlasalle

それが本当に継承よりも構成を支持する唯一の理由ですか?

いいえ

継承よりも構成を優先する理由はたくさんあります。正解は1つではありません。しかし、私は継承よりも構成を優先する別の理由を示します:

継承よりも構成を優先すると、コードが読みやすくなります。これは、複数の人がいる大規模なプロジェクトで作業する場合に非常に重要です。

ここで私はそれについてどう思いますか:私が他の誰かのコードを読んでいるときに、それらがクラスを拡張したことがわかったら、私は尋ねますスーパークラスのどの動作が変更されていますか?

次に、オーバーライドされた関数を探して、コードを調べます。何も表示されない場合は、継承の誤用と分類します。彼らがコードを読んで5分から私(およびチームの他のすべての人)を救うためにさえあれば、彼らは代わりに構成を使用するべきでした!

これが具体例です: in Java JFrameクラスはウィンドウを表します。ウィンドウを作成するには、JFrameのインスタンスを作成し、そのインスタンスで関数を呼び出して追加しますそれにものを表示します。

多くのチュートリアル(公式のものも含む)では、JFrameを拡張して、クラスのインスタンスをJFrameのインスタンスとして扱うことができるようにすることを推奨しています。しかし、imho(そして他の多くの人の意見)は、これはかなり悪い習慣になるということです!代わりに、JFrameのインスタンスを作成し、そのインスタンスで関数を呼び出すだけです。この場合、親クラスのデフォルトの動作を変更しないため、JFrameを拡張する必要はまったくありません。

一方、カスタムペイントを実行する1つの方法は、JPanelクラスを拡張し、paintComponent()関数をオーバーライドすることです。これは継承をうまく利用したものです。コードを調べたところ、JPanelを拡張し、paintComponent()関数をオーバーライドしたことがわかった場合は、変更している動作が正確にわかります。

自分でコードを書くだけの場合は、コンポジションを使用するのと同じくらい簡単に継承を使用できます。しかし、あなたがグループ環境にいて、他の人があなたのコードを読んでいるようなふりをしてください。 継承よりも構成を優先すると、コードが読みやすくなります。

0
Kevin Workman