ここ数か月の間に、「継承よりも構成を優先する」というマントラは、どこからともなく出現し、プログラミングコミュニティ内でほぼ何らかのミームになったようです。そして、それを見るたびに、私は少し不思議に思っています。それは誰かが「ハンマーよりもドリルを好む」と言ったようなものです。私の経験では、合成と継承は使用例が異なる2つの異なるツールであり、交換可能であり、一方が他方より本質的に優れているかのように扱うことは意味がありません。
また、私はwhy継承が正しくなく、構成が適切であることについての本当の説明を見たことがないので、私はさらに疑わしくなります。信仰によって受け入れられるだけなのでしょうか? Liskov置換とポリモーフィズムには、よく知られている明確な利点があり、IMOはオブジェクト指向プログラミングを使用することの全体的なポイントを構成し、合成のためにそれらを破棄する理由を説明する人はいません。
この概念がどこから来たのか、そしてその背後にある理論的根拠は何か知っている人はいますか?
GoFよりずっと前に、構成と継承の議論を聞いたと思いますが、特定のソースに指を置くことはできません。とにかくBoochだったのかもしれません。
<暴言>
ああ、でも多くのマントラのように、これは典型的な線に沿って退化しています:
もともとn00bsをEnlightenmentに導くことを意図していたミームは、現在、彼らを無意識のうちに殴打するためのクラブとして使用されています。
構成と継承は非常に異なるものであり、互いに混同しないでください。合成を使用して継承を多くの追加作業でシミュレートできることは事実ですが、これは継承を二級市民にすることも、それは作曲を好きな息子にします。多くのn00bsが継承をショートカットとして使用しようとするという事実はメカニズムを無効にせず、ほとんどすべてのn00bsは間違いから学び、それによって改善します。
あなたのデザインについて[〜#〜] think [〜#〜]してください、そしてスローガンの噴出を停止してください。
</ rant>
経験。
あなたが言うように、それらは異なる仕事のためのツールですが、このフレーズは人々がそのようにそれを使用していなかったために生じました。
継承は主にポリモーフィックなツールですが、一部の人々は、後からの危険にかなりの注意を払い、コードの再利用/共有の手段としてそれを使用しようとします。理論的根拠は「継承すればすべてのメソッドを無料で入手できる」ということですが、これらの2つのクラスには多相関係がない可能性があるという事実を無視しています。
それでは、継承よりも構成を優先するのはなぜでしょうか。クラス間の関係が多態的なものではないことが多いためです。それは単に、相続することによってひどい反応をしないように人々に思い出させるのを助けるために存在します。
これは新しいアイデアではありません。1994年に発行されたGoFデザインパターンブックで実際に紹介されたと思います。
継承の主な問題は、ホワイトボックスであることです。定義により、継承元のクラスの実装の詳細を知る必要があります。一方、コンポジションでは、作成しているクラスのパブリックインターフェースのみに関心があります。
GoFの本から:
継承はサブクラスをその親の実装の詳細に公開し、「継承はカプセル化を壊す」としばしば言われます
あなたの質問の一部に答えるために、この概念は最初にGOF Design Patterns:Elements of Reusable Object-Oriented Software で最初に発表されたと思います。 20、はじめに:
継承よりもオブジェクト構成を優先する
彼らはこの声明の前に、継承と構成を簡単に比較しています。彼らは「継承を決して使用しない」とは言いません。
「継承を介した合成」は、「クラスのデータ(または動作)を別のクラスに組み込む必要があると感じる場合は、継承を盲目的に適用する前に必ず合成を使用することを検討してください」という短い(そして明らかに誤解を招く)方法です。
なぜこれが本当ですか?継承により、2つのクラス間の緊密なコンパイル時の結合が作成されるためです。対照的に、構成は疎結合であり、とりわけ、懸念事項の明確な分離、実行時に依存関係を切り替える可能性、およびより簡単で分離された依存関係のテスト容易性を可能にします。
つまり、継承はコストがかかるため、慎重に処理する必要があるということであり、役に立たないということではありません。実際、「依存関係の合成」は、「依存関係の合成+継承」になることがよくあります。これは、合成された依存関係を具象サブクラス自体ではなく抽象スーパークラスにしたい場合が多いためです。これにより、実行時に依存関係の異なる具体的な実装を切り替えることができます。
その理由から(とりわけ)、バニラ継承よりもインターフェイス実装または抽象クラスの形で継承がより頻繁に使用されることをおそらく目にするでしょう。
(比喩的な)例は次のようになります。
「Snakeクラスがあり、Snakeが噛んだときに何が起こるかをそのクラスの一部として含めたいと思っています。SnakeにBite()メソッドを持つBiterAnimalクラスを継承させ、そのメソッドをオーバーライドして有毒な咬傷を反映させたくなります。 。しかし、継承を介した構成では、代わりに構成を使用する必要があると警告されます...この場合、これは、Biteメンバーを持つSnakeに変換できます。Biteクラスは、複数のサブクラスを持つ抽象(またはインターフェイス)である可能性があります。これにより、 VenomousBiteとDryBiteのサブクラスを作成したり、ヘビが年齢を重ねるにつれて、同じSnakeインスタンスでバイトを変更できるなど、素晴らしいことです。さらに、Biteのすべての効果を独自のクラスで処理すると、Frostクラスで再利用できます。 、霜が噛み付くが、BiterAnimalではないので...」
構成は少し言語/フレームワークにとらわれない
継承とそれが実施する/必要とする/可能にすることは、サブ/スーパークラスがアクセスできるものと、仮想メソッドなどのパフォーマンスへの影響の点で言語間で異なります。構成は非常に基本的で、必要な言語はほとんどありませんサポート、つまり異なるプラットフォーム/フレームワークにわたる実装では、構成パターンをより簡単に共有できます。
構成は、オブジェクトを構築する非常にシンプルで触覚的な方法です
継承は比較的理解しやすいですが、実際の生活ではそれほど簡単に実証されていません。現実の多くのオブジェクトは、パーツに分解して構成できます。自転車は、2つの車輪、フレーム、シート、チェーンなどを使用して構築できるとします。構成で簡単に説明できます。一方、継承のメタファーでは、自転車は一輪車を拡張していると言えます。ある程度実現可能ですが、実際の構成からははるかに遠いです(明らかに、これは非常に優れた継承の例ではありませんが、ポイントは同じです)。 Wordの継承(少なくとも、私が期待するほとんどの米国英語話者)でさえ、「亡くなった親族から受け継がれた何か」という意味を自動的に呼び出します。
ほとんどの場合、構成はより柔軟です
コンポジションを使用すると、常に独自の動作を定義するか、単にコンポジションパーツのその部分を公開するかを選択できます。このようにして、継承階層によって課される可能性のある制限に直面しません(仮想vs非仮想など)。
つまり、コンポジションは当然、継承よりも理論的な制約が少ない単純なメタファだからです。さらに、これらの特定の理由は、設計時に明らかになるか、または継承の問題点のいくつかを処理するときに突き出る可能性があります。
免責事項:
明らかに、これは明確ではありません/一方通行です。各設計は、いくつかのパターン/ツールの評価に値します。継承は広く使用されており、多くの利点があり、多くの場合、構成よりもエレガントです。これらは、作曲を支持するときに使用できるいくつかの考えられる理由にすぎません。
たぶん、ここ数ヶ月の間にこれを言っている人々に気づいたかもしれませんが、それよりずっと長い間、優れたプログラマーに知られています。私は確かにそれを適切なところで約10年間言い続けてきました。
コンセプトのポイントは、継承に大きな概念上のオーバーヘッドがあることです。継承を使用している場合、すべての単一のメソッド呼び出しには暗黙的なディスパッチがあります。深い継承ツリー、複数のディスパッチ、または(さらに悪い場合は)両方がある場合、特定の呼び出しで特定のメソッドがどこにディスパッチされるかを把握することは、ロイヤルPITAになる可能性があります。コードに関する正しい推論がより複雑になり、デバッグが難しくなります。
説明する簡単な例を挙げましょう。継承ツリーの奥深くで、メソッドにfoo
という名前を付けたとします。次に、誰かがやって来て、ツリーの最上部にfoo
を追加しますが、別のことをしています。 (このケースは多重継承でより一般的です。)現在、ルートクラスで作業しているその人は、あいまいな子クラスを壊し、おそらくそれを認識していません。ユニットテストのカバレッジは100%ですが、最上位の人は子クラスのテストを考えておらず、子クラスのテストは最上位で作成された新しいメソッドのテストを考えていないため、この破損に気付かない可能性があります。 。 (確かに、これをキャッチする単体テストを作成する方法はありますが、そのようにテストを簡単に作成できない場合もあります。)
対照的に、コンポジションを使用する場合、通常、各呼び出しで、呼び出しをディスパッチする対象が明確になります。 (たとえば、依存関係の注入などでコントロールの反転を使用している場合、呼び出しがどこに行くのかを理解することも問題になる可能性があります。しかし、通常は簡単に理解することができます。)これにより、それについての推論が容易になります。おまけとして、コンポジションはメソッドを互いに分離させることになります。上の例は、子クラスが不明瞭なコンポーネントに移動するため発生しません。また、foo
の呼び出しが不明瞭なコンポーネントとメインオブジェクトのどちらを対象としたものであるかについての質問はありません。
これで、継承と構成が2つの異なるタイプの機能を果たす2つの非常に異なるツールであることは完全に正しいことです。確かに継承は概念的なオーバーヘッドを伴いますが、それがジョブに適切なツールである場合、それを使用せずに手動で行うよりも概念的なオーバーヘッドが少なくなります。彼らが何をしているのかを知っている人は誰もあなたが継承を使うべきではないと言っているでしょう。しかし、それが正しいことであることを確認してください。
残念ながら、多くの開発者はオブジェクト指向ソフトウェアについて学び、継承について学び、その後、新しいaxをできるだけ頻繁に使用します。つまり、構成が適切なツールであった場合に、継承を使用しようとします。うまくいけば、時間をかけて上手に学習できるようになりますが、手足を数本外した後などになることはよくあります。悪い考えだと前もって説明すると、学習プロセスがスピードアップし、怪我が減る傾向があります。
OO初心者は必要のないときに継承を使用する傾向があるという観察に対する反応です。継承は確かに悪いことではありませんが、過度に使用することができます。1つのクラスだけが機能を必要とする場合他からの場合、コンポジションはおそらく機能しますが、他の状況では、継承は機能し、コンポジションは機能しません。
クラスから継承することは多くのことを意味します。これは、DerivedがBaseのタイプであることを示しています(詳細は Liskov Substitutionの原則 を参照)。Baseを使用する場合は常に、Derivedを使用することが理にかなっています。これにより、保護されたメンバーおよびBaseのメンバー機能への派生アクセス権が付与されます。これは密接な関係にあるため、カップリングが高く、一方を変更すると、もう一方も変更する必要が生じる可能性が高くなります。
カップリングは悪いことです。プログラムの理解と変更が難しくなります。他の条件が同じであれば、カップリングの少ないオプションを常に選択する必要があります。
したがって、合成または継承のいずれかが効果的に機能する場合は、結合が低いため、合成を選択します。あなたがしなければならないので、合成が仕事を効果的に行わず、継承がそうである場合、継承を選択します。
これが私の2セントです(すでに提起されたすべての優れたポイントを超えています)。
私見、それはほとんどのプログラマーが実際には継承を取得しておらず、部分的にこの概念がどのように教えられた結果としてそれをやり過ぎてしまうという事実に帰着します。この概念は、正しい方法を教えることに焦点を当てるのではなく、無理をしすぎないようにする方法として存在します。
教育やメンタリングに時間を費やしたことのある人なら誰でも、これが特に他のパラダイムでの経験を持つ新しい開発者によく起こることであることを知っています。
これらの開発者は、最初は継承がこの恐ろしい概念であると感じています。そのため、インターフェースのオーバーラップ(一般的なサブタイプなしの同じ指定された動作など)と、共通の機能を実装するためのグローバルを持つタイプを作成することになります。
それから(しばしば熱心な教えの結果として)彼らは継承の利点について学びますが、それはしばしば再利用のための包括的なソリューションとして教えられます。彼らはすべての共有された行動は継承を通して共有されなければならないという認識に終わります。これは、サブタイプではなく実装継承に重点が置かれることが多いためです。
ケースの80%で十分です。しかし、残りの20%が問題の始まりです。書き換えを回避し、実装の共有を確実に活用するために、基盤となる抽象化ではなく、目的の実装を中心に階層の設計を開始します。 「スタックは二重にリンクされたリストから継承する」がこれの良い例です。
ほとんどの教師は、この時点でインターフェイスの概念を導入することを強く求めていません。これは別の概念であるか、(C++では)この段階では教えられていない抽象クラスと多重継承を使用してそれらを偽造する必要があるためです。 Javaでは、多くの教師は「多重継承なし」または「多重継承は悪い」とインターフェースの重要性を区別していません。
これらすべては、実装の継承で余分なコードを記述する必要がないことの美しさの多くを学んだという事実によってさらに悪化し、単純な委任コードの多くは不自然に見えます。そのため、コンポジションは乱雑に見えます。そのため、これらの経験則を使用して新しいプログラマをプッシュするために、これらの経験則が必要なのはそのためです(これは、やりすぎです)。
コメントの1つで、メイソンはある日継承は有害と見なされるについて話していると述べました。
そうだといい。
継承の問題は簡単であり、致命的です。1つの概念に1つの機能があるべきだという考えは尊重されません。
ほとんどのオブジェクト指向言語では、基本クラスから継承する場合:
そして、ここでトラブルになります。
これはnotが唯一のアプローチですが、00言語はほとんどの場合それにこだわっています。幸いなことに、それらにはインターフェース/抽象クラスが存在します。
また、他の方法を実行することの容易さの欠如は、それを主に使用することに貢献します:率直に言って、これを知っていても、インターフェイスから継承し、基本クラスを構成によって埋め込み、ほとんどのメソッド呼び出しを委任しますか?それはかなり良いことですが、突然新しいメソッドがインターフェースに表示され、それを実装する方法を意識的に選択しなければならない場合は警告されることさえあります。
対置として、Haskell
は、純粋なインターフェース(概念と呼ばれる)から「派生」する場合にのみ、Liskov原理を使用できます(1)。既存のクラスから派生することはできません。構成だけがそのデータを埋め込むことができます。
(1)の概念は実装に適切なデフォルトを提供する可能性がありますが、それらにはデータがないため、このデフォルトは、概念によって提案された他のメソッドに関して、または定数に関してのみ定義できます。
この種のアドバイスは、「飛行機よりも運転をしたい」というようなものだと思います。つまり、飛行機は自動車に比べてあらゆる種類の利点がありますが、それにはある程度の複雑さが伴います。したがって、多くの人が市内中心部から郊外に飛ぶことを試みる場合、彼らが本当に、本当に聞く必要があるアドバイスは、彼らが飛ぶ必要がないということです。短期的にはクール/効率的/簡単に見えても、長期的にはより複雑になります。あなたがdoを飛ばす必要があるとき、それは一般的に明白であるはずです。
同様に、継承は合成ができないことを実行できますが、それが必要なときに使用し、不要な場合は使用しないでください。したがって、継承が不要なときに継承が必要であると想定したくない場合は、「構成を優先する」というアドバイスは必要ありません。しかし、多くの人々doおよびdoはそのアドバイスを必要とします。
継承が本当に必要な場合は明白であり、それを使用する必要があることは暗黙的に想定されています。
また、スティーブン・ロウの答え。本当に、本当に。
簡単な答えは、継承は合成よりも結合が大きいということです。他の点では同等の品質の2つのオプションが与えられた場合、結合の少ないオプションを選択します。
継承は本質的に悪いわけではなく、構成は本質的に良いわけではありません。これらは、OOプログラマがソフトウェアの設計に使用できるツールにすぎません。
あなたがクラスを見るとき、それは絶対に必要以上のことをしていますか( [〜#〜] srp [〜#〜] )?機能を不必要に複製していますか( [〜#〜] dry [〜#〜] )、または他のクラスのプロパティまたはメソッドに過度に関心がありますか( Feature Envy ) ?。クラスがこれらの概念のすべて(および場合によってはそれ以上)に違反している場合、それは 神のクラス になろうとしていますか?これらは、ソフトウェアの設計時に発生する可能性のある多くの問題であり、必ずしも継承の問題ではありませんが、ポリモーフィズムも適用されている場合、大きな頭痛と壊れやすい依存関係をすばやく作成する可能性があります。
問題の原因は、継承についての理解の欠如ではなく、デザインの選択が不適切であるか、または単一の責任の原則。 PolymorphismおよびLiskov Substitutionは、合成のために破棄する必要はありません。ポリモーフィズム自体は継承に依存せずに適用できます。これらはすべて非常に補完的な概念です。慎重に適用した場合。秘訣は、コードをシンプルでクリーンな状態に保ち、堅牢なシステム設計を作成するために作成する必要のあるクラスの数に過度に関心を持たないようにすることです。
継承よりも構成を優先するという問題に関しては、これは本当に、解決される問題に関して最も意味のある設計要素の思慮深い適用の単なる別のケースです。動作を継承する必要がない必要がない場合、構成が非互換性や主要なリファクタリング作業を後で回避するのに役立つため、おそらくそうすべきではありません。一方、多くのコードを繰り返しているため、すべての複製が類似したクラスのグループに集中している場合、共通の祖先を作成すると、同一の呼び出しとクラスの数を減らすことができる可能性があります。各クラス間で繰り返す必要があるかもしれません。したがって、構成を優先しますが、継承が適用されないことを想定していません。