OOコミュニティでは、「継承よりも構成を優先する」ことが一般的に認められています。一方、継承は、ポリモーフィズムとすべてを基本クラスに委任する簡単で簡潔な方法の両方を提供します明示的にオーバーライドされていないので、非常に便利で便利な場合を除きます。
継承の乱用の最も明白で私見で最も確実な兆候は、 Liskov Substitution Principle の違反です。継承が便利であるように見えても、継承が「The Wrong Tool for the Job」であることを示す他の兆候は何ですか?
機能を取得するためだけに継承する場合、継承を悪用している可能性があります。これは 神オブジェクト の作成につながります。
継承自体は、クラス間の実際の関係(Dog extends Animalなどの古典的な例のように)を参照し、一部のクラスでは意味をなさないメソッドを親クラスに配置しない限り、問題にはなりません子(たとえば、AnimalクラスにBark()メソッドを追加しても、Animalを拡張するFishクラスでは意味がありません)。
クラスが機能を必要とする場合は、コンポジションを使用します(おそらく、機能プロバイダーをコンストラクターに注入しますか?)。クラスが他のクラスと同じである必要がある場合は、継承を使用します。
撃たれる危険を冒して、継承はそれ自体がコードのにおいだと言います:)
継承の問題は、2つの直交する目的に使用できることです。
両方を取得するための単一のメカニズムを持つことは、最初に「継承の悪用」を引き起こす原因になります。ほとんどの人は継承がインターフェイスに関するものであると期待しているためです。これを見落とす...)
実際、HaskellやGoなどの現代の言語では、両方の懸念を分離するために継承を放棄しています。
軌道に戻す:
したがって、契約の「インターフェース」部分が尊重されないことを意味するため、リスコフ原則への違反は最も確実な兆候です。
ただし、インターフェースが尊重されている場合でも、メソッドの1つが有用であると見なされたという理由だけで、「ファット」基本クラスからオブジェクトを継承している場合があります。
したがって、Liskov原理自体では十分ではありません。多型がsedであるかどうかを知る必要があります。そうでない場合、そもそも継承に意味がありませんでした。それは乱用です。
概要:
Liskov Principle
それを回避する方法:
懸念の明確な分離を課す:
つまり、各メソッドには少なくとも2回のキーストロークが必要であり、突然、この「ファット」クラスをその小さな部分に再利用するのが素晴らしいアイデアであるかどうかについて人々は考え始めます。
それは本当に「一般に受け入れられている」のでしょうか?これは私が何度か聞いたアイデアですが、普遍的に認識されている原則ではありません。先ほど指摘したように、継承と多態性はOOPの特徴です。それらがなければ、間抜けな構文を使った手続き型プログラミングができます。
コンポジションは、オブジェクトを特定の方法で構築するためのツールです。継承は、オブジェクトを別の方法で構築するためのツールです。どちらにも問題はありません。両方がどのように機能するか、およびそれぞれのスタイルを使用するのが適切な場合を知っている必要があります。
継承の強みは再利用性です。インターフェース、データ、行動、コラボレーション。これは、新しいクラスに属性を伝えるための便利なパターンです。
継承を使用することの欠点は、再利用性です。インターフェース、データ、動作はカプセル化され、まとめて送信されるため、オールオアナッシングの提案になります。問題は、階層が深くなるにつれて特に明らかになります。抽象で有用なプロパティが少なくなり、ローカルレベルでプロパティが不明瞭になり始めるためです。
合成オブジェクトを使用するすべてのクラスは、それと対話するために同じコードを多かれ少なかれ再実装する必要があります。この複製にはDRYの違反が必要ですが、ドメインにとって最も重要なクラスのプロパティをデザイナーが自由に選択できるようになります。これは、クラスがより専門的になるときに特に有利です。
一般に、コンポジションを介してクラスを作成し、それらのクラスのプロパティの大部分を共通に持つクラスを継承に変換し、違いをコンポジションとして残すことが推奨されます。
IOW、YAGNI(継承は必要ありません)。
A
をクラスB
にサブクラス化しているが、B
がA
から継承されているかどうかを誰も気にしていない場合は、それが間違っている可能性があります。
「継承よりも好意的な構成」は、最も愚かな主張です。どちらもOOPの重要な要素です。 「のこぎりでハンマーを好む」と言っているようなものです。
もちろん、継承は悪用される可能性があり、あらゆる言語機能が悪用される可能性があり、条件付きステートメントが悪用される可能性があります。
おそらく最も明白なのは、継承するクラスが、公開されたインターフェイスで決して使用されないメソッド/プロパティの山で終わる場合です。基本クラスに抽象メンバーが含まれる最悪の場合、抽象メンバーの空の(または無意味な)実装が見つかるだけで、「空白を埋める」ことができます。
さらに微妙なことに、コードを再利用するために、類似した実装を持つ2つのものが、関連していないにもかかわらず1つの実装に絞り込まれている箇所が見られることがあります。これにより、コードの結合が増加し、コードのもつれが解消される傾向があります。類似性が単なる偶然であることが判明すると、早期に発見されない限り、非常に苦痛になる可能性があります。
いくつかの例で更新:プログラミング自体とは直接関係ありませんが、この種のことの最良の例はローカリゼーション/国際化です。英語には「はい」を表す1つの単語があります。他の言語では、質問に文法的に同意するために、多くの言語が使用されます。誰かが言うことができる、ああ「はい」の文字列を持っているとしましょう、それは多くの言語では問題ありません。次に、「はい」という文字列は「はい」に実際には対応していないが、実際には「この質問に対する肯定的な回答」の概念になっていることに気付きます。常に「はい」であるという事実は偶然であり、時間は節約されるだけです「はい」が1つあると、結果として生じる混乱が解消されます。
コードベースで特に厄介な例を1つ見ましたが、実際には親と子の関係で、親と子に同様の名前のプロパティがあり、継承でモデル化されていました。すべてが機能しているように見えるので誰もこれに気づかず、子(実際にはベース)と親(サブクラス)の動作は異なる方向に進む必要がありました。運が良かったように、これは締め切りが迫っている間違った時期に起こりました-モデルをあちこちで微調整しようとすると、複雑さ(ロジックとコードの重複の両方で)がすぐに終わりを迎えました。また、コードはビジネスがこれらのクラスが表す概念についてどのように考えたり話したりしたのかとは関係がないので、理由/作業を推論するのも非常に困難でした。結局、弾丸をかじり、いくつかの大きなリファクタリングを行わなければなりませんでした。元の男が、偶然同じである値を持つ2つの類似のサウンドプロパティが同じ概念を表すと決めていなかった場合、完全に回避できました。
継承と構成のような戦いは、実際にはより深いコアとなる戦いの一部に過ぎないと思います。
その戦いは次のとおりです。将来の拡張で最大の拡張性を実現するために、コードの量が最も少なくなるのは何ですか。
それがその質問に答えるのがとても難しい理由です。ある言語では、他の言語よりも構成よりも継承が迅速に行われる、またはその逆の場合があります。
または、コードで解決している特定の問題が、長期的な成長のビジョンよりも便宜から得られるメリットである可能性があります。それはプログラミングの問題と同じくらいビジネス上の問題です。
したがって、質問に戻ると、将来のある時点でストレートジャケットになる場合、または存在する必要のないコードを作成する場合(継承は理由もなく他から継承するクラス、またはさらに悪いことに、子供のための多くの条件付きテストを行わなければならない基本クラス)。
構成より継承を優先すべき状況があり、その区別はスタイルの問題よりもはるかに明確です。私はSutterとAlexandrescuのC++ Coding Standardsから言い換えています。私のコピーは現在、本棚にあります。ちなみに、読書を強くお勧めします。
まず、継承は不要な場合は推奨されません。継承は、基本クラスと派生クラスの間に非常に親密な結合関係を作成し、メンテナンスのために頭痛の種になる可能性があるためです。継承の構文が読みづらくなったり、何かを理解したりするのは、一部の人の意見だけではありません。
そうは言っても、基本クラスが現在使用されているコンテキストでderivedクラス自体を再利用して、そのコンテキストでコードを変更する必要がない場合は、継承が推奨されます。たとえば、if (type1) type1.foo(); else if (type2) type2.foo();
のように見えるコードがある場合、おそらく継承を調べるのに非常に良いケースがあります。
要約すると、基本クラスのメソッドを再利用するだけの場合は、合成を使用します。現在基本クラスを使用しているコードで派生クラスを使用する場合は、継承を使用します。