28

構成と継承。

どちらも適切な場合に選択すべきツールであり、コンポジションと継承のどちらを選択するかはコンテキストが非常に重要であることを認識しています。ただし、それぞれの適切なコンテキストに関する議論は、通常、少しあいまいです。これにより、継承とポリモーフィズムが従来の OOP の別個の側面であることがいかに明確であるかを考え始めました。

ポリモーフィズムにより、「is-a」関係と継承を同等に指定できます。特に、基本クラスから継承すると、そのクラスとそのサブクラスの間に多態的な関係が暗黙的に作成されます。ただし、ポリモーフィズムは純粋なインターフェイスを使用して実装できますが、継承は実装の詳細を同時に転送することにより、ポリモーフィックな関係を複雑にします。このように、継承は純粋なポリモーフィズムとはまったく異なります。

ツールとしての継承は、些細なケースでの実装の再利用を簡素化することにより、(純粋なインターフェイスを介した) ポリモーフィズムとは異なる方法でプログラマーに役立ちます。. ただし、ほとんどの場合、スーパークラスの実装の詳細は、サブクラスの要件と微妙に競合します。これが、「オーバーライド」と「メンバーの非表示」がある理由です。これらの場合、継承によって提供される実装の再利用は、コードのカスケード レベル全体で状態の変更と実行パスを検証するという追加の労力によって購入されます。サブクラスの完全な「フラット化された」実装の詳細は、複数のクラスに分散されます。複数のファイル。その一部のみが問題のサブクラスに適用されます。スーパークラスのコードを見ないと、オーバーライドされていない詳細が状態を操作したり、実行を迂回させたりしていることを知る方法がないため、継承を扱うときは、その階層を調べることが絶対に必要です。

比較すると、コンポジションを排他的に使用すると、明示的にインスタンス化されたオブジェクトによってどの状態が変更され、そのメソッドが任意に呼び出されるかがわかります。真にフラット化された実装はまだ達成されていません (そして、構造化プログラミングの利点は実装の詳細のカプセル化と抽象化であるため、実際には望ましいことではありません) が、それでもコードを再利用することができ、1 か所を調べるだけで済みます。コードが正しく動作しない場合。

これらのアイデアを実際にテストすることを目標に、伝統的な継承を避けて、純粋なインターフェースベースのポリモーフィズムとオブジェクト構成の組み合わせを避けています。

継承では達成できないオブジェクトの構成とインターフェイスはありますか?

編集

これまでの回答で、ewernli は、一方の技術に利用できる技術的偉業はなく、他方の技術には利用できないと考えています。彼は後で、それぞれの手法に固有のパターンと設計アプローチの違いについて言及しています。これは理にかなっています。しかし、この提案により、従来の継承の代わりにコンポジションとインターフェイスを排他的に使用すると、主要なデザイン パターンの使用が禁止されるかどうかを尋ねることで、質問を絞り込むことができます。もしそうなら、私の状況で使用するための同等のパターンはありませんか?

4

5 に答える 5

25

技術的には、継承で実現できるすべてのものは、委任でも実現できます。したがって、答えは「いいえ」になります。

継承を委任に変換する

次のクラスが継承で実装されているとしましょう。

public class A {
    String a = "A";
    void doSomething() { .... }
    void getDisplayName() {  return a }
    void printName { System.out.println( this.getDisplayName() };   
}

public class B extends A {
    String b = "B";
    void getDisplayName() {  return a + " " + b; }
    void doSomething() { super.doSomething() ; ... }    
}

これらはうまく機能し、printNameBのインスタンスを呼び出すと"A B"コンソールに出力されます。

さて、委任でそれを書き直すと、次のようになります。

public class A {
    String a = "A";
    void doSomething() { .... }
    void getDisplayName() {  return a }
    void printName { System.out.println( this.getName() };  
}

public class B  {
    String b = "B";
    A delegate = new A();
    void getDisplayName() {  return delegate.a + " " + b; }
    void doSomething() { delegate.doSomething() ; ... } 
    void printName() { delegate.printName() ; ... }
}

Bで定義printNameし、Bがインスタンス化されたときにデリゲートを作成する必要があります。の呼び出しdoSomethingは、継承の場合と同じように機能します。ただし、への呼び出しはコンソールに出力printNameされます。"A"実際、委任により、オブジェクトインスタンスにバインドされる「this」の強力な概念と、オーバーライドされたメソッドを呼び出すことができるベースメソッドが失われました。

これは、純粋な委任をサポートする言語で解決できます。純粋な委任では、委任内の「this」は引き続きBのインスタンスを参照します。これはthis.getName()、クラスBからメソッドディスパッチを開始することを意味します。継承の場合と同じことを実現します。これは、委任があるSelfなどのプロトタイプベースの言語で使用されるメカニズムであり、組み込みの機能があります( Selfで継承がどのように機能するかをここで読むことができます)。

しかし、Javaには純粋な委任はありません。それではいつ立ち往生していますか?いいえ、実際には、もう少し努力すれば、自分たちでそれを行うことができます。

public class A implements AInterface {
    String a = "A";
    AInterface owner; // replace "this"
    A ( AInterface o ) { owner = o }
    void doSomething() { .... }
    void getDisplayName() {  return a }
    void printName { System.out.println( owner.getName() }; 
}

public class B  implements AInterface {
    String b = "B";
    A delegate = new A( this );
    void getDisplayName() {  return delegate.a + " " + b; }
    void doSomething() { delegate.doSomething() ; ... } 
    void printName() { delegate.printName() ; ... }
}

基本的に、組み込みの継承が提供するものを再実装しています。それは意味がありますか?いいえ、違います。しかし、それは継承が常に委任に変換できることを示しています。

討論

継承は、基本クラスがサブクラスでオーバーライドされるメソッドを呼び出すことができるという事実によって特徴付けられます。これは、たとえば、テンプレートパターンの本質です。そのようなことは、委任では簡単に行うことはできません。一方、これが継承を使いにくくしているのです。ポリモーフィックディスパッチが発生する場所と、メソッドがオーバーライドされた場合の影響を理解するには、精神的な工夫が必要です。

継承とそれが設計にもたらす可能性のある脆弱性については、いくつかの既知の落とし穴があります。特にクラス階層が進化する場合。継承が使用されている場合、およびの平等に関していくつかの問題が発生する可能性もあります。しかし一方で、それはまだいくつかの問題を解決するための非常にエレガントな方法です。hashCodeequals

また、継承を委任に置き換えることができたとしても、それらは依然として異なる目的を達成し、互いに補完し合うと主張することができます。それらは、純粋な技術的同等性によって捉えられない同じ意図を伝えません。

(私の理論では、誰かがOOを始めたとき、それは言語の特徴のように認識されるため、継承を使いすぎてしまいがちです。次に、パターン/アプローチである委任を学び、それを好きになることも学びます。しばらくすると、私たちは両方のバランスを見つけ、どちらが良いかという直感の感覚を発達させます。ご覧のとおり、私はまだ両方が好きで、両方とも紹介される前に注意が必要です。)

いくつかの文献

継承と委任は、増分定義と共有の代替方法です。委任はより強力なモデルを提供すると一般に信じられてきました。このペーパーは、委任のすべてのプロパティをキャプチャする継承の「自然な」モデルがあることを示しています。独立して、継承をキャプチャする委任の能力に対する特定の制約が示されています。最後に、委任と継承の両方を完全にキャプチャする新しいフレームワークの概要を説明し、このハイブリッドモデルの影響のいくつかを検討します。

オブジェクト指向プログラミングにおける最も興味をそそると同時に最も問題のある概念の1つは、継承です。継承は、オブジェクト指向プログラミングを他の最新のプログラミングパラダイムと区別する機能と一般に見なされていますが、研究者がその意味と使用法に同意することはめったにありません。[...]

クラスの強力な結合と、継承によって引き起こされる不要なクラスメンバーの急増のために、代わりに構成と委任を使用するという提案が一般的になっています。文献での対応するリファクタリングの提示は、そのような変換が簡単な作業であると信じさせるかもしれません。[...]

于 2010-02-10T17:03:22.760 に答える
4

クイックガンプログラマーがメソッドを追加して階層を拡張することで問題を解決しようとする場合(自然な階層を考えるのではなく)、構成は継承のように人生を台無しにすることはできません

構成によって奇妙なダイヤモンドが発生することはありません。これにより、メンテナンスチームは夜の油を燃やして頭をかきむしります。

継承はGOFデザインパターンでの議論の本質であり、プログラマーが最初にコンポジションを使用した場合は同じではなかったでしょう。

于 2010-02-10T17:04:13.360 に答える
1

私はこれについて間違っているかもしれませんが、とにかくそれを言うつもりです、そして誰かが私が間違っている理由があるならば、ただコメントで答えてください、そして私に反対票を投じないでください。継承が構成よりも優れていると私が考えることができる1つの状況があります。

プロジェクトで使用しているクローズドソースのウィジェットライブラリがあるとします(つまり、実装の詳細は、文書化されているもの以外に私には謎です)。ここで、各ウィジェットに子ウィジェットを追加する機能があるとします。継承を使用すると、Widgetクラスを拡張してCustomWidgetを作成し、ライブラリ内の他のウィジェットの子ウィジェットとしてCustomWidgetを追加できます。次に、CustomWidgetを追加するコードは次のようになります。

Widget baseWidget = new Widget();
CustomWidget customWidget = new CustomWidget();
baseWidget.addChildWidget(customWidget);

非常にクリーンで、子ウィジェットを追加するためのライブラリの規則に準拠しています。ただし、構成では、次のようにする必要があります。

Widget baseWidget = new Widget();
CustomWidget customWidget = new CustomWidget();
customWidget.addAsChildToWidget(baseWidget);

それほどきれいではなく、図書館の慣習を破っています

今、私はあなたが作曲でこれを達成できなかったと言っているわけではありません(実際、私の例はあなたが非常に明確にできることを示しています)、それはすべての状況で理想的ではなく、慣習を破ったり、他の視覚的に魅力のない回避策につながる可能性があります。

于 2013-03-03T03:48:51.103 に答える
1

GUIツールキットを考えてみましょう。

エディット コントロールはウィンドウです。ウィンドウの閉じる/有効にする/ペイントする機能を継承する必要があります。ウィンドウは含まれません。

次に、リッチ テキスト コントロールに編集コントロールの保存/読み取り/切り取り/貼り付け機能を含める必要があります。これは、ウィンドウと編集コントロールのみを含む場合は非常に使いにくいものです。

于 2010-02-10T17:12:15.743 に答える