構図を優先することは、多態性だけではありません。それはその一部ですが、あなたは正しいです(少なくとも名目上の型付けされた言語では)人々が本当に意味するのは「構成とインターフェース実装の組み合わせを好む」ということです。しかし、(多くの状況で)作曲を好む理由は深遠です。
多態性とは、複数の動作をすることの1つです。したがって、ジェネリック/テンプレートは、単一のコードで型によって動作を変えることができる限り、「ポリモーフィック」機能です。実際には、このタイプのポリモーフィズムは実際に最も適切に動作し、バリエーションはパラメーターによって定義されるため、一般にパラメトリックポリモーフィズムと呼ばれます。
多くの言語は、「オーバーロード」と呼ばれるポリモーフィズムまたはアドホックポリモーフィズムを提供します。このポリモーフィズムでは、同じ名前の複数のプロシージャがアドホックに定義され、言語によって選択されます(おそらく最も具体的な)。開発された規約を除き、2つのプロシージャの動作を接続するものは何もないため、これは最も動作の悪い種類のポリモーフィズムです。
3番目の種類の多型は、サブタイプ多型です。ここで、特定のタイプで定義されたプロシージャは、そのタイプの「サブタイプ」ファミリー全体でも機能します。インターフェイスを実装するか、クラスを拡張すると、通常、サブタイプを作成する意図を宣言します。真のサブタイプは、リスコフの置換原則によって管理されます、スーパータイプのすべてのオブジェクトについて何かを証明できれば、サブタイプのすべてのインスタンスについてそれを証明できるという。しかし、C ++やJavaのような言語では、一般に人々はサブクラスについて真実であるかもしれないし、そうでないかもしれないクラスについて強制されておらず、しばしば文書化されていない仮定を持っているので、人生は危険になります。つまり、コードは実際よりも多くのことが証明可能であるかのように書かれており、不注意にサブタイプした場合、多くの問題が発生します。
継承は、実際には多型とは無関係です。自分自身への参照を持つ「T」というものがある場合、「T」から「T」の参照を「S」への参照に置き換えて新しいもの「S」を作成すると、継承が発生します。継承は多くの状況で発生する可能性があるため、その定義は意図的に曖昧ですが、最も一般的なのはthis
、仮想関数によって呼び出されるthis
ポインターをサブタイプへのポインターで置き換える効果を持つオブジェクトをサブクラス化することです。
継承は、すべての非常に強力なものが継承を破壊する力を持っているように、危険です。たとえば、あるクラスから継承するときにメソッドをオーバーライドするとします:元のクラスの作成者がそれを設計した方法であるため、そのクラスの他のメソッドが継承するメソッドを特定の方法で動作させると仮定するまで、すべてが順調です。オーバーライドされるように設計されていない限り、プライベートまたは非仮想(最終)メソッドによって呼び出されるすべてのメソッドを宣言することにより、これに対して部分的に保護できます。しかし、これでも常に十分とは限りません。時々、このようなものが表示されることがあります(擬似Javaで、C ++およびC#ユーザーが読み込めることを願っています)
interface UsefulThingsInterface {
void doThings();
void doMoreThings();
}
...
class WayOfDoingUsefulThings implements UsefulThingsInterface{
private foo stuff;
public final int getStuff();
void doThings(){
//modifies stuff, such that ...
...
}
...
void doMoreThings(){
//ignores stuff
...
}
}
これは素敵だと思い、独自の「物事」のやり方がありますが、継承を使用して「moreThings」をする能力を獲得します。
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
そしてすべてが順調です。WayOfDoingUsefulThings
あるメソッドを置き換えても他のメソッドのセマンティクスは変わらないように設計されていました...待機を除いて、そうではありませんでした。どうやらそのように見えますが、doThings
重要な変更可能な状態が変更されました。したがって、オーバーライド可能な関数を呼び出さなかったとしても、
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
あなたがそれを渡すとき、今予想されたことと異なることをしMyUsefulThings
ます。さらに悪いことに、あなたはWayOfDoingUsefulThings
それらがそれらの約束をしたことさえ知らないかもしれません。たぶん、dealWithStuff
同じライブラリーから来ているWayOfDoingUsefulThings
とgetStuff()
さえ(考えるライブラリによってエクスポートされていない友人のクラス C ++で)。さらに悪いことに、あなたはそれを気付かずに言語の静的チェックを破りました:特定の方法で振る舞う関数を持っていることを確認するためだけにdealWithStuff
取りました。WayOfDoingUsefulThings
getStuff()
コンポジションを使用する
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
静的型の安全性を取り戻します。一般に、サブタイプを実装する場合、コンポジションは継承よりも使いやすく安全です。また、finalメソッドをオーバーライドすることもできます。つまり、ほとんどの場合、インターフェースを除き、すべての final / non-virtualを自由に宣言できます。
より良い世界では、言語はdelegation
キーワードとともにボイラープレートを自動的に挿入します。ほとんどはそうではないので、欠点は大きなクラスです。ただし、委任インスタンスを作成するためにIDEを取得できます。
今、人生とは多態性だけではありません。常にサブタイプする必要はありません。ポリモーフィズムの目標は一般にコードの再利用ですが、その目標を達成する唯一の方法ではありません。多くの場合、機能を管理する方法として、サブタイプ多型なしで構成を使用することが理にかなっています。
また、行動継承には用途があります。これは、コンピューターサイエンスの最も強力なアイデアの1つです。そのほとんどの場合、優れたOOPアプリケーションは、インターフェイスの継承と構成のみを使用して作成できます。2つの原則
- 継承または設計を禁止する
- 構図を好む
上記の理由から優れたガイドであり、実質的なコストは発生しません。