web-dev-qa-db-ja.com

多重継承が嫌われる「本当の」理由はありますか?

ある言語で多重継承をサポートするという考えはいつも気に入っていました。ほとんどの場合、それは意図的に放棄されていますが、想定される「置き換え」はインターフェースです。インターフェースは単純に、多重継承と同じ地面をすべてカバーしているわけではなく、この制限により、ボイラープレートコードが増える場合があります。

私がこれについて聞いたことのある唯一の基本的な理由は、基本クラスでの ダイヤモンドの問題 です。私はそれを受け入れることができません。私には、「まあ、それを台無しにするのは可能だから、それは自動的に悪い考えだ」のようなひどいことが出てきます。ただし、プログラミング言語で何でも台無しにすることができます。私はこれを真剣に受け止めることはできません。少なくとも、より徹底的な説明なしには。

この問題に気づいているだけで、90%の戦いになります。さらに、何年か前に、「エンベロープ」アルゴリズムなどの汎用的な回避策について何か聞いたと思います(これはベルを鳴らしますか?)。

ダイヤモンドの問題に関して、私が考えることができる唯一の潜在的に真の問題は、サードパーティのライブラリを使用しようとしていて、そのライブラリ内の無関係に見える2つのクラスに共通の基本クラスがあることを確認できない場合に加えて、簡単な言語機能のドキュメントでは、たとえば、実際にダイヤモンドをコンパイルする前に、ダイヤモンドを作成する意図を具体的に宣言する必要がある場合があります。このような機能を使用すると、ダイヤモンドの作成は意図的、無謀、またはこの落とし穴を知らないために行われます。

すべてが言われるように...ほとんどの人が多重継承を嫌う理由がありますか本当ですか、それともすべてがより多くの害を引き起こすヒステリーの束だけですか?良いより?ここに表示されていないものはありますか?ありがとうございました。

CarはWheeledVehicleを拡張し、KIASpectraはCarおよびElectronicを拡張し、KIASpectraはラジオを含みます。 KIASpectraにElectronicが含まれていないのはなぜですか?

  1. それはエレクトロニックだからです。継承と構成は常にis-a関係かhas-a関係である必要があります。

  2. それはエレクトロニックだからです。ワイヤー、回路基板、スイッチなどが上下にあります。

  3. それはエレクトロニックだからです。冬にバッテリーが切れると、すべての車輪が突然なくなったのと同じくらいのトラブルになります。

インターフェースを使用しないのはなぜですか?たとえば、#3を考えてみましょう。私はこれを何度も何度も書きたくありません、そして私は本当にこれを行うために奇妙なプロキシヘルパークラスを作成したくありません:

private void runOrDont()
{
    if (this.battery)
    {
        if (this.battery.working && this.switchedOn)
        {
            this.run();
            return;
        }
    }
    this.dontRun();
}

(その実装が良いか悪いかは知りません。)WheeledVehicleの何にも関連しない、Electronicに関連するこれらの関数がいくつかあると想像できます。 、 およびその逆。

そこには解釈の余地があるので、その例に落ち着くべきかどうか確信が持てませんでした。また、PlaneがVehicleとFlyingObjectを拡張し、BirdがAnimalとFlyingObjectを拡張するという観点から、またはより純粋な例について考えることもできます。

129
Panzercrisis

多くの場合、人々は継承を使用してクラスに特性を提供します。たとえば、ペガサスについて考えてみましょう。多重継承を使用すると、鳥を翼を持つ動物として分類したため、ペガサスが馬と鳥を拡張すると言いたくなるかもしれません。

ただし、鳥にはペガシにはない他の特性があります。たとえば、鳥は産卵し、ペガシは出産します。継承が共有特性を渡す唯一の方法である場合、卵を産む特性をペガサスから除外する方法はありません。

一部の言語では、言語内で特性を明示的な構成にすることを選択しています。他のものは、言語からMIを削除することによって、その方向に優しくあなたを導きます。どちらにしても、「これを正しく行うには本当にMIが必要だ」と思った1つのケースは考えられません。

また、本当に継承とは何かについて説明しましょう。クラスから継承する場合、そのクラスへの依存関係を取得しますが、暗黙的および明示的の両方で、クラスがサポートするコントラクトもサポートする必要があります。

長方形から継承した正方形の古典的な例を見てみましょう。四角形は、長さと幅のプロパティ、およびgetPerimeterおよびgetAreaメソッドも公開します。正方形は長さと幅をオーバーライドするため、一方が設定されている場合、もう一方はgetPerimeterと一致するように設定され、getAreaは同じように機能します(2 * length + 2 * width for perimeterおよびlength * width for area)。

この四角形の実装を四角形の代わりに使用すると壊れるテストケースが1つあります。

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

単一の継承チェーンで物事を正しく行うのに十分なタフです。ミックスに別のものを追加すると、さらに悪化します。


MIのペガサスで述べた落とし穴と、四角形/正方形の関係は、どちらもクラスの経験の浅い設計の結果です。基本的に多重継承を回避することは、開発者が最初から自分自身を攻撃することを回避するのに役立つ方法です。すべての設計原則と同様に、それらに基づいて規律とトレーニングを行うことで、それらから脱却してもよい時期をすぐに発見できます。 スキル習得のドレフュスモデル を参照してください。エキスパートレベルでは、固有の知識が格言/原則に依存しません。ルールが適用されないときに「感じる」ことができます。

そして、私がMIが眉をひそめている理由の「現実の世界」の例で多少だまされたことにも同意します。

UIフレームワークを見てみましょう。具体的には、最初は他の2つのウィジェットの組み合わせにすぎないように見えるいくつかのウィジェットを見てみましょう。コンボボックスのようです。 ComboBoxは、サポートされているDropDownListを持つTextBoxです。つまり値を入力することも、事前に定義された値のリストから選択することもできます。素朴なアプローチは、TextBoxおよびDropDownListからComboBoxを継承することです。

しかし、テキストボックスは、ユーザーが入力したものからその値を導き出します。 DDLは、ユーザーが選択したものからその値を取得します。誰が先例をとりますか? DDLは、元の値のリストになかった入力を検証して拒否するように設計されている可能性があります。そのロジックをオーバーライドしますか?つまり、継承する側がオーバーライドするための内部ロジックを公開する必要があります。またはさらに悪いことに、サブクラスをサポートするためにそこにのみ存在する基本クラスにロジックを追加します( 依存関係の逆転の原則 に違反します)。

MIを回避すると、この落とし穴を完全に回避できます。また、UIウィジェットの一般的で再利用可能な特性を抽出して、必要に応じて適用できるようにする場合もあります。この優れた例は WPF添付プロパティ です。これにより、WPFのフレームワーク要素が、別のフレームワーク要素が親フレームワーク要素から継承せずに使用できるプロパティを提供できるようになります。

たとえば、グリッドはWPFのレイアウトパネルであり、グリッドの配置で子要素を配置する場所を指定する列と行の添付プロパティがあります。添付プロパティがない場合、グリッド内にボタンを配置する場合、ボタンはグリッドから派生する必要があるため、列プロパティと行プロパティにアクセスできます。

開発者はこの概念をさらに取り入れ、動作をコンポーネント化する方法として添付プロパティを使用しました(たとえば、WPFがDataGridを含める前に記述された 添付プロパティを使用したソート可能なグリッドビュー の作成に関する私の投稿です)。このアプローチは、 添付された動作 と呼ばれるXAMLデザインパターンとして認識されています。

うまくいけば、これにより、なぜ多重継承が一般的に嫌われるのかについてもう少し洞察が得られたと思います。

70
Michael Brown

ここに表示されていないものはありますか?

多重継承を許可すると、関数のオーバーロードや仮想ディスパッチに関するルールが、オブジェクトレイアウトに関連する言語の実装だけでなく、明らかによりトリッキーになります。これらは言語の設計者/実装者にかなり影響を与え、言語を完成させ、安定させ、採用させるために、すでに高い水準を引き上げます。

私が見た(そして時々作った)もう1つの一般的な議論は、2つ以上の基本クラスを持つことにより、オブジェクトがほぼ常に単一責任の原則に違反することです。 2つ以上の基本クラスは、(違反の原因となる)独自の責任を持つニース自己完結型クラスであるか、相互に連携して単一のまとまりのある責任を負う部分的/抽象的タイプです。

この別のケースでは、3つのシナリオがあります。

  1. AはBについて何も知らない-すばらしい、あなたは運が良かったので、クラスを組み合わせることができました。
  2. AはBについて知っています-なぜAはBから継承しなかったのですか?
  3. AとBはお互いを知っています。なぜ1つのクラスを作成しなかったのですか。これらのものをそのように結合しているが部分的に置き換え可能にすることからどのような利点が得られますか?

個人的には、多重継承はラップが悪いと思います。特性スタイル構成のよくできたシステムは本当に強力で便利です... C++のような言語ではお勧めできません。

[編集]あなたの例に関しては、それはばかげています。 A Kia hasエレクトロニクス。それはhasエンジンです。同様に、それは電子機器ですhave電源、これはたまたま自動車のバッテリーです。継承は言うまでもなく、多重継承には場所がありません。

60
Telastyn

それが許可されない唯一の理由は、人々が足で自分を撃つことが容易になるためです。

この種のディスカッションで通常従うのは、ツールを使用する柔軟性が足を撃ち落とさないという安全性よりも重要であるかどうかに関する議論です。プログラミングにおける他のほとんどのものと同様に、答えはコンテキストに依存するため、その議論に対する明確な正解はありません。

開発者がMIに慣れていて、MIが実行しているコンテキストで意味がある場合、MIをサポートしていない言語ではMIが見落とされます。同時に、チームがそれに不慣れである場合、またはそれが本当に必要でなく、人々が「できるから」それを使用する場合、それは逆効果です。

しかし、いいえ、多重継承が悪い考えであることを証明するすべての確信のある完全に真の議論は存在しません。

[〜#〜]編集[〜#〜]

この質問への回答は全会一致のようです。悪魔の擁護者となるために、多重継承の良い例を示します。そうしないとハックにつながります。

資本市場アプリケーションを設計しているとします。証券のデータモデルが必要です。一部の証券は株式商品(株式、不動産投資信託など)であり、その他は負債(債券、社債)であり、その他はデリバティブ(オプション、先物)です。したがって、MIを回避する場合は、非常に明確で単純な継承ツリーを作成します。株式は株式を受け継ぎ、債券は負債を受け継ぎます。これまでのところ素晴らしいですが、デリバティブはどうですか?それらは、エクイティのような商品またはデビットのような商品に基づくことができますか?さて、継承ツリーをさらに分岐させると思います。心に留めておいてください、いくつかのデリバティブは株式商品、債務商品、またはどちらにも基づいていません。したがって、継承ツリーは複雑になっています。次に、ビジネスアナリストが来て、インデックス付き証券(インデックスオプション、インデックスフューチャーオプション)をサポートするようになったことを伝えます。そして、これらのことは、株式、負債、またはデリバティブに基づくことができます。これは乱雑になっています!私のインデックスの将来のオプションは、エクイティ->株式->オプション->インデックスを導出しますか?なぜエクイティ->ストック->インデックス->オプションではないのですか?コードに両方が見つかった場合はどうなりますか?

ここでの問題は、これらの基本的なタイプが、相互に自然に派生しない任意の順列に混在する可能性があることです。オブジェクトはis a関係で定義されるため、合成はまったく意味がありません。ここでは、多重継承(またはミックスインの同様の概念)が唯一の論理表現です。

この問題の実際の解決策は、データモデルを作成するために、複数の継承を使用して、資本、負債、派生物、インデックスタイプを定義および混合することです。これにより、両方のオブジェクトが作成され、意味があり、コードの再利用が容易になります。

29
MrFox

ここでの他の答えは、ほとんど理論になっているようです。だから、ここに具体的なPythonの例、簡略化したものを示します。私が実際に真っ向からぶつけたので、かなりの量のリファクタリングが必要です:

_class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()
_

Barは、独自のzeta()の実装があることを前提に書かれていますが、これは一般にかなり良い仮定です。サブクラスは、適切に機能するように、適切にオーバーライドする必要があります。残念ながら、名前は偶然同じでした-それらはかなり異なることをしましたが、BarFooの実装を呼び出していました:

_foozeta
---
barstuff
foozeta
_

スローされたエラーがなく、アプリケーションが少しだけ間違って動作し始め、それを引き起こしたコード変更(_Bar.zeta_の作成)が問題のある場所ではない場合、それはかなりイライラします。

11
Izkata

MI 正しい言語でには実際の問題はないと私は主張します。重要なのは、ダイヤモンド構造を許可することですが、コンパイラーが何らかの規則に基づいて実装の1つを選択するのではなく、サブタイプが独自のオーバーライドを提供することを要求します。

これは、現在取り組んでいる言語 Guava で行います。 Guavaの特徴の1つは、特定のスーパータイプのメソッドの実装を呼び出すことができることです。したがって、特別な構文なしで、どのスーパータイプ実装を「継承」するかを示すのは簡単です。

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

OrderedSetに独自のtoStringを指定しないと、コンパイルエラーが発生します。驚く様な事じゃない。

コレクションではMIが特に便利です。たとえば、配列や両端キューなどでRandomlyEnumerableSequenceを宣言しないようにするには、getEnumerator型を使用します。

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

MIがなかった場合は、いくつかのコレクションを使用するためにRandomAccessEnumeratorを作成できますが、簡単なgetEnumeratorメソッドを作成する必要があるため、ボイラープレートが追加されます。

同様に、MIは、コレクションのequalshashCodeおよびtoStringの標準実装を継承するのに役立ちます。

9
Daniel Lubarov

複数またはその他の継承はそれほど重要ではありません。異なるタイプの2つのオブジェクトが置換可能であれば、継承によってリンクされていなくても、それが重要です。

リンクリストと文字列はほとんど共通点がなく、継承によってリンクする必要はありませんが、length関数を使用していずれかの要素の数を取得できると便利です。

継承は、コードの繰り返し実装を回避するためのトリックです。継承によって作業が節約され、多重継承によって単一の継承に比べてさらに多くの作業が節約される場合は、それで十分です。

一部の言語は多重継承をあまりうまく実装していないと思います。それらの言語の実践者にとって、多重継承とはそういうことです。 C++プログラマーに多重継承について言及します。2つの異なる継承パスを介してクラスがベースの2つのコピーで終了するときの問題と、ベースクラスでvirtualを使用するかどうかが問題になります。デストラクタの呼び出し方法に関する混乱など。

多くの言語では、クラスの継承はシンボルの継承と融合しています。クラスBからクラスDを派生させると、型関係が作成されるだけでなく、これらのクラスが字句名前空間としても機能するため、B名前空間からD名前空間へのシンボルのインポートに加えて、タイプBおよびD自体で何が起こっているかのセマンティクス。したがって、多重継承はシンボルの衝突の問題をもたらします。 graphicメソッドを「持っている」card_deckdrawから継承する場合、結果のオブジェクトのdrawはどういう意味ですか?この問題のないオブジェクトシステムは、Common LISPのオブジェクトシステムです。おそらく偶然ではないかもしれませんが、LISPプログラムでは多重継承が使用されています。

不適切に実装され、不便なもの(多重継承など)は嫌われます

7
Kaz

私が知る限り、問題の一部(デザインを少し理解しにくくする(ただし、コーディングしやすくする)ことを除く)は、コンパイラがクラスデータ用に十分なスペースを節約し、これにより大量の次の場合のメモリの浪費:

(私の例は最善ではないかもしれませんが、同じ目的で複数のメモリ空間についての要点を取得しようとする、それは私の頭に浮かんだ最初のことでした:P)

クラス犬がカニヌスとペットから伸びている場合のDDDを考慮してください。カニヌスには、dietKgという名前で食べるべき食品の量を示す変数(整数)がありますが、ペットには通常、同じ目的で別の変数もあります。名前(別の変数名を設定しない限り、追加のコードを体系化します。これは最初の問題であり、回避したかったのですが、ブース変数の整合性を処理および保持するため)、正確な2つのメモリスペースがあります。同じ目的で、これを回避するには、同じ名前空間でその名前を認識するようにコンパイラを変更し、単一のメモリ空間をそのデータに割り当てるだけです。これは、残念ながらコンパイル時に決定することは不可能です。

もちろん、そのような変数がすでに別の場所で定義されたスペースを持っている可能性があることを指定するように言語を設計することもできますが、プログラマーは最後に、この変数が参照しているメモリスペースがどこにあるかを指定する必要があります(ここでも追加のコード)。

このすべてを真剣に考えて実装している人々を私に信じてください、しかしあなたが尋ねたのはうれしいです、あなたの親切な見方はパラダイムを変えるものです;)そしてこれを考えて、それは不可能ではないと言っています(しかし多くの仮定とマルチフェーズコンパイラーを実装する必要があり、本当に複雑なものです)、これはまだ存在しないと言っています。「これ」(多重継承)を実行できる独自のコンパイラーのプロジェクトを開始する場合は、私に知らせてください。あなたのチームに参加できてうれしいです。

1
Ordiel