経験則として、一般化は特定の状況でのみ使用されます。たとえば、Xが文字通りYのサブクラスであると言える場合、馬は哺乳類のサブクラスであると喜んで言えます。私は常に、ここで一般化と継承を使用する必要があると信じるように導かれてきました。 2つのオブジェクト間にこの厳密な対応がない場合、そうするべきではありません。
馬は哺乳類です。しかし、それは文字通り交通手段でもあります。では、馬が両方から継承できない単一の継承の世界ではどうなるでしょうか。次に、馬が文字通り実現への輸送手段、つまりITransportableなどのインターフェースであるという事実を覆しますか?
ここで重要なのは、(単一の)継承は希少なリソースであることです。スーパークラスは1つだけです。ドメインを検討することで、最適な割り当て方法を決定するのに役立ちます。分類プログラムを作成していますか?次に、おそらくMammalをサブクラス化します。交通プログラムを書いていますか?次に、おそらくVehicle(または何か)をサブクラス化する必要があります。
馬が哺乳類のタイプであり、輸送装置のタイプでもあるからといって、両方の概念がドメイン内で等しく価値があるとは限りません。
GoFの本からの「継承よりも好ましい構成」の格言も考慮してください。おそらくその本から出てくるのに最適なものです。
「哺乳類である」が実際に何を意味するか考えてください。グーグルから:
毛や毛皮の所持、雌による乳汁の分泌、そして(通常)生きている若い動物の誕生によって区別されるクラスの温血脊椎動物。
したがって、プログラミングのコンテキストでは、GrowHairメソッドが含まれている可能性があります(今のところ生殖システムを忘れてみましょう。これは非常に複雑です)。しかし、GrowHairの実装には、さまざまな種類の哺乳類に共通するものはほとんどありません。
ユニバースクラスは、タイマーループの各哺乳類でGrowHairを呼び出す場合がありますが、実装、つまり人間と犬が髪を育てるさまざまな方法について心配する必要はありません。人間と犬のクラスにそれを処理させてください。それは、各哺乳類が育毛のプロパティを持っていることを知りたいだけです。これは、GrowHairメソッドを通じてアクティブ化できます。
突然、これは基本クラス自体ではなく、インターフェース(哺乳類と宇宙の間の契約であり、哺乳類は実装に関係なく特定の機能を実行します)です。
これはOOが移動した方向です。多重継承により多くの問題が発生し、ほとんどすべての場合、1つを除くすべての基本クラスが実装なしで存在する可能性がありました(特に、 「継承よりも構成を優先する」アプローチ)。そのため、インターフェイスは代替として生まれました。
簡単に言えば、インターフェースはコントラクト用であり、クラスは実装用です。
インターフェースの継承は型階層用であり、クラスの継承(および構成)はコードの再利用用です。
複数のis-a関係は単一の継承で問題を引き起こしますが、多くの場合、関係はそれほど難しくありません。たとえば、ITransportationインターフェイスを使用します。これは、さまざまな形態の交通機関が同じように動作しないためです。多重継承が利用可能であったとしても、私はこれを行います。現実の世界には「交通手段」のクラスはなく、当然のことながらインターフェースです。
多くの場合、多重継承はオブジェクトの処理が多すぎることを示していると思いますが、ケースバイケースで検討する必要があります。重要なのは、多重継承を使用して、多くの動作を1つのクラスにドラッグしないようにすることです。1つのクラスは、設計の責任を負います。
本当に単一継承の世界でクラスに複数の継承を行わせたい場合でも、クラスにインターフェイスを実装させ、その実装を内部オブジェクトに委任するオプションがあります。
より一般的な概念である類似性について話しましょう。
[〜#〜] b [〜#〜] は [〜#〜のサブクラスです。 ] a [〜#〜] は、 [〜#〜] b [〜#〜] が- [〜#〜] a [〜#〜] (ただし、それではない [〜#〜] a [〜 #〜] は [〜#〜] b [〜#〜] に似ています-したがって、サブタイプは not symmetric)です。
[〜#〜] b [〜#〜] が [〜#〜] a [ 〜#〜] 次に、どこでも [〜#〜] a [〜#〜] 使用できるa [〜#〜] b [〜#〜] 。
多重継承とは、 [〜#〜] b [〜#〜] は [ 〜#〜] a [〜#〜] と [〜#〜] b [〜#〜] は似ていますto [〜#〜] c [〜#〜] 。 [〜#〜] a [〜#〜] および [である限り、これは問題ではありません。 〜#〜] c [〜#〜] は互いに異なるではありません。それらが異なる場合、私は基本的に [〜#〜] b [〜#〜] は近くにない2つのものに近いと言っていますお互い、矛盾。
したがって、多重継承が機能するには、 [〜#〜] a [〜#〜] および [〜 #〜] c [〜#〜] 互いに類似している必要があるか、相互に直交している必要があります(つまり、異なる空間に住んでいて、互いに関係がありません)。
さて、サブタイピングは対称的ではないため、相互に類似させることはできません( [〜#〜] a [〜#〜] および- [〜#〜] c [〜#〜] 両方を互いに継承することはできません)。
つまり、相互に直交している必要があります。つまり、共通のスペースがありません。しかし、これは脆弱です。なぜなら、直交として開始したとしても、それぞれが同じ名前と署名を持つメソッドを取得すると、これは壊れます(つまり、同じ名目上のスペース、つまり名前のスペースになります)。また、ある時点で、それぞれが同じ基本クラスから継承することになった場合にも破損する可能性があります(菱形継承問題)。
したがって、実際には、一般的な場合に当てはまるのは単一の継承です。
質問に対する回答は十分に得られていますが、寄付を差し上げます。つまり、プログラマーは、アーキテクチャーではなく分類学でアーキテクチャーを考える傾向があります。つまり、プログラマーは、オブジェクトの構造ではなく、オブジェクトの分類を探す傾向があります。これはおそらく実生活に起因している可能性があります。人に会ったとき、あなたはそれらを
これは、私たちの脳が女性の見方を処理するときに約1秒以内にトリガーされます。あなたはクラス階層になるでしょう:
_class Woman {}
class WhiteWoman extends Woman {}
class PrettyWhiteWoman extends WhiteWoman {}
class NicelyDressedPrettyWhiteWoman extends PrettyWhiteWoman {}
_
さて、このアーキテクチャが非常に素朴であることはかなり明白ですが、階層がそれほど簡単に定義されていない場合、おそらくこれらの設計ミスを犯します。
より再利用可能なコードを書くためには、分類したいという脳の欲求と戦い、代わりに構成を試みなければなりません。彼らが言うように「継承をめぐる構成」。
あなたの例では、馬は「哺乳類」であり、馬は「輸送手段」(つまり、延長)であると考えています。実際、馬は哺乳類の生物学を「持って」おり、馬は輸送手段として機能する能力を持っていると考える方が正しい。したがって、HorseはMammalable
およびTransporter
インターフェースコントラクトを実装する必要があります。
ここで、いくつかの階層をどのように構成するかについて論争の的になります。たとえば、私たちの農場にはたくさんのFourLeggedAnimal
sがいて、それぞれが4本足の兄弟と同じ動作を示すとします。私たちの農場には、雑食動物もいます。雑食動物も同様の行動を示します。問題は、一部の動物が4本足である以上、雑食動物ではないということです。 FourLeggedAnimal
をOmnivore
から拡張したり、その逆を行ったりしても意味がありません。この競合の例は、私たちの農場に人間と豚の両方がいる場合です。
代わりに、アーキテクチャを再考する必要があります。オブジェクトについては、上から下ではなく、内側から考える必要があります。 「何が馬を定義するのか」を考えるのではなく、「何が馬を構成するのか」を考える必要があります。私たちの農場の文脈では、馬のこの定義は最も理にかなっているかもしれません(phpベースの疑似コード):
_interface Mammal {
function growHair();
}
interface Transporter {
function loadUp(bulk);
function moveOut(destination);
}
class FourLeggedAnimal {
public function walk() {
echo "Going for a walk";
}
}
class Omnivore {
public function eat(Food food) {
echo "Enjoying some food";
}
}
class Herbivore {
public function eat(Meat food) {
echo "Blech!";
}
public function eat(Veggies food) {
echo "Nom nom nom";
}
}
class Horse implements Mammal, Transporter {
private FourLeggedAnimal;
private Herbivore;
//Mammal/Transporter methods omitted
public function __construct(FourLeggedAnimal fla, Herbivore h) {
this.FourLeggedAnimal = fla;
this.Herbivore = h;
}
public function eat(Food food) {
this.Herbivore.eat(food);
}
public function walk() {
this.FourLeggedAnimal.walk();
}
}
_
この例は非常に素朴で、おそらくいくつかの改善を使用できますが、概念がそこにあることを願っています。 「is a」という考え方から遠ざかったので、私たちのアーキテクチャはより堅実です。馬の依存関係は逆転しているので、リスコフの置換違反などを気にする必要はありません。
このアーキテクチャの唯一の欠点は、大きな問題ですが、コードが重複していることです。 Horse
の動作を示すためだけにFourLeggedAnimal
のwalk()
メソッド全体が本当に必要ですか?この固有の問題を回避する方法を見つけることができれば、私たちを止めることはできません。
では、馬が両方から継承できない単一の継承の世界ではどうなるでしょうか。
多重継承のサポートの欠如を回避するために、多くのノイズの多いコードになってしまいます。の代わりに
class A { private: *data* ;
public: void doA() { ... } }
class B { private: *data* ;
public: void doB1() { ... }
void doB2() { ... };
}
public class MI: public A, public B { ... }
必要なもの:パブリッククラスA {...; }パブリックインターフェイスB {...; }パブリッククラスBImplはBを実装します{...; }
public class SI extends A implements B {
private BImpl bImpl;
public void doB1() { bImpl.doB1(); }
public void doB2() { bImpl.doB2(); }
}
2つの基本クラス間でメソッド名が競合する可能性を回避するためのすべての問題。
ところで、ドメイン内のカテゴリを模倣するためにOOクラスを作成しようとするのは間違いです。ユースケースを実装する必要がある場合にのみOOクラスを作成し、プログラム構造を改善するための継承。牛と車、および馬と牛の繁殖と馬または車での移動を必要とする使用例ができるまで、動物とトランスポーターについては心配しませんでした。多重継承をサポートする言語が必要かもしれません。
はいといいえ。
一方では、クラスはまったく必要ありません。言語がチューリング完全であり、必要なすべてのI/O機能を備えている場合は、必要なすべてのツールがあります。しかし、それは「必要性」の極端な解釈です。
一方、言語に組み込まれた多重継承のサポートがあると、いくつかのタスクを単純化して明確にすることができます。単一の継承だけで同じ要件を達成することはできますが、物事を別の方法で構造化する必要があり、かなり雑然としています。
同様の問題は、単一ディスパッチと複数ディスパッチで発生します。私は以前、複数のディスパッチによってビジターパターンが冗長になると主張していました。複数のディスパッチ自体はビジターとビジターの役割の責任を分離しないため、おそらく今は間違っていると思いますが、ビジターパターンのはるかに単純でおそらくより柔軟なバリアントにつながります。複数のディスパッチをサポートしていない言語(人気のあるOOP言語)のほとんど)にとって、かなりの数の人々がドメイン固有の言語を書いていることは十分に説得力があります。私自身も;-)
多重継承を使用することで単純化または排除できるデザインパターンがいくつかあると思いますが、はっきりとは言えません。
よくあることですが、それはバランスの問題です。すべての仕事に最適なツールを用意するのが最も簡単に思えるかもしれませんが、その場合は無限の数のツールが必要になります。ミニマリストのツールキットは、手動で行う作業が増えることを意味しますが、使用可能な特定のツールについての経験が増えます。プログラマーはおそらく、ツールキットの大きさ、正確にはどのツールを含めるべきかについて、時間の終わりまでお互いを撃ち、爆撃するでしょう。
注目に値する1つの点-多重継承は、それに伴ってすぐに明らかになるよりも複雑になります。 「ダイヤモンド継承パターン」は、この文脈でしばしば言及されます。このパターンを考えてみてください...
A
/ \
/ \
B C
\ /
\ /
D
D
はA
の単一のコピーを継承しますか、それともA
の2つの独立したコピーを継承しますか?この質問は、D
がB
とC
の両方を継承するためにのみ発生する可能性があります。 C++では、それは選択であり、間接的に表現されます。 B
とC
の両方がvirtual継承を使用してA
を継承する場合、D
はA
-それ以外の場合、A
の2つの独立したコピーがあります。
しかし、この場合はどうですか...
A
/|\
/ | \
B C D
\ | /
\|/
E
そして、B
とC
の両方が仮想継承を使用しているが、D
は使用していない場合はどうなるでしょうか。
正しい答えは、E
はA
の2つのコピーを継承することだと思います。1つはB
とC
が共有し、もう1つはD
。原則として難しいことではありませんが、コードでは、関連情報が散在しています。
継承設計の問題を脇に置きます(これは、インターフェイスの継承と「完全な」MIの両方に適用されます)。
答えは「いいえ」です。MIでできるはずのこと、SIとインターフェイスの構成でできることです。違いは、SI + IIでは、同じことを実行し、このコード重複の管理を処理するために、さらに多くのコードを作成する必要があることです。
'ITransporter'と 'IStockKeepingUnit'の両方である馬が必要な場合、MIを使用すると、継承してそれを実行できます。SIでは、他のインターフェイスを実装するクラスを作成し、それに接続する必要があります。あなたの馬のクラスによって実装されるインターフェースメソッド。多くのレイヤーを深く継承することに夢中にならず、同じクラスから継承する複数のクラスがある限り、MIの方が適しています。
インターフェースに独自の特定のメソッドを実装する必要があり、基本クラスのメソッドを単純に再実装できない場合は、IIの方が(単純であるため)優れています。
私は過去にいくつかの真面目なアナリストと一緒に働き、非常にラレグなスケールのシステムを書きました。彼らはシステム設計を分解するときにMIを頻繁に使用したので、複数の再利用されたインターフェースの利点を見ることができます。また、これらのインターフェイスを1レベルの深さに保つ必要があることもわかります。そうしないと、複雑になりすぎてtpを開始できます。