継承と構成について友人とほとんど議論していました。その議論から学んだことは、今日の業界では継承の使用が多かれ少なかれ非難され、構成またはコピーアンドペーストに置き換えられているということです。
クイックGoogle検索を実行すると、「継承よりも構成を使用するXの理由」、「継承を回避する」などのタイトルが付いた多くの記事でこれが確認されます...
ポリモーフィズムを実現する1つの方法は、オブジェクトAとBが「is-a」の関係にある場合に継承を使用することです。四角形の典型的な例である四角形は、四角形などに拡張されます。
Class Shape {
private String name;
private double area;
public double getArea()
}
Class Rectangle extends Shape {
}
この例では、共有されているフィールドまたは子クラスが2つしかない場合は、コンポジションを使用するか、単にコピーして貼り付けることを好みました。コンポジションを使用する利点と、それが継承と比較してパワフルで優れたデザインパターンになる理由を理解しています。上記のような単純なシナリオで、深いポリモーフィズムがない場合、継承よりも構成またはコピーアンドペーストの利点は何ですか?
これらすべての質問でごめんなさい、私は業界が継承の使用を非難する理由とそれを使用することが有効なときを理解しようとしています。
継承を使用する前に、最初にコンポジションを使用することで、単純なミスを防ぐことができます。また、クラスの継承:
また、Javaは完璧ではありません。
OOPおよび業界では、なぜ継承がそれほど嫌われているのですか?
「憎しみ」は公正な表現ではないと思います。継承よりも構成を使用することについての多くのアドバイスがあります。
以下は、開発者が継承の問題を解決したい場合の例ですが、これは最良の選択肢ではありません。
厳密には「is-a」ではない
たとえば、「is-a」関係に失敗したものに継承を使用する場合があります。むしろ、むしろ、「振る舞い」の関係がはるかに近いと言えるでしょう。たとえば、ビデオゲームでは目に見えるオブジェクトがたくさんあるので、適切にレンダリングする必要があります。たとえば、 "Enemy is-a Visible"と言ってVisible
クラスを作成するのは魅力的です。しかし、それは間違っています。正しいことは、IVisible
インターフェースを作成することです。
ビデオゲームでは、「敵は可視コンポーネント」と言うプッシュを見つけることがあります。これには、たとえば、目に見えない敵を作成したい場合など、いくつかの利点があります。注:はい、bool IsVisible
とcheckを使用できますが、Visible
のリストがあり、それを削除した場合、まったく確認する必要はありません。
グリーンカーは別の種類の車ではありません
「Green Car is-a a Car」ですが、色属性を持つことができるときにGreenCar
クラスを作成することは非現実的であり、賢明ではありません。そうすれば、新しい色が必要になるたびに新しいクラスを作成する必要がなくなります。 また、継承の場合、どのように色を変更しますか?これは継承の構成です。
補遺:
おそらく、別のクラスがあるとしましょう。Building
。 Car
と同様に、Building
にも色があります。 ColoredThing
基本クラスを作成する必要がありますか?いいえ、できません。どうして?さて、もしCar
とBuilding
の両方が所有者を持っているとしたら...それがOwnedThing
であるべきでしょうか?いいえ。
一般的な属性を抽象化すると、設計が不適切な継承ツリーになり、実際の問題を解決しない場合に多重継承を使用することになります。
色のあるもののリストが必要な場合はどうでしょうか? ColoredThing
クラスを使用する必要がありますか?いいえ。インターフェースを使用する必要があります。
どうして?繰り返しますが、OwnedThing
のリストも必要な場合はどうしますか? -それは 拡張にオープン であることについてです。クラスのようにシステムを拡張する可能性を狭めないため、インターフェースの方が優れています。
これはコピーアンドペーストと呼ばれるものですか。コピーアンドペーストではないことを主張します。
さて、ここで繰り返さないという議論があります...ある日、車や建物には単一の色がなく、複数の色があることに気付くでしょう!今、あなたはすべてのものにもっと色を追加する必要があります...私はそれが悪いことを認めます、あなたはこれらのことを一箇所で変えることができるはずです。とにかく、タイプの属性を変更することは、基本クラスを置き換えるよりも簡単です...すべての色属性をColoring
クラスに置き換えます。Coloring
は複数の色を持つことができます。
複数の継承からコンポーネントシステムへ
たとえば、ビデオゲームで、「アバターは兵士」の場合(銃を使用して撮影できる)。さらに、「アバターis-aパイロット」(車両を運転できます)では、多重継承を使用したくなる場合があります。
多重継承のない多くの言語があることはさておき、...
新しい能力を獲得するAvatar
をどのようにモデル化しますか?このモデルでは、ブールフラグを設定できます。 Avatar
はdisabledパイロットとして作成されます。ゲームでプログラムしたい能力が多いほど、そのようなものを追加する必要があります。
多重継承は維持が難しくなります。インターフェイスを使用すると、コードの重複につながり、メンテナンスも困難になります。構成を使用する:「アバターにはPilotAbilityがあります」。
ご覧のとおり、複数のクラスを継承しない言語での継承よりも、合成の方が特に用途が広いです。さらに、私は多くの言語が見落としによってではなく、設計によってそれを持たないことを主張します。多重継承は誤用しやすいため、通常のケース(インターフェース)に限定されており、他の状況に代わるものがあります。
最近継承を使用する有効なケースはありますか?
部屋には少なくとも3頭の象がいます。そのうちの2つをすぐに処分し、3つ目を認めます...
必要継承しないでください。証拠は、いくつかの言語はまっすぐにそれを持っていないことであり、それらは完全に使用可能です。ただし、継承は便利です。
拡張のために継承が必要なシステムで作業する必要がある場合があります。その場合、あなたはneedの継承です。なぜなら、そのシステムの設計が選択肢を狭めたからです。ただし、そのようなシステム(または同じ目的を果たす代替システム)が継承なしでは不可能であったことを意味するものではありません。なぜ継承を選んだのですか?便利。注:ここでは、カスタム例外の作成について詳しく説明します。
インターフェースの継承は継承です。ただし、インターフェースの継承については、それが何か別のものであるかのように扱います。注:デフォルトメソッドの出現により、クラス継承のいくつかの使用例が削除されました。
注:継承は最適化である場合もありますが、そのことについては触れません継承の引数としての最適化は弱すぎると思います。ただし、UIおよびウィジェットライブラリは、多くの場合、仮想メソッドを呼び出すと、コールバック、リスナー、オブザーバー、継続などよりもパフォーマンスが向上するという事実al。実際、言語が関数参照(Javaなど)をサポートしていない場合は、継承を使用して仮想メソッドを呼び出すのが、委任と通知の最も効率的な方法です。
したがって、継承のneedについては説明せず、代わりに利便性について説明します。
そうは言っても、はい、有効なユースケースがあります。
リスコフ置換原理
基本型を使用できる場所ならどこでも、派生型を使用できるはずです。
時々人々は継承にリソースを割り当て、それから彼らが望まないメンバーで例外を隠したり、nullに設定したり、例外を投げたりしなければなりませんでした。その状況では、コンポジションを使用する方が良いです。 サードパーティシステムを拡張していて、継承が唯一のオプションである場合を除いて(仮想メソッドが必要か、基本クラスを想定するメソッドを呼び出す必要があるため)、サードパーティのせいにします。
コードベースで検索を行い、継承を使用してサードパーティシステムを拡張する必要があるすべてのケースを破棄し、デフォルトのメソッドで異なる方法で実行するすべてのケースを破棄して、次の例に到達しました。
SortedSet
タイプのライブラリと、そこからSubSet
を作成できるメソッドがあるライブラリを考えてみましょう。これらのサブセットは、基本タイプとは異なり(実際にはアイテムを格納しません)、代わりに、サブセットであるセットへの範囲と参照によって定義されたビューです。ただし、サブセットは「is-a」セットです。
この場合、実際にアイテムを格納する別のタイプがあり、SortedSet
はそれへの参照(構成)を持っています。これで、SortedSet
とSubSet
の両方が同じストレージオブジェクトへの参照を持つことになります。 SubSet
タイプには、範囲を定義する他の2つのメンバーもあります。 SortedSet
には、SubSet
がオーバーライドする仮想メソッドもあります(たとえば、サブセットのサブセットを取得できるようにするため)。
ここで重要なのは、消費者の観点から見たSubSet
がSortedSet
であることです。実際、私はSubSet
クラスをプライベートにして、SortedSet
からSortedSet
としてのみ公開し、機能を失うことはありません(私はそうしました)。
では、合成ではなく継承を使用することがなぜ重要だったのでしょうか。派生クラスは、基本ケースクラスがどこでも使用できる必要があるため(Liskov置換原理)。
なぜインターフェースを使用しなかったのですか?サブセットの実装は親に対して非常に緊密であるため、それらは多くのコードを共有し、お互いを認識しています(実際、親は派生クラスをインスタンス化します)。さらに、それらを使用できるようにするために、それらが異なるクラスであることをまったく知る必要はありません。一方、私がインターフェースを使用した場合、両方を許可できるようにするには、クラスではなくインターフェースとしてパラメーターを定義することを忘れないでください。
注:SortedSet
はISet
インターフェースも実装します。さらに、はい、それらはすべてジェネリックです。
保証としての継承
継承は、virtualおよびSealed(.NET)/ final(Java)メンバーの適切な使用と組み合わせると、実装を厳密に制御できるため、信頼できることを意味します。
これは、拡張を許可するために渡す必要がある外部リソースを処理するシナリオで役立ちます。派生クラスがシステムを台無しにするような方法で外部リソースを使用できる場合、システムに干渉する派生クラスをすべての人が使用することは望ましくありません。
この状況では、必要な派生クラスを持つ継承ツリーを作成します。しかし、適切に設計されていれば、ライブラリの利用者は、継承を必要とせずに、それらをコンポジションで使用できます。
継承よりもコンポジションを使用することが悪い設計パターンと見なされるのはいつですか?
Liskov置換原理の破れ
あなたが意図するとき、タイプは置換です。型を継承する代わりにラップすることにより、ベース型を使用できるすべての場所で新しい型を使用できなくなります。したがって、自分のタイプを置き換えとして使用する場合は、継承を使用する必要があります。
継承と構成が連動する必要があることに注意してください。たとえば、コンポジションのおかげでオブジェクトのコンポーネントを削除または変更でき、継承のおかげでさまざまなタイプの置換を使用できます。
Javaソースコードは継承と深い継承でいっぱいです。 Javaソースのデザインが悪いという意味ですか?
私はこれを 権威からの議論 に変装して読みます:Javaは継承を使用し、Javaを作成した人は賢いです、継承を使用するのは賢明でなければなりません。
まあ、あなたの馬を保持...
最初に、Javaは-少なくとも-3つのこと:言語、標準ライブラリ、仮想マシンです。標準ライブラリについて尋ねるのを知っています。まもなく標準ライブラリが作成されます。私がこれを指摘する理由を理解してください。
さて、Javaは完璧ではありません。そのデザインのいくつかの側面はその古さを示しています。確かに、廃止予定がありますが、いくつかはJava Javaの古い部分に戻りますが、最初に、どの部分が継承を適切に使用しているかを見てみましょう。
継承の適切な使用
免責事項:私はメモリで標準ライブラリを知りません、または今まで知りませんでした。実際、私がJavaを深刻なものに使用して以来、しばらく経っています。したがって、これを調べています: すべてのパッケージの階層 。
例外(Throwable
)と列挙型に継承を使用することは、最新の技術です。
UIやデータベース関連のタイプなど、外部リソースとインターフェースするものがあります。これらをパスします。
一部のスレッド関連の型が継承を使用しているのがわかります。これは理にかなっており、外部リソースと同様に、継承は有用な保証を提供します。リフレクションについても同様の議論をすることができます。
Archaic but justified
抽象データ構造はおそらくインターフェイスである必要があります(おそらくユーティリティクラスが付随します)。これらの抽象的なデータ構造には使用可能な共通コードがあるため、これらのインターフェイスを作成するだけでは機能しないことを知っています。さらに、デフォルトのメソッドが作成されたとき、デフォルトのメソッドは存在しませんでした(デフォルトのメソッドには言語の改善が必要でした-そしておそらく仮想マシン)そしてJavaには拡張メソッドのようなものがありません。したがって、それらは古風ですが正当化されます。
少なくとも現代の標準では、リスナーモデル全体が悪いと私は主張します。ことは、Javaには、関数を参照する方法がない(仮想マシンの制限)ためです。そのため、これらはこれで行き詰まっています。Java以前は、匿名クラス(言語改善)では、クラスにEventListener
を実装する方がよいでしょう。各イベントタイプを実装する小さなクラスの束を作成し、共有する必要のあるメンバーと、カプセル化を解除せずにそれを行う方法を解決する方が望ましいです。 EventListener
の実装では、イベントのタイプを分類し、EventObject
のタイプと一致させる必要があります。したがって、EventObject
の多くのサブタイプが必要です。したがって、 、これは古風ですが正当化されます。
言語ビジター、テキスト形式、および何らかの形式の反復が必要なもの、または文字列やバイナリストリームでの作業は、継承に依存しています。リスナーが苦痛である(そして、Stream APIのようなものは何もなかった)ことを考えると、継承を使用する方が良いでしょう。したがって、これらはすべて同じボートにあります。古風ですが正当化されます。
コピーと貼り付け
コピーと貼り付けは、再利用の2番目に古い形式です(転記が最初の形式です)。そうは言っても、コピーアンドペーストを推奨することはめったにありません(実際、私は主張しませんが)。
例では:
Class Shape {
private String name;
private double area;
public double getArea()
}
Class Rectangle extends Shape {
}
Rectangle
クラスが必要な理由は明らかではありません。 Shape
は何も追加しないので、Rectangle
で作業できると私は主張します。 Rectangle
がさらに追加する場合、継承がここで役立ちます。
彼らはむしろクラスShapeをコピーして貼り付けるだけでした
いいえ、継承を避けるためだけにコピーして貼り付けないでください。議論は継承と構成の間にあります:「長方形には形がある」と言うべきですか? Liskov置換の原則を参照してください。Rectangle
がShape
が使用されている場所で使用される場合は、継承を使用してください。
メソッドのオーバーロード
タイプTを受け入れるメソッドを作成すると、Tから派生したタイプのオブジェクトも受け入れます。このクラスを変更できず、このメソッドが必要な場合は、Tから継承する必要があります。ここで、受け入れるものについては可能な限り寛容であり、通常は、メソッドがオブジェクトを使用する方法とよく似たインターフェースを使用することを意味します。
動的に型付けされた言語で作業している場合、メソッドのオーバーロードはなく、オブジェクトに問い合わせる必要がありますが、それは別のことです。
Liskov置換原理 および 界面分離原理 を参照してください。
「業界が好む」というのは、まったく間違った状況の見方です。正しい見方は次のとおりです。「業界では、構成が継承よりも優れていることが多く、より優れたソリューションが望ましいため、継承ではなく構成がよく使用されます。業界は、それがより良い解決策である場合でも、継承を好んでいます。 「
継承を使用することは悪い設計とは見なされていませんが、一般に、構成により多くのクラスを犠牲にしてより多くの柔軟性が作成されます。
私は一般に、どのような種類の階層も作成するのではなく、テンプレートメソッドパターンを実装して継承を使用します。