継承よりも合成を優先する必要があるのはなぜですか?


109

私は常に、構成が継承よりも優先されることを読みました。種類とは異なり、上のブログの記事は、例えば、相続上の組成物を用いて提唱したが、私は多型が達成されたかを確認することはできません。

しかし、私は人々が作曲を好むと言うとき、彼らは本当に作曲とインターフェース実装の組み合わせを好むことを意味すると感じています。継承なしでどのようにポリモーフィズムを取得するのですか?

継承を使用する具体的な例を次に示します。構成を使用するためにこれをどのように変更しますか?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

57
いいえ、人々が言うとき、彼らは本当に意味構図好む好む、構図をない 決してこれまでの継承を使用。あなたの質問全体は、誤った前提に基づいています。適切な場合は継承を使用します。
トカゲのビル


2
私はこの法案に同意します。継承を使用することは、GUI開発の一般的な慣行であると考えています。
のPrashant Cholachagudda

2
正方形は単なる2つの三角形の合成であるため、合成を使用する必要があります。実際、楕円以外の形状はすべて三角形の合成であると思います。多態性は契約上の義務に関するもので、継承から100%削除されます。誰かがピラミッドを生成できるようにしたいために三角形に奇妙なものが追加された場合、三角形から継承すると、六角形から3Dピラミッドを生成することはありませんが、そのすべてを取得します。
ジミー・ホッファ

2
@BilltheLizard私は、それを本当に言う人の多くは、決して継承を決して使用しないことを意味すると思いますが、彼らは間違っています。
イミビス

回答:


51

多態性は必ずしも継承を意味するわけではありません。多くの場合、継承は多態的な動作を実装する簡単な手段として使用されます。これは、同様の動作オブジェクトを完全に共通のルート構造と動作を持つものとして分類すると便利だからです。あなたが長年にわたって見てきたこれらすべての車と犬のコードの例を考えてみてください。

しかし、同じではないオブジェクトについてはどうでしょう。車と惑星のモデリングは非常に異なりますが、どちらもMove()の動作を実装する場合があります。

実際、あなたはあなたが言っ"But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation."たときに基本的にあなた自身の質問に答えました。共通の動作は、インターフェースおよび動作複合を通じて提供できます。

どちらが良いかについての答えは多少主観的であり、実際にシステムをどのように動作させたいか、コンテキストとアーキテクチャの両方で理にかなっていること、テストと保守がどれだけ簡単かということになります。


実際には、「インターフェイスを介したポリモーフィズム」はどのくらいの頻度で現れ、それは正常であると見なされます(ラングスの表現力の活用とは対照的に)。継承を介したポリモーフィズムは、仕様の後に発見された言語(C ++)の結果ではなく、慎重な設計によるものだと思います。
-samis

1
誰が惑星と車でmove()を呼び出し、同じと考えますか?!ここでの質問は、どのような状況で彼らが動くことができるかということです。両方が単純な2Dゲームの2Dオブジェクトである場合、移動を継承できます。大規模なデータシミュレーションのオブジェクトである場合、同じベースから継承させることはあまり意味がないため、インターフェイス
-NikkyD

2
@SamusArin どこでも表示 され 、インターフェースをサポートする言語では完全に正常であると考えられています。「言語の表現力の活用」とはどういう意味ですか?これがインターフェースの目的です。
アンドレスF.

@AndresF。オブザーバーパターンを示した「アプリケーション指向のオブジェクト指向分析と設計」を読んでいるときに、例で「インターフェイスを介したポリモーフィズム」に出会いました。それから私の答えに気づきました。
-samis

@AndresF。最後の(最初の)プロジェクトでポリモーフィズムをどのように使用したかにより、これに関する私のビジョンは少し盲目だったと思います。すべて同じベースから派生した5つのレコードタイプがありました。とにかく悟りをありがとう。
-samis

79

構図を優先することは、多態性だけではありません。それはその一部ですが、あなたは正しいです(少なくとも名目上の型付けされた言語では)人々が本当に意味するのは「構成とインターフェース実装の組み合わせを好む」ということです。しかし、(多くの状況で)作曲を好む理由は深遠です。

多態性とは、複数の動作をすることの1つです。したがって、ジェネリック/テンプレートは、単一のコードで型によって動作を変えることができる限り、「ポリモーフィック」機能です。実際には、このタイプのポリモーフィズムは実際に最も適切に動作し、バリエーションはパラメーターによって定義されるため、一般にパラメトリックポリモーフィズムと呼ばれます。

多くの言語は、「オーバーロード」と呼ばれるポリモーフィズムまたはアドホックポリモーフィズムを提供します。このポリモーフィズムでは、同じ名前の複数のプロシージャがアドホックに定義され、言語によって選択されます(おそらく最も具体的な)。開発された規約を除き、2つのプロシージャの動作を接続するものは何もないため、これは最も動作の悪い種類のポリモーフィズムです。

3番目の種類の多型は、サブタイプ多型です。ここで、特定のタイプで定義されたプロシージャは、そのタイプの「サブタイプ」ファミリー全体でも機能します。インターフェイスを実装するか、クラスを拡張すると、通常、サブタイプを作成する意図を宣言します。真のサブタイプは、リスコフの置換原則によって管理されます、スーパータイプのすべてのオブジェクトについて何かを証明できれば、サブタイプのすべてのインスタンスについてそれを証明できるという。しかし、C ++やJavaのような言語では、一般に人々はサブクラスについて真実であるかもしれないし、そうでないかもしれないクラスについて強制されておらず、しばしば文書化されていない仮定を持っているので、人生は危険になります。つまり、コードは実際よりも多くのことが証明可能であるかのように書かれており、不注意にサブタイプした場合、多くの問題が発生します。

継承は、実際には多型とは無関係です。自分自身への参照を持つ「T」というものがある場合、「T」から「T」の参照を「S」への参照に置き換えて新しいもの「S」を作成すると、継承が発生します。継承は多くの状況で発生する可能性があるため、その定義は意図的に曖昧ですが、最も一般的なのはthis、仮想関数によって呼び出されるthisポインターをサブタイプへのポインターで置き換える効果を持つオブジェクトをサブクラス化することです。

継承は、すべての非常に強力なものが継承を破壊する力を持っているように、危険です。たとえば、あるクラスから継承するときにメソッドをオーバーライドするとします:元のクラスの作成者がそれを設計した方法であるため、そのクラスの他のメソッドが継承するメソッドを特定の方法で動作させると仮定するまで、すべてが順調です。オーバーライドれるように設計されていない限り、プライベートまたは非仮想(最終)メソッドによって呼び出されるすべてのメソッドを宣言することにより、これに対して部分的に保護できます。しかし、これでも常に十分とは限りません。時々、このようなものが表示されることがあります(擬似Javaで、C ++およびC#ユーザーが読み込めることを願っています)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

これは素敵だと思い、独自の「物事」のやり方がありますが、継承を使用して「moreThings」をする能力を獲得します。

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

そしてすべてが順調です。WayOfDoingUsefulThingsあるメソッドを置き換えても他のメソッドのセマンティクスは変わらないように設計されていました...待機を除いて、そうではありませんでした。どうやらそのように見えますが、doThings重要な変更可能な状態が変更されました。したがって、オーバーライド可能な関数を呼び出さなかったとしても、

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

あなたがそれを渡すとき、今予想されたことと異なることをしMyUsefulThingsます。さらに悪いことに、あなたはWayOfDoingUsefulThingsそれらがそれらの約束をしたことさえ知らないかもしれません。たぶん、dealWithStuff同じライブラリーから来ているWayOfDoingUsefulThingsgetStuff()さえ(考えるライブラリによってエクスポートされていない友人のクラス C ++で)。さらに悪いことに、あなたはそれを気付かずに言語の静的チェックを破りました:特定の方法で振る舞う関数を持っていることを確認するためだけにdealWithStuff取りました。WayOfDoingUsefulThingsgetStuff()

コンポジションを使用する

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

静的型の安全性を取り戻します。一般に、サブタイプを実装する場合、コンポジションは継承よりも使いやすく安全です。また、finalメソッドをオーバーライドすることもできます。つまり、ほとんどの場合、インターフェースを除き、すべての final / non-virtualを自由に宣言できます。

より良い世界では、言語はdelegationキーワードとともにボイラープレートを自動的に挿入します。ほとんどはそうではないので、欠点は大きなクラスです。ただし、委任インスタンスを作成するためにIDEを取得できます。

今、人生とは多態性だけではありません。常にサブタイプする必要はありません。ポリモーフィズムの目標は一般にコードの再利用ですが、その目標を達成する唯一の方法ではありません。多くの場合、機能を管理する方法として、サブタイプ多型なしで構成を使用することが理にかなっています。

また、行動継承には用途があります。これは、コンピューターサイエンスの最も強力なアイデアの1つです。そのほとんどの場合、優れたOOPアプリケーションは、インターフェイスの継承と構成のみを使用して作成できます。2つの原則

  1. 継承または設計を禁止する
  2. 構図を好む

上記の理由から優れたガイドであり、実質的なコストは発生しません。


4
素敵な答え。継承を使用してコードの再利用を実現しようとすることは、明らかに間違った道であると要約します。継承は非常に強い制約であり(「パワー」を追加することは間違ったアナロジーです!)、継承されたクラス間に強い依存関係を作成します。あまりにも多くの依存=不正なコード:)だから、継承は、一般的に統一されたインタフェースのための輝く(別名「として振る舞う」)(=継承されたクラスの複雑さを隠して)何か考え直すまたは組成物を使用するために、...
MAR

2
これは良い答えです。+1。少なくとも私の目には、余分な複雑さはかなりのコストになるかもしれないと思われます。そして、これは個人的に、作曲を好むことから大したことをするのを少しheしました。特に、インターフェイス、コンポジション、およびDI(これが他のものを追加していることを知っています)を介して多くの単体テストフレンドリーなコードを作成しようとしている場合、誰かがいくつかの異なるファイルを調べて非常に検索するのは非常に簡単ですいくつかの詳細。継承の設計原理よりも構成だけを扱う場合でも、なぜこれが頻繁に言及されないのですか?
パンツァークライシス

27

人々がこれを言う理由は、継承による多態性の講義から新たに始まったOOPプログラマーは、多くの多態性のメソッドで大規模なクラスを書く傾向があり、その後、どこかでメンテナンス不能な混乱に陥るからです。

典型的な例は、ゲーム開発の世界から来ています。プレイヤーの宇宙船、モンスター、弾丸など、すべてのゲームエンティティの基本クラスがあるとします。各エンティティタイプには独自のサブクラスがあります。継承のアプローチは、例えば、いくつかの多型のメソッドを使用することになりupdate_controls()update_physics()draw()など、それぞれのサブクラスのためにそれらを実装します。ただし、これは、関連性のない機能を結合していることを意味します。オブジェクトを移動するためにオブジェクトがどのように見えるかは関係なく、描画するためにそのAIについて何も知る必要はありません。代わりに、合成アプローチは、いくつかの基本クラス(またはインターフェイス)、たとえばEntityBrain(サブクラスがAIまたはプレイヤー入力を実装する)、EntityPhysics(サブクラスが運動物理学を実装する)、(サブクラスEntityPainterが描画を処理する)、および非多態性クラスを定義しますEntityそれぞれのインスタンスを1つ保持します。このように、任意の外観を任意の物理モデルおよびAIと組み合わせることができます。また、それらを分離しておくため、コードもずっときれいになります。また、「レベル1のバルーンモンスターのように見えるが、レベル15のクレイジーピエロのように振る舞うモンスターが欲しい」などの問題はなくなります。適切なコンポーネントを取り、それらを接着するだけです。

合成アプローチでは、引き続き各コンポーネント内で継承を使用することに注意してください。ただし、ここではインターフェイスとその実装のみを使用するのが理想的です。

「関心の分離」はここでのキーフレーズです。物理学の表現、AIの実装、エンティティの描画は3つの関心事であり、それらをエンティティに結合することは4番目です。構成アプローチでは、各懸念事項が1つのクラスとしてモデル化されます。


1
これはすべて良いことであり、元の質問にリンクされている記事で提唱されています。ポリモーフィズム(C ++)を維持しながら、これらすべてのエンティティを含む簡単な例を提供できれば(時間があれば)大好きです。または、リソースを指定します。ありがとう。
ムスタファム

私はあなたの答えが非常に古いことを知っていますが、あなたが私のゲームで述べたのとまったく同じことをしました。
スネ

「私は…のように見えるモンスターが欲しい」これはどのように意味がありますか?インターフェイスは実装を提供しません。外観と動作のコードを何らかの方法でコピーする必要があります
-NikkyD

1
@NikkyDあなたは取るMonsterVisuals balloonVisualsMonsterBehaviour crazyClownBehaviourしてインスタンス化しMonster(balloonVisuals, crazyClownBehaviour)、一緒にMonster(balloonVisuals, balloonBehaviour)し、Monster(crazyClownVisuals, crazyClownBehaviour)レベル1とレベル15にインスタンス化されたインスタンス
Caleth

13

あなたが与えた例は、継承が自然な選択である例です。合成は常に継承よりも良い選択だとは誰も主張しないと思います。これは単なるガイドラインに過ぎず、多くの非常に特殊化されたオブジェクトを作成するよりも、いくつかの比較的単純なオブジェクトを組み立てる方が良いことを意味します。

委任は、継承の代わりに構成を使用する方法の一例です。委任により、サブクラス化せずにクラスの動作を変更できます。ネットワーク接続を提供するクラス、NetStreamを検討してください。NetStreamをサブクラス化して共通のネットワークプロトコルを実装するのが自然な場合があるため、FTPStreamとHTTPStreamを思い付くかもしれません。ただし、UpdateMyWebServiceHTTPStreamなどの単一の目的のために非常に具体的なHTTPStreamサブクラスを作成する代わりに、多くの場合、HTTPStreamの単純な古いインスタンスと、そのオブジェクトから受信するデータの処理方法を知っているデリゲートを使用することをお勧めします。より良い理由の1つは、維持する必要があるが再利用できないクラスの急増を回避することです。


11

このサイクルは、ソフトウェア開発の議論で多く見られます。

  1. 一部の機能またはパターン(「パターンX」と呼びます)は、特定の目的に役立つことがわかりました。ブログの投稿は、パターンXの長所を称賛して書かれています。

  2. 誇大広告は、可能な限りパターンXを使用する必要があると考える人を導きます。

  3. 他の人々は、パターンXが適切ではない状況で使用されているパターンXを見てイライラし、常にパターンXを使用するべきではなく、状況によっては有害であると述べているブログ投稿を書いています 。

  4. バックラッシにより、パターンXは常に有害であり、決して使用すべきではない考える人がいます。

この誇大広告/バックラッシュサイクルはGOTO、パターンからSQL、NoSQL、そしてはい、継承まで、ほとんどすべての機能で発生します。解毒剤は、常にコンテキストを考慮することです。

Circleから派生しShapeている正確に継承は、継承をサポートするオブジェクト指向言語で使用されることになっていますか。

経験則の「継承よりも合成を優先する」は、コンテキストなしでは本当に誤解を招きます。継承がより適切な場合は継承を優先する必要がありますが、構成がより適切な場合は構成を優先する必要があります。この文は、誇大宣伝サイクルのステージ2で、どこでも継承を使用すべきだと考える人々を対象としています。しかし、サイクルは進んでおり、今日では、一部の人々は相続自体が何らかの形で悪いと考えるようになっているようです。

ハンマーとドライバーのように考えてください。あなたはハンマーよりもドライバーを好むべきですか?質問は意味がありません。ジョブに適したツールを使用する必要がありますが、それはすべて、実行する必要のあるタスクによって異なります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.