web-dev-qa-db-ja.com

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

私はいつも、構成は継承よりも優先されることを読んでいます。 A 異なる種類のブログ投稿 、たとえば、継承よりも構成を使用することを提唱していますが、どのようにしてポリモーフィズムが達成されるのかわかりません。

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

これが継承を使用する具体的な例です。これはコンポジションを使用するためにどのように変更されますか?

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

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

ポリモーフィズムは、必ずしも継承を意味するものではありません。多くの場合、継承は、ポリモーフィック動作を実装する簡単な手段として使用されます。これは、類似した動作オブジェクトを、完全に共通のルート構造と動作を持つものとして分類すると便利だからです。何年にもわたって目にしてきた車と犬のコードの例を考えてみてください。

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

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

どちらが優れているかについての答えはやや主観的であり、実際にはシステムの動作方法、コンテキストとアーキテクチャの両方で意味のあるもの、そしてテストと保守がどれほど簡単になるかによって決まります。

51
S.Robins

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

Polymorphismは、1つのことで複数の方法で動作します。したがって、ジェネリックス/テンプレートは、単一のコードが型によってその動作を変化させることを可能にする限り、「ポリモーフィック」機能です。実際、このタイプのポリモーフィズムは実際には最も適切に動作し、変化はパラメータによって定義されるため、一般にparametric polymorphismと呼ばれます。

多くの言語は、「オーバーロード」またはad hoc polymorphismと呼ばれる多態性の形式を提供します。この場合、同じ名前の複数のプロシージャがアドホックな方法で定義され、言語によって選択されます(おそらく最も具体的なもの) )。開発された規則を除いて、2つのプロシージャの動作を接続するものがないため、これは最も適切に動作しない種類の多態性です。

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

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

継承は危険ですすべての非常に強力なものと同様に、継承には大混乱を引き起こす力があります。たとえば、あるクラスから継承するときにメソッドをオーバーライドするとします。そのクラスの他のいくつかのメソッドが、継承したメソッドが特定の動作をすることを想定するまでは、すべて元気です。結局のところ、元のクラスの作成者が設計した方法です。 。 designedがオーバーライドされない限り、別のメソッドによって呼び出されるすべてのメソッドをプライベートまたは非仮想(最終)として宣言することにより、これから部分的に保護できます。 。これでも、常に十分であるとは限りません。時々、あなたはこのようなものを見るかもしれません(疑似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は、1つのメソッドを置き換えても他のメソッドのセマンティクスが変更されないように設計されています...待機を除いて、そうではありませんでした。元のように見えますが、doThingsは重要な変更可能な状態を変更しました。したがって、オーバーライド可能な関数を呼び出さなかったとしても、

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

MyUsefulThingsを渡すと、予想とは異なる動作をするようになりました。さらに悪いことに、WayOfDoingUsefulThingsがこれらの約束をしたことさえ知らないかもしれません。多分dealWithStuffWayOfDoingUsefulThingsと同じライブラリからのもので、getStuff()はライブラリによってエクスポートされていません(friend classes(C++の場合)。さらに悪いことに、あなたはそれを実現せずに言語の静的チェックを打ち負かしました:dealWithStuffWayOfDoingUsefulThingsを取り、動作するgetStuff()関数があることを確認しました特定の方法。

構成を使用する

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

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

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

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

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

  1. 継承やデザインを禁止する
  2. 構成を好む

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

82
Philip JF

人々がこれを言う理由は、OOPプログラマは継承から継承された多態性の講義から新しく、多くの多態性メソッドを含む大規模なクラスを書き、その後どこかで維持不可能な混乱で終わる。

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

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

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

27
tdammers

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

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

13
Caleb

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

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

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

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

  4. この反発により、一部の人々はパターンXは常に有害であり、neverを使用する必要があると考えています。

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

CircleShapeから派生することは、exactly継承がOO継承をサポートする言語。

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

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

11
JacquesB