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


13

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

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

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



1
参照:この$ {blog}
gnat

2
引用は、それが「唯一の」理由であるとは言っていません。
Tulainsコルドバ

3
継承は通常、複雑さ、結合、コード読み取りを追加し、柔軟性を低下させるため、構成はほぼ常に優れています。ただし、注目すべき例外があります。場合によっては、基本クラスが1つでも2つでも問題はありません。そのため、「常に」ではなく「好む」。
マークロジャース

回答:


27

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

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

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


6
90年代後半から2000年代初頭に、VCRおよびDVDプレーヤーを搭載したテレビがたくさんありました。
JAB

@JABと今日販売されている多くの(ほとんどの?)テレビは、ストリーミングをサポートしています。
ケーシー

4
@JABそして、Blurayプレーヤーの購入を支援するためにDVDプレーヤーを販売することはできません
Alexander-

1
@JABそうです。「VCRプレーヤーが組み込まれたテレビを見たことはありますか?」それらが存在することを意味します。
ジミージェームズ

4
@Casey明らかに、この技術に取って代わる別の技術はありませんよね?誰かが新しいテレビを購入せずに他の何かを使用したい場合に備えて、これらのテレビのいずれかが外部デバイスからの入力もサポートすることを望みます。むしろ、そのような入力を欠くテレビを購入しようとする教育を受けたバイヤーはほとんどいません。私の前提は、これらのアプローチが存在しないことではなく、継承が存在しないと主張することでもありません。ポイントは、そのようなアプローチは本質的に構成よりも柔軟性が低いということです。
ジミージェームズ

4

いいえそうではありません。私の経験では、継承を超える組成の束としてあなたのソフトウェアのことを考えた結果であるオブジェクト行動することをお互いに話 /は、オブジェクトの提供サービスを代わりにお互いにクラスおよびデータ

このように、サービスを提供するオブジェクトがいくつかあります。あなたは(例えばA、他の動作を可能にするために(実際にはかなり多くのレゴのような)ブロックを構築するとして、それらを使用UserRegistrationServiceが提供するサービス使用EmailerServiceUserRepositoryを)。

このアプローチでより大きなシステムを構築するとき、私はごくわずかなケースで自然に継承を使用しました。結局のところ、継承は非常に強力なツールであり、適切な場合にのみ注意して使用する必要があります。


ここにJavaの考え方が見えます。OOPは、それに作用する関数とともにパッケージ化されるデータに関するものです-あなたが記述しているものは、「モジュール」と呼ばれる方が適切です。オブジェクトをモジュールとして再利用する場合、継承を避けることは良い考えですが、オブジェクトを使用する好ましい方法としてそれを推奨しているように思われるのは気がかりです。
ブリリアンド

1
@Brilliandあなたが記述するスタイルでどれだけのJavaコードが書かれているか、そしてこれがどれほど恐ろしいことかを知っているなら... SmalltalkはOOPという用語を導入し、メッセージを受信するオブジェクトを中心設計されました。:「コンピューティングは、メッセージを送信することで一様に呼び出すことができるオブジェクトの固有の機能と見なされる必要があります。」データとその上で動作する機能についての言及はありません。申し訳ありませんが、私の意見では、あなたはひどく間違っています。データと関数だけの場合は、喜んで構造体を書き続けることができます。
-marstato

@Tsarは、残念ながら、Javaは静的メソッドをサポートしていますが、Javaカルチャはサポートしていません
k_g

@Brilliand誰がOOPが「約」であるかを気にしますか?重要なのは、それどのように使用できるかと、それらの方法で使用した結果です。
user253751

1
@Tsarとの議論の後:Javaクラスをインスタンス化する必要はありません。実際、UserRegistrationServiceEmailServiceなどのクラスは通常インスタンス化されるべきではありません。関連するデータのないクラスは静的クラスとして最適です質問)。以前のコメントの一部を削除しますが、marsatoの答えに対する当初の批判はそのままです。
ブリリアンド

4

「相続上の構図を優先」であるちょうど良いヒューリスティック

これは普遍的なルールではないため、コンテキストを考慮する必要があります。あなたが作曲をすることができるとき、決して継承を使用しないことを意味すると考えてはいけません。その場合は、継承を禁止することで修正できます。

この投稿に沿ってこの点を明らかにしたいと思います。

作曲のメリットを単独で擁護しようとはしません。私はトピックを検討します。代わりに、開発者がコンポジションを使用したほうがよい継承を検討する可能性がある状況について説明します。その点については、継承には独自のメリットがあります。


車両の例

私は物語の目的のために物事を愚かな方法でしようとする開発者について書いています

私たちはいくつかのOOPのコースが使用する古典例の変形のために行ってみよう...私たちは持っているVehicleし、我々は派生クラスをCarAirplaneBalloonShip

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

そしてCarAirplaneそれらは両方とも陸上で車輪の上を転がることができるので、いくつかの一般的なコードを持っているかもしれません。開発者は、そのための継承チェーンに中間クラスを作成することを検討できます。しかし、実際にはとの間にいくつかの共有コードもAirplaneありBalloonます。そのために、継承チェーンに別の中間クラスを作成することを検討できます。

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

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

必要に応じて、インターフェイスを使用して、(インターフェイスの)複数の継承を持つことができます。また、特定の実装に結合していません。コードの再利用性とテスト容易性の向上。

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

言及せずにすべてのことAmphibious

実際、私たちは地面から落ちるものを必要としないかもしれません。Stephen Hurnの記事「継承よりもお気に入りの構成」のパート1パート2に、より雄弁な例があります。


代替性とカプセル化

A継承または構成する必要がありBますか?

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

派生クラスを守るために、防衛的プログラミングの一形態としての合成に興味があるかもしれません。特に、いったんB他の異なる目的で使用を開始すると、それらの目的により適したものに変更または拡張する圧力がかかる場合があります。B無効な状態になる可能性のあるメソッドを公開するリスクがある場合A、継承の代わりに構成を使用する必要があります。私たちがとの両方の作者であるとしてもBA心配することは1つ少ないので、合成によりの再利用が容易になりBます。

必要のない機能がBあるA場合(A現在の実装または将来のいずれかでそれらの機能が無効な状態になる可能性があるかどうかわからない場合)、コンポジションを使用することをお勧めします継承の代わりに。

合成には、実装の切り替えを許可し、モッキングを緩和するという利点もあります。

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

最後に、もちろん、継承は親クラスのカプセル化を破壊するため、親クラスを保護するために構成使用する必要があるという議論があります。

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

-デザインパターン:再利用可能なオブジェクト指向ソフトウェアの要素、Gang of Four

まあ、それは不十分に設計された親クラスです。それがあなたがすべき理由です:

継承を設計するか、禁止します。

-効果的なJava、Josh Bloch


ヨーヨー問題

合成が役立つ別のケースは、ヨーヨーの問題です。これはウィキペディアからの引用です:

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

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


カウンターの例

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

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

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

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

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

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

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

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


最終的な考え

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

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

最近の投稿で、C#でUI、ビジネス、データアクセスの間にインターフェイスが必要な理由について、インターフェイスの利点について説明しています。分離を助け、再利用性とテスト容易性を容易にします。興味があるかもしれません。


ええと... .Netフレームワークは、いくつかのタイプの継承されたストリームを使用して、ストリーム関連の作業を行います。それは信じられないほどうまく機能し、使用と拡張が非常に簡単です。あなたの例では、...非常に良いものではありません
T.サール-復活モニカ

1
@TSar「非常に使いやすく、拡張しやすい」標準出力ストリームをデバッグすることを試みたことがありますか?それは彼らのストリームを拡張しようとする私の唯一の経験です。簡単ではありませんでした。クラスのすべてのメソッドを明示的にオーバーライドすることになったのは悪夢でした。非常に繰り返します。そして、.NETソースコードを見ると、すべてを明示的にオーバーライドするのがほとんどの方法です。そのため、これほど簡単に拡張できるとは思いません。
jpmc26

ストリームからの構造体の読み取りと書き込みは、無料の関数またはマルチメソッドを使用する方が適切です。
user253751

@ jpmc26ご不便をおかけして申し訳ありません。ただし、ストリーム関連のものをさまざまな用途に使用したり実装したりすることについては、自分で問題はありませんでした。
T.サー-

3

通常、Composition-Over-Inheritanceを推進する考え方は、設計の柔軟性を高めることであり、変更を伝達するために変更する必要があるコードの量を減らすことではありません(疑わしいと思います)。

上の例 ウィキペディアは、彼のアプローチの力のより良い例を与えます:

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


したがって、このセクションでは、構成の例を示しますよね?それは記事では明らかではないので、私は尋ねています。
-Utku

1
はい、「継承に対する構成」は、インターフェイスがないことを意味するものではありません。Playerオブジェクトを見ると、階層にうまく収まることがわかりますが、(残酷さを理由に)コードは継承から来ているわけではありません。しかし、仕事をするいくつかのクラスの構成から
lbenini

2

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

これを言わなければならない主な理由は、プログラマーが怠け者だからです。だから怠け者は、キーボード入力を減らすことを意味する解決策を採用します。それが相続の主なセールスポイントです。私は今日あまり入力せず、明日誰か他の人が台無しになります。だから、私は家に帰りたいです。

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

翌日、これが望んでいたものかどうか尋ねます。上司は何と言いますか?

(実行時に)継承されているものを動的に(状態パターンを参照)変更できる必要がある場合を除き、これは膨大な時間の浪費でした。上司はどうやってそれを言って、それでも相続よりも構成を好むのでしょうか?

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

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

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


0

もう1つの理由は次のとおりです。多くのOO言語(ここではC ++、C#、Javaなどの言語を考えています)では、作成したオブジェクトのクラスを変更する方法はありません。

従業員データベースを作成しているとします。抽象ベースのEmployeeクラスがあり、特定の役割(エンジニア、警備員、トラック運転手など)の新しいクラスの派生を開始します。各クラスには、そのロールに特化した動作があります。すべてが良いです。

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

さらに悪いことに、トラック運転手が長期の病気休暇を取り、警備員がトラックの運転をパートタイムで満たすことを申し出ているとします。

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


または、ロールのリストへのポインタを使用してEmployeeクラスを作成し、実際の各ジョブがロールを拡張します。あなたの例は、あまり多くの作業をすることなく、非常に優れた賢明な方法で継承を使用するようにすることができます。
T.サー-

0

継承には次の2つがあります。

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

2)多態性:「インターフェース」(すべてのメソッドが純粋仮想であるクラス)を介して実現できます。

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

すべての親クラスメソッドのオーバーライドを必要とするコーディングスタイルを使用しても、とにかく多数のパススルーメソッドを記述することは避けられません。さらに、別のメソッドを追加してスーパークラスを拡張する場合、コーディングスタイルでは、そのメソッドをすべてのサブクラスに追加する必要があります(そして、「インターフェイス分離」に違反する可能性が十分にあります)。この問題は、継承に複数のレベルがあるとすぐに大きくなります。

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


0

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

いや。

継承よりも合成を優先する多くの理由があります。正解は1つではありません。ただし、継承よりも構成を優先する別の理由をミックスに追加します。

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

考え方は次のとおりです。他の人のコードを読んでいるときに、クラスが拡張されていることがわかったら、スーパークラスのどのような動作が変わっているのかを尋ねます。

そして、オーバーライドされた関数を探して、コードを調べます。表示されない場合は、継承の誤用として分類します。私(およびチームの他のすべての人)がコードを5分間読むのを防ぐためだけに、代わりに構成を使用する必要がありました。

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

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

一方、カスタムペイントを実行する1つの方法は、JPanelクラスを拡張し、paintComponent()関数をオーバーライドすることです。これは継承の素晴らしい使用法です。あなたのコードを調べて、あなたJPanelpaintComponent()関数を拡張してオーバーライドしたことがわかったら、あなたが変更している動作を正確に知るでしょう。

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


継承の代わりにコンポジションを使用できるように、オブジェクトのインスタンスを返す単一のメソッドでファクトリクラス全体を追加しても、コードが実際に読みやすくなるわけではありません...(はい、毎回新しいインスタンスが必要な場合はこれが必要です特定のメソッドが呼び出されます。)それは、継承が良いことを意味するわけではありませんが、合成は常に読みやすさを改善するわけではありません。
jpmc26

1
@ jpmc26 ...クラスの新しいインスタンスを作成するためだけにファクトリクラスが必要なのはなぜですか?そして、継承はあなたが記述しているものの読みやすさをどのように改善しますか?
ケビンワークマン

上記のコメントで、そのような工場の理由を明示しませんでしたか?読み取るコード少なくなるため、読みやすくなります。基本クラスといくつかのサブクラスの1つの抽象メソッドで同じ動作を実現できます。(とにかく、クラスごとに合成クラスの異なる実装が必要になります。)オブジェクトの構築の詳細が基本クラスに強く結び付けられている場合、コードの他の場所ではあまり使用されず、さらに削減されます。別のクラスを作成する利点。次に、コードの密接に関連する部分を互いに近くに保ちます。
jpmc26

先ほど言ったように、これは必ずしも継承が良いことを意味するわけではありませんが、状況によっては合成が読みやすさを改善しないことを実証しています。
jpmc26

1
特定の例を見ることなく、本当にコメントするのは難しいです。しかし、あなたが説明したことは、私にとって過剰なエンジニアリングの場合のように聞こえます。クラスのインスタンスが必要ですか?newキーワードは、あなたに1つを提供します。とにかく議論をありがとう。
ケビンワークマン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.