多重継承を使用するのは良い概念ですか、それとも代わりに他のことを行うことができますか?
15 に答える
多重継承 (MI と略す)の匂いがします。つまり、通常、それは悪い理由で行われ、メンテナの前で吹き飛ばされます。
概要
- 継承ではなく機能の構成を検討する
- ダイヤモンド・オブ・ドレッドに注意
- オブジェクトの代わりに複数のインターフェイスの継承を検討する
- 場合によっては、多重継承が正しいこともあります。ある場合は、それを使用してください。
- コード レビューで多重継承アーキテクチャを防御する準備をする
1.おそらく構成?
これは継承にも当てはまり、多重継承にはさらに当てはまります。
あなたのオブジェクトは本当に別のものから継承する必要がありますか? は動作するために から継承する必要も、Car
から継承する必要もありません。Aには anと fourがあります。Engine
Wheel
Car
Engine
Wheel
コンポジションではなく多重継承を使用してこれらの問題を解決する場合は、何か問題があります。
2.恐怖のダイヤモンド
通常、クラスがA
ありB
、C
両方とも から継承しA
ます。そして (理由は聞かないでください) 誰かが と の両方を継承する必要があると判断しD
ます。B
C
私はこの種の問題に 8 8 年間で 2 回遭遇しました。
- 最初からどれだけの間違いがあったか (どちらの場合もと
D
の両方から継承するべきではなかった)、これは悪いアーキテクチャだったからです (実際には、まったく存在するべきではありませんでした...)B
C
C
A
C++ では、親クラスがその孫クラスに 2 回存在していたため、メンテナーがそれに対して支払った金額。D
したがって、1 つの親フィールドA::field
を更新することは、それを 2 回 ( および を介しB::field
てC::field
) 更新するか、何かが静かに間違って後でクラッシュすることを意味しました。 ( にポインタを新規作成しB::field
、削除C::field
...)
C++ でキーワード virtual を使用して継承を修飾すると、上記のダブル レイアウトが望ましくない場合に回避されますが、私の経験では、おそらく何か間違ったことをしている可能性があります...
オブジェクト階層では、階層をグラフとしてではなく、ツリー (ノードには 1 つの親があります) として維持するようにしてください。
ダイヤモンドの詳細 (2017-05-03 編集)
C++ でのDiamond of Dread の本当の問題 (設計が適切であると仮定して - コードをレビューしてください! ) は、選択を行う必要があることです:
- クラスがレイアウトに 2 回存在することは望ましいこと
A
ですか? また、それはどういう意味ですか? はいの場合は、必ず 2 回継承します。 - 一度だけ存在する必要がある場合は、仮想的に継承します。
この選択は問題に固有のものであり、C++ では、他の言語とは異なり、言語レベルで設計をドグマに強制することなく実際に行うことができます。
しかし、すべての力と同様に、その力には責任が伴います。設計をレビューしてもらいます。
3. インターフェース
上記のDiamond of Dreadに遭遇しないため、ゼロまたは1つの具象クラスとゼロまたは複数のインターフェースの多重継承は通常問題ありません。実際、これが Java での処理方法です。
通常、C がA
とを継承するというB
ことは、ユーザーがC
それを であるかのようにA
、および/または であるかのように使用できるということB
です。
C++ では、インターフェースは以下を持つ抽象クラスです。
そのすべてのメソッドが純粋な仮想として宣言されました (接尾辞 = 0)(2017-05-03 を削除)- メンバー変数なし
ゼロから 1 つの実オブジェクトへの多重継承、およびゼロ以上のインターフェースは、「臭い」とは見なされません (少なくとも、それほどではありません)。
C++ 抽象インターフェイスの詳細 (2017-05-03 編集)
最初に、NVI パターンを使用してインターフェイスを作成できます。これは、実際の基準は状態を持たないこと(つまり、 以外のメンバ変数がないことthis
) であるためです。あなたの抽象的なインターフェイスのポイントは、コントラクトを公開することです (「あなたは私をこのように、そしてこのように呼ぶことができます」)、それ以上でもそれ以下でもありません。抽象仮想メソッドのみを持つという制限は、義務ではなく、設計上の選択であるべきです。
第 2 に、C++ では、抽象インターフェイスから仮想的に継承することは理にかなっています (追加のコスト/間接性があっても)。そうしないと、インターフェイスの継承が階層内に複数回出現すると、あいまいさが生じます。
第 3 に、オブジェクト指向は優れていますが、C++の唯一の真実ではありません。適切なツールを使用し、C++ にはさまざまな種類のソリューションを提供する他のパラダイムがあることを常に覚えておいてください。
4.多重継承は本当に必要ですか?
時々、はい。
通常、あなたのC
クラスは と から継承されA
、B
とA
はB
2 つの無関係なオブジェクトです (つまり、同じ階層にない、共通点がない、概念が異なるなど)。
たとえば、Nodes
X、Y、Z 座標を使用して多くの幾何学的計算 (おそらく点、幾何学的オブジェクトの一部) を実行できるシステムを持つことができ、各ノードは自動化されたエージェントであり、他のエージェントと通信できます。
おそらく、それぞれが独自の名前空間を持つ 2 つのライブラリに既にアクセスしている可能性があります (名前空間を使用するもう 1 つの理由... しかし、名前空間を使用していますよね?) geo
。ai
own::Node
したがって、 と の両方から派生した独自のものがありai::Agent
ますgeo::Point
。
これは、代わりに構成を使用する必要があるかどうかを自問する必要がある瞬間です。own::Node
が本当に aai::Agent
と a の両方である場合geo::Point
、合成はうまくいきません。
own::Node
次に、3D 空間での位置に応じて他のエージェントと通信するために、複数の継承が必要になります。
( ai::Agent
andgeo::Point
は完全に、完全に、完全に無関係であることに注意してください...これにより、多重継承の危険性が大幅に減少します)
その他のケース (2017-05-03 編集)
他にも次のような場合があります。
- 実装の詳細として (できれば非公開の) 継承を使用する
- ポリシーのような一部の C++ イディオムは、複数の継承を使用できます (各部分が を介して他の部分と通信する必要がある場合
this
) 。 - std::exception からの仮想継承 ( Is Virtual Inheritance for Exceptions? )
- 等
コンポジションを使用できる場合もあれば、MI の方が優れている場合もあります。要点: あなたには選択肢があります。責任を持って実行してください (そしてコードをレビューしてもらいます)。
5. では、多重継承を行うべきですか?
ほとんどの場合、私の経験では、いいえ。MI は、機能しているように見えても、適切なツールではありません。怠惰な人が結果に気付かずに機能を積み上げるために使用できるためです ( aと a のCar
両方を作成するEngine
などWheel
)。
しかし、時には、はい。そしてその時、MIほどうまくいくものはありません。
しかし、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;
常に設計を見直し、データの再利用を節約するために継承を使用していないことを確認してください。コンポジションで同じものを表現できる場合 (そして通常はできる場合)、これははるかに優れたアプローチです。
w:多重継承を参照してください。
多重継承は批判を受けており、多くの言語では実装されていません。批判には次のものが含まれます。
- 複雑さの増大
- 意味のあいまいさは、ダイヤモンドの問題として要約されることがよくあります。
- 単一のクラスから複数回明示的に継承できない
- クラスのセマンティクスを変更する継承の順序。
C++/Java スタイルのコンストラクターを使用する言語での多重継承は、コンストラクターとコンストラクター チェーンの継承の問題を悪化させ、その結果、これらの言語でメンテナンスと拡張性の問題が生じます。構築方法が大きく異なる継承関係にあるオブジェクトは、コンストラクター チェーン パラダイムの下で実装するのは困難です。
これを解決する最新の方法は、COM や Java インターフェイスのようなインターフェイス (純粋な抽象クラス) を使用することです。
これの代わりに他のことができますか?
はい、できます。GoFから盗むつもりです。
- 実装ではなくインターフェイスへのプログラム
- 継承より合成を好む
パブリック継承は IS-A 関係であり、クラスが複数の異なるクラスの型になる場合があり、これを反映することが重要な場合があります。
「ミックスイン」も時々役に立ちます。これらは一般に小さなクラスであり、通常は何も継承せず、便利な機能を提供します。
継承階層がかなり浅く (ほとんどの場合そうであるべきです)、適切に管理されている限り、恐ろしいダイヤモンド継承を取得する可能性は低くなります。ひし形は、多重継承を使用するすべての言語で問題になるわけではありませんが、C++ でのひし形の扱いはしばしばぎこちなく、場合によっては不可解です。
多重継承が非常に便利なケースに出くわしましたが、実際にはかなりまれです。これは、多重継承が本当に必要ない場合に、他の設計方法を使用することを好むためと考えられます。私は紛らわしい言語構造を避けたいと思っています.何が起こっているのかを理解するためにマニュアルをよく読まなければならない継承のケースを構築するのは簡単です.
多重継承を「回避」するべきではありませんが、「ダイヤモンドの問題」( http://en.wikipedia.org/wiki/Diamond_problem ) などの発生する可能性のある問題に注意し、与えられた権限を慎重に扱う必要があります。 、すべての力でそうする必要があります。
慎重に使用する必要があります。ダイヤモンド問題のように、物事が複雑になる場合があります。
(出典:learncpp.com)
エッフェルを使用しています。私たちは優れた MI を持っています。心配ない。問題ない。簡単に管理できます。MI を使用しない場合もあります。ただし、次の理由により、人々が認識している以上に有用です: A) うまく管理できない危険な言語で - または - B) 何年にもわたって MI を回避してきた方法に満足している - または - C) その他の理由 (リストするには多すぎると確信しています-上記の回答を参照してください)。
Eiffel を使用している私たちにとって、MI は他の何よりも自然であり、ツールボックスの優れたツールの 1 つです。率直に言って、他の誰も Eiffel を使用していないことにはまったく関心がありません。心配ない。私たちは私たちが持っているものに満足しており、ぜひご覧ください。
探している間: Void 安全性と Null ポインター逆参照の根絶に特に注意してください。私たちは皆、MI の周りで踊っていますが、ポインタは失われています! :-)
すべてのプログラミング言語には、長所と短所があるオブジェクト指向プログラミングの扱いがわずかに異なります。C++ のバージョンは、パフォーマンスを真っ向から重視しており、無効なコードを驚くほど簡単に記述できるという欠点があります。これは多重継承にも当てはまります。結果として、プログラマーをこの機能から遠ざける傾向があります。
他の人々は、多重継承が良くないという問題に取り組んでいます。しかし、多かれ少なかれ、それを避ける理由は安全ではないということをほのめかしているコメントがかなり多く見られました. はい、いいえ。
C++ ではよくあることですが、基本的なガイドラインに従えば、常に「肩越しに見る」必要なく安全に使用できます。重要なアイデアは、「ミックスイン」と呼ばれる特別な種類のクラス定義を区別することです。すべてのメンバー関数が仮想 (または純粋仮想) である場合、クラスはミックスインです。次に、1 つのメイン クラスと好きなだけ「ミックスイン」を継承できますが、「仮想」キーワードを使用してミックスインを継承する必要があります。例えば
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 つにすぎません。
この記事は、継承とその危険性をうまく説明しています。
ダイアモンド パターンを超えて、多重継承はオブジェクト モデルを理解しにくくする傾向があり、その結果、メンテナンス コストが増加します。
構成は本質的に理解しやすく、理解しやすく、説明しやすいものです。コードを書くのは面倒かもしれませんが、優れた IDE (私が Visual Studio を使い始めてから数年になりますが、Java IDE にはすべて優れたコンポジション ショートカット自動化ツールがあることは確かです) があれば、そのハードルを乗り越えることができます。
また、メンテナンスの観点から、「ダイヤモンドの問題」は、非リテラル継承のインスタンスでも発生します。たとえば、A と B があり、クラス C がそれらの両方を拡張し、A にオレンジ ジュースを作成する 'makeJuice' メソッドがあり、それを拡張してライムを加えたオレンジ ジュースを作成するとします。 B' は、電流を生成する 'makeJuice' メソッドを追加しますか? 「A」と「B」は今のところ互換性のある「親」である可能性がありますが、常にそうであるとは限りません。
全体として、継承、特に多重継承を避ける傾向があるという格言は健全です。すべての格言と同様に、例外はありますが、コーディングするすべての例外を指し示す緑色のネオンサインが点滅していることを確認する必要があります (そして、そのような継承ツリーを見つけたときはいつでも独自の点滅する緑色のネオンで描くように脳を訓練する必要があります)。サイン)、時々チェックして、すべてが理にかなっていることを確認してください。
具体的なオブジェクトの MI に関する重要な問題は、正当に「A でありかつ B である」必要があるオブジェクトがめったにないことです。そのため、論理的な根拠に基づいて正しい解決策になることはめったにありません。多くの場合、「C は A または B として機能する」に従うオブジェクト C を持っています。これは、インターフェイスの継承と合成によって実現できます。ただし、間違いはありません。複数のインターフェイスの継承は、依然として MI であり、そのサブセットにすぎません。
特に C++ の場合、この機能の主な弱点は複数の継承が実際に存在することではなく、ほとんどの場合不正な形式であるいくつかの構成要素を許可することです。たとえば、次のように同じオブジェクトの複数のコピーを継承します。
class B : public A, public A {};
は定義により不正な形式です。これを英語に訳すと「B is an A and an A」です。ですから、人間の言葉でさえ、深刻な曖昧さがあります。「B には 2 つの As がある」という意味ですか、それとも単に「B は A です」という意味ですか? そのような異常なコードを許可し、さらに悪いことにそれを使用例にすることは、後継言語で機能を維持することを主張することになると、C++ に有利に働きませんでした。
継承よりも構成を使用できます。
全体的な感想としては、構成の方が優れており、非常によく議論されています。
関連するクラスごとに 4/8 バイトが必要です。(クラスごとに 1 つの this ポインター)。
これは決して問題にならないかもしれませんが、ある日、何十億回もインスタンス化されるマイクロデータ構造があるとしたら、それは問題になるでしょう。