回答:
多重継承(MIと略記)はにおいがします。つまり、通常、それは悪い理由で行われ、メンテナーの顔に吹き返します。
これは継承にも当てはまるので、多重継承にも当てはまります。
オブジェクトは本当に別のものから継承する必要がありますか?A Car
はからEngine
、またはから継承する必要はありませんWheel
。A Car
にはEngine
と4つありWheel
ます。
構成の代わりに多重継承を使用してこれらの問題を解決すると、何か問題が発生します。
通常、あなたはクラス持ちA
、その後、B
とC
の両方から継承しますA
。そして、(理由は聞かないでください)次にD
、B
との両方を継承する必要があると誰かが決定しC
ます。
私はこの種類の問題に8と8年の間に2回遭遇しました。
D
両方から継承されていないはずB
とC
、これは悪いアーキテクチャだったので、)(実際には、C
...すべてに存在していてはいけません)A
がその孫クラスに2回存在していたため、メンテナがいくら払ったかということです。D
したがって、1つの親フィールドA::field
を更新すると、それを2回更新する(B::field
およびを介してC::field
)か、何かが黙って間違ってクラッシュしてしまうことになります。 (に新しいポインタB::field
、および削除C::field
...)C ++でキーワードvirtualを使用して継承を修飾すると、これが必要なものでない場合は、上記の二重レイアウトを回避できますが、とにかく、私の経験では、おそらく何か間違ったことをしています...
オブジェクト階層では、階層をグラフではなくツリー(ノードには1つの親があります)として維持するようにしてください。
C ++でのDiamond of Dreadの本当の問題(設計が適切であると想定-コードをレビューしてもらいます!)は、選択する必要があることです。
A
がレイアウトに2回存在することが望ましいですか、それはどういう意味ですか?はいの場合は、必ず2回継承します。この選択は問題に固有であり、C ++では、他の言語とは異なり、言語レベルで設計を強制することなく実際に行うことができます。
しかし、すべての権限と同様に、その権限には責任があります。設計をレビューしてもらいます。
ゼロまたは1つの具象クラス、および0以上のインターフェースの多重継承は通常、問題ありません。これは、上記のダイヤモンドの恐怖に遭遇しないためです。実際、これがJavaで行われる方法です。
通常、Cが継承したときの意味A
とB
は、ユーザーC
がであるA
、またはであるかのようにユーザーが使用できるということB
です。
C ++では、インターフェースは次のものを含む抽象クラスです。
ゼロから1つの実オブジェクト、およびゼロ以上のインターフェースの多重継承は、「臭い」とは見なされません(少なくとも、それほどではありません)。
まず、NVIパターンを使用してインターフェイスを作成できます。これは、実際の基準が状態を持たない(つまり、以外のメンバー変数がないthis
)ためです。あなたの抽象的なインターフェースのポイントは、契約を公開することです(「あなたは私をこのように、そしてこのように呼ぶことができます」)、それ以上でもそれ以下でもありません。抽象仮想メソッドのみを持つという制限は、設計上の選択であり、義務ではありません。
第2に、C ++では、抽象インターフェイスから仮想的に継承することは理にかなっています(追加のコスト/間接参照があっても)。そうでない場合、インターフェイスの継承が階層内に複数回出現すると、あいまいになります。
第3に、オブジェクト指向は優れていますが、C ++のThe Only Truth Out There TMではありません。適切なツールを使用し、C ++にはさまざまな種類のソリューションを提供する他のパラダイムがあることを常に覚えておいてください。
時々、はい。
通常、あなたのC
クラスから継承しているA
とB
し、A
そしてB
2つの無関係なオブジェクト(すなわちないなどと同じ階層に、共通して何も、異なる概念、中)です。
たとえば、Nodes
X、Y、Z座標のシステムを使用して、多くの幾何学的計算(おそらくポイント、幾何学的オブジェクトの一部)を実行でき、各ノードは他のエージェントと通信できる自動エージェントです。
おそらく、あなたはすでに2つのライブラリ、独自の名前空間を持つ各へのアクセス持っている(名前空間を使用する別の理由を...しかし、あなたは、あなたの名前空間をしていないでしょう?)、1ビーイングgeo
や他のビーイングをai
したがってown::Node
、ai::Agent
との両方から独自に派生させることができますgeo::Point
。
これは、代わりにコンポジションを使用するべきではないかどうかを自問する必要がある瞬間です。own::Node
本当にa ai::Agent
とaの両方である場合geo::Point
、合成は行われません。
次にown::Node
、3D空間での位置に応じて他のエージェントと通信できるように、複数の継承が必要になります。
(あなたはそれに気づくでしょう、ai::Agent
そしてgeo::Point
完全に、完全に、完全に無関係です...これは多重継承の危険を劇的に減らします)
他の場合があります:
this
)コンポジションを使用できる場合もあれば、MIが優れている場合もあります。ポイントは次のとおりです。あなたには選択肢があります。責任を持って実行してください(そして、コードを確認してください)。
ほとんどの場合、私の経験では、違います。MIは、それが山に怠惰で使用することができますので、それは(作るような結果を実現することなく一緒に備えて、仕事に思える場合でも、適切なツールではありませんCar
両方Engine
とWheel
)。
しかし、時々、はい。そして、その時、MIより良いものは何もありません。
しかし、MIは臭いので、コードレビューでアーキテクチャを守る準備をしてください(そして、それを守ることができない場合は、行うべきではないので、それを守ることは良いことです)。
多重継承でできることはすべて単一継承でもできるので、人々は多重継承は必要ないとかなり正確に言っています。あなたは私が述べた委任トリックを使うだけです。さらに、継承はまったく必要ありません。単一継承で行うことはすべて、クラスを介して転送することで継承なしでも行うことができるためです。実際には、ポインタとデータ構造ですべてを実行できるため、クラスも必要ありません。しかし、なぜそれをしたいのでしょうか?言語機能はいつ使用すると便利ですか。いつ回避策を使用しますか?多重継承が役立つケースを見たことがありますし、かなり複雑な多重継承が役に立つケースを見たこともあります。一般に、私はこの言語が提供する機能を使用して回避策を実行することを好みます
const
- クラスが実際に可変および不変のバリアントを必要とする場合、不自然な回避策(通常はインターフェイスと構成を使用)を記述しなければなりませんでした。ただし、一度も多重継承を見逃したことはありません。また、この機能がないために回避策を書かなければならないと感じたこともありません。それが違いです。私が今までに見たどの場合でも、MIを使用しない方が設計上の選択であり、回避策ではありません。
これを回避する理由はなく、状況によっては非常に役立ちます。ただし、潜在的な問題に注意する必要があります。
最大のものは死のダイヤモンドです:
class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;
これで、Child内にGrandParentの「コピー」が2つあります。
C ++はこのことを考慮しており、問題を回避するために仮想継承を行うことができます。
class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;
常に設計を見直し、継承を使用してデータの再利用を節約していないことを確認してください。同じことを構成で表すことができる場合(通常は可能)、これははるかに優れたアプローチです。
GrandParent
ありChild
ます。人々は言語のルールを理解していないかもしれないと思っているだけなので、MIへの恐れがあります。しかし、これらの単純なルールを取得できない人は、重要なプログラムを作成することもできません。
「w:多重継承」を参照してください。
多重継承は批判を受けているため、多くの言語では実装されていません。批判は次のとおりです。
C ++ / Javaスタイルのコンストラクターを使用する言語で複数の継承を行うと、コンストラクターの継承問題とコンストラクターチェーンが悪化し、これらの言語でメンテナンスと拡張性の問題が発生します。大きく変化する構築メソッドと継承関係にあるオブジェクトは、コンストラクターチェーンパラダイムの下で実装するのが困難です。
これを解決してCOMおよびJavaインターフェースのようなインターフェース(純粋な抽象クラス)を使用するための最新の方法。
これの代わりに他のことができますか?
はい、できます。GoFから盗みます。
パブリック継承はIS-A関係であり、クラスはいくつかの異なるクラスのタイプになる場合があり、これを反映することが重要な場合があります。
「ミックス」も時々役立ちます。これらは一般に小さなクラスであり、通常は何からも継承せず、便利な機能を提供します。
継承階層がかなり浅く(ほとんどの場合そうであるはずです)、適切に管理されている限り、恐ろしいひし形の継承を取得することはほとんどありません。ダイヤモンドは、多重継承を使用するすべての言語に問題があるわけではありませんが、C ++によるダイヤモンドの扱いは、しばしば厄介で、時として不可解です。
多重継承が非常に便利な場合に遭遇しましたが、実際にはかなりまれです。これは、多重継承が本当に必要ないときに、他の設計方法を使用したいためです。私は言語の構成を混乱させないようにしていますが、何が起こっているのかを理解するためにマニュアルをよく読まなければならない継承のケースは簡単に構成できます。
多重継承を「避ける」べきではありませんが、「ダイヤモンドの問題」(http://en.wikipedia.org/wiki/Diamond_problem)などの発生する可能性のある問題に注意し、与えられた力を注意深く扱う必要があります、すべての権限で必要なように。
少し抽象的になるリスクがありますが、カテゴリー理論の枠内で継承について考えるのは明るいと思います。
すべてのクラスとそれらの間の矢印が継承関係を表すと考えると、このようなもの
A --> B
はからclass B
派生することを意味しますclass A
。与えられたことに注意してください
A --> B, B --> C
CはAから派生するBから派生するため、CもAから派生すると言われます。
A --> C
さらに、A
ささいにA
から派生するすべてのクラスについてA
、継承モデルはカテゴリの定義を満たします。より伝統的な言語ではClass
、オブジェクトにはすべてのクラスと継承関係の射が含まれるカテゴリがあります。
これは少し設定ですが、それでは、Diamond of Doomを見てみましょう。
C --> D
^ ^
| |
A --> B
これは怪しげな見た目ですが、実際はそうです。だから、D
全てからの継承A
、B
とC
。さらに、OPの質問への対応に近づくとD
、のスーパークラスも継承されますA
。図を描くことができます
C --> D --> R
^ ^
| |
A --> B
^
|
Q
ここで、Diamond of Deathに関連する問題は、プロパティとメソッドの名前を共有しているときにC
、B
あいまいになる場合です。ただし、共有の動作を移動するとA
、あいまいさがなくなります。
カテゴリ用語で言えば、私たちが望むA
、B
とC
あれば、そのようなことをするB
とC
から継承Q
その後は、A
のサブクラスとしてのように書き換えることができますQ
。これはA
、プッシュアウトと呼ばれるものになります。
プルバックD
と呼ばれる対称構造もあります。これは本質的に、との両方を継承して構築できる最も一般的な有用なクラスです。あなたが他のクラスにしている場合つまり、乗算から継承すると、そのクラスでのサブクラスとしてのように書き換えることができますが。B
C
R
B
C
D
R
D
ダイアモンドのヒントがプルバックとプッシュアウトであることを確認することで、そうでない場合に発生する可能性のある名前の競合やメンテナンスの問題を一般的に処理するための優れた方法が得られます。
注 Paercebalの答えは、彼の忠告は、我々はすべての可能なクラスの完全なカテゴリクラスで働くことを考えると上記のモデルによって暗示されているとして、これを触発しました。
彼の議論を一般化して、複雑な多重継承関係がどれほど強力で問題のないものになるかを示しました。
TL; DRプログラムの継承関係は、カテゴリを形成するものと考えてください。次に、多重継承されたクラスを対称的にプッシュアウトし、プルバックである共通の親クラスを作成することにより、Diamond of Doomの問題を回避できます。
エッフェルを使用しています。私たちは素晴らしいMIを持っています。心配ない。問題ない。簡単に管理できます。MIを使用しない場合があります。ただし、次の理由により、人々が気付く以上に役立ちます:A)うまく管理できない危険な言語-OR- B)何年にもわたってMIを回避してきた方法に満足している-または-C)その他の理由(リストするには数が多すぎるので、確信があります-上記の回答を参照してください)。
私たちにとって、Eiffelを使用すると、MIは他の何よりも自然であり、ツールボックス内の別の優れたツールでもあります。率直に言って、他の誰もがエッフェルを使用していないことを私たちはまったく心配していません。心配ない。私たちは私たちが持っているものに満足しており、あなたに見てもらうことを勧めます。
あなたが見ている間:Void-safetyとNullポインターの逆参照の根絶に特別な注意を払ってください。私たちはみんなMIの周りを踊っていますが、あなたのポインタは失われています!:-)
すべてのプログラミング言語には、長所と短所を持つオブジェクト指向プログラミングの扱いがわずかに異なります。C ++のバージョンは、パフォーマンスに真っ向から重点を置いており、それに伴い、無効なコードを作成するのが邪魔になりやすいという欠点があります。これは多重継承にも当てはまります。結果として、プログラマーをこの機能から遠ざける傾向があります。
他の人々は、多重継承が何のために良くないかという問題に対処しました。しかし、多かれ少なかれそれを避ける理由が安全ではないということを暗示するかなりの数のコメントを見てきました。はい、そうです。
C ++ではよくあることですが、基本的なガイドラインに従えば、常に「肩越しに見渡す」ことなく安全に使用できます。重要なアイデアは、「ミックスイン」と呼ばれる特別な種類のクラス定義を区別することです。すべてのメンバー関数が仮想(または純粋仮想)の場合、クラスはミックスインです。その後、単一のメインクラスと必要な数の「ミックスイン」から継承できますが、「仮想」というキーワードでミックスインを継承する必要があります。例えば
class CounterMixin {
int count;
public:
CounterMixin() : count( 0 ) {}
virtual ~CounterMixin() {}
virtual void increment() { count += 1; }
virtual int getCount() { return count; }
};
class Foo : public Bar, virtual public CounterMixin { ..... };
私の提案は、クラスをミックスインクラスとして使用する場合は、命名規則を採用して、コードを読んでいる人が何が起こっているのかを簡単に確認し、基本的なガイドラインのルールに従ってプレイしていることを確認できるようにすることです。 。また、仮想ベースクラスの動作方法が原因で、ミックスインにもデフォルトのコンストラクターがある場合は、はるかにうまく機能することがわかります。また、すべてのデストラクタを仮想化することも忘れないでください。
ここでの「ミックスイン」という言葉の使い方は、パラメータ化されたテンプレートクラスと同じではないことに注意してください(適切な説明については、このリンクを参照してください)。用語の公平な使い方だと思います。
これが、多重継承を安全に使用する唯一の方法であるという印象を与えたくありません。これは、かなり簡単に確認できる1つの方法です。
慎重に使用する必要があります。DiamondProblemのように、状況が複雑になる場合があります。
(ソース:learncpp.com)
Printer
べきではありませんPoweredDevice
。A Printer
は印刷用であり、電源管理用ではありません。特定のプリンターの実装は、いくつかの電源管理を行う必要があるかもしれませんが、これらの電源管理コマンドは、プリンターのユーザーに直接公開されるべきではありません。この階層を実際に使用することは想像できません。
ひし形パターンを超えると、多重継承によりオブジェクトモデルが理解しにくくなる傾向があり、その結果、メンテナンスコストが増加します。
構成は本質的に理解、理解、説明が簡単です。コードを書くのは退屈な作業ですが、優れたIDE(Visual Studioを使用してから数年になりますが、Java IDEにはすばやいコンポジションショートカット自動化ツールがあります)なら、そのハードルを乗り越えられるはずです。
また、メンテナンスの面では、非リテラル継承インスタンスでも「ダイヤモンドの問題」が発生します。たとえば、AとBがあり、クラスCがそれらを両方とも拡張し、Aがオレンジジュースを作る「makeJuice」メソッドを持っている場合、ライムをひねったオレンジジュースを作るためにそれを拡張します。 B 'は、電流を生成する「makeJuice」メソッドを追加しますか?「A」と「B」は現在互換性のある「親」である可能性がありますが、必ずしもそうであるとは限りません。
全体として、継承、特に多重継承を避ける傾向の格言は健全です。すべての格言として、例外がありますが、コード化する例外を指す点滅する緑のネオンサインがあることを確認する必要があります(そして、このような継承ツリーを見るたびに、点滅する緑のネオンで描くように脳を訓練します記号)、そして時々すべてが理にかなっていることを確認するためにチェックすること。
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
もちろん、コンパイルエラーが発生します(使用法があいまいな場合)。
具象オブジェクトのMIの重要な問題は、正当に「Aであり、Bになる」必要があるオブジェクトがないことです。そのため、論理的な理由でそれが正しい解決策になることはほとんどありません。はるかに多くの場合、「CはAまたはBとして機能できる」に従うオブジェクトCがあり、これはインターフェイスの継承と構成を介して実現できます。しかし、間違いはありません。複数のインターフェースの継承は依然としてMIであり、そのサブセットにすぎません。
特にC ++の場合、この機能の主な弱点は、多重継承の実際の存在ではありませんが、許可されているいくつかの構成体は、ほとんど常に不正な形式です。たとえば、次のような同じオブジェクトの複数のコピーを継承します。
class B : public A, public A {};
BY DEFINITIONの形式が正しくありません。これは英語に翻訳された「B is A and A」です。したがって、人間の言語でさえ、深刻なあいまいさが存在します。「B has 2 As」または単に「B is a A」ですか?そのような病理学的コードを許可し、さらに悪いことにそれを使用例にすると、C ++は、後続の言語で機能を維持するための主張をすることに関して不利に働きました。
継承よりも構成を使用できます。
一般的な感じとしては、構成が優れているということであり、非常によく議論されています。
The general feeling is that composition is better, and it's very well discussed.
構成が優れているという意味ではありません。
関係するクラスごとに4/8バイトかかります。(クラスごとに1つのthisポインター)。
これは問題になることは決してないかもしれませんが、ある日、数十億回インスタンス化されるマイクロデータ構造がある場合は、そのようになります。