web-dev-qa-db-ja.com

Java継承を「拡張する」ことを避ける理由

ジェーム・ゴズリングは言った

「可能な限り、実装の継承は避けるべきです。」

代わりに、インターフェイスの継承を使用します。

しかし、なぜ?キーワード「extends」を使用してオブジェクトの構造を継承しないようにすると同時に、コードをオブジェクト指向にする方法を教えてください。

「本屋で本を注文する」のようなシナリオで、この概念を説明するオブジェクト指向の例を誰かに教えてもらえますか?

40
newbie

ゴスリングは、extendsキーワードの使用を避けようとはしませんでした...彼は、コードの再利用を達成する手段として継承を使用しないと言っただけです。

継承自体は悪くないものであり、OO構造の作成に使用する非常に強力な(必須の)ツールです。ただし、適切に使用されない場合(つまり、何かに使用される場合elseオブジェクト構造を作成するよりも)非常に密に結合され、保守が非常に難しいコードを作成します。

継承はポリモーフィック構造の作成に使用する必要があるため、インターフェースを使用して簡単に実行できるため、オブジェクト構造を設計するときに、クラスではなくインターフェースを使用するステートメントを使用できます。あるオブジェクトから別のオブジェクトへのコードのコピーと貼り付けを回避するための手段としてextendsを使用している場合、おそらくそれを間違って使用しており、この機能を処理し、代わりにコンポジションを通じてそのコードを共有するための専用クラスを提供するほうがよいでしょう。

Liskov Substitution Principle について読んで理解してください。クラス(またはその点についてはインターフェース)を拡張しようとするときはいつでも、原則に違反しているかどうかを自問してください。そうである場合は、不自然な方法で継承を使用している可能性があります。それは簡単で、しばしば正しいことのように思われますが、コードの保守、テスト、および進化がより困難になります。

---編集 この答え は、より一般的な用語で質問に答えるためにかなり良い議論をします。

38
Newtopian

オブジェクト指向プログラミングの初期の頃、実装の継承はコードの再利用の黄金のハンマーでした。一部のクラスに必要な機能がある場合、やるべきことはそのクラスから公に継承することでした(これらは、C++がJavaより普及していた時代でした)。その機能は「無料」で入手できました。

それ以外は本当に無料ではありませんでした。その結果、処理が困難な菱形の継承ツリーを含め、理解するのがほとんど不可能であった非常に複雑な継承階層ができました。これは、Javaの設計者がクラスの多重継承をサポートしないことを選択した理由の一部です。

ゴスリングの助言(および他の声明)は、それがかなり深刻な病気に対して強力な薬であるようであり、一部の赤ちゃんが浴槽水で捨てられた可能性があります。継承を使用して、本当にis-aであるクラス間の関係をモデル化することには何の問題もありません。ただし、別のクラスの機能を使用したいだけの場合は、最初に他のオプションを調査することをお勧めします。

9
JohnMcG

相続は最近、かなり恐ろしい評判を集めています。これを回避する理由の1つは、「継承よりも構成」という設計原則に沿っているため、基本的には、関係のないところで継承を使用せず、コードの記述を保存するだけです。この種の継承により、バグの修正や発見が困難になる場合があります。あなたの友人がおそらく代わりにインターフェースを使用することを提案した理由は、インターフェースが分散APIに対してより柔軟で保守可能なソリューションを提供するためです。多くの場合、ユーザーのコードを壊すことなく、APIに変更を加えることができます。クラスを公開すると、ユーザーのコードが壊されることがよくあります。 .Netでのこの最も良い例は、ListとIListの使用法の違いです。

6

拡張できるクラスは1つだけですが、多くのインターフェースを実装できます。

継承はあなたが何であるかを定義すると聞いたことがありますが、インターフェースはあなたが果たすことができる役割を定義します。

インターフェースはまた、ポリモーフィズムのより良い使用を可能にします。

それは理由の一部かもしれません。

5
Mahmoud Hossam

私もこれを教えられており、可能な限りインターフェースを好みます(もちろん、意味のある場所で継承を使用します)。

私が考えていることの1つは、コードを特定の実装から分離することです。 ConsoleWriterというクラスがあり、それをメソッドに渡して何かを書き出すと、それがコンソールに書き込まれます。ここで、GUIウィンドウへの出力に切り替えたいとしましょう。さて、メソッドを変更するか、GUIWriterをパラメーターとして使用する新しいメソッドを作成する必要があります。 IWriterインターフェイスの定義から開始し、メソッドにIWriterを取り込む場合、ConsoleWriter(IWriterインターフェイスを実装します)から始めて、後でGUIWriter(IWriterインターフェイスも実装します)と呼ばれる新しいクラスを記述してから、渡されるクラスを切り替えるだけです。

もう1つ(C#には当てはまりますが、Javaについては不明です)、拡張できるクラスは1つだけですが、多くのインターフェースを実装できます。 Teacher、MathTeacher、HistoryTeacherという名前のクラスがあるとします。 MathTeacherとHistoryTeacherはTeacherから拡張されましたが、MathTeacherとHistoryTeacherの両方である誰かを表すクラスが必要な場合はどうでしょうか。一度に1つしか実行できないときに複数のクラスから継承しようとすると、かなり面倒になります(方法はありますが、完全には最適ではありません)。インターフェイスを使用すると、IMathTeacherおよびIHistoryTeacherと呼ばれる2つのインターフェイスを作成でき、Teacherから拡張してこれら2つのインターフェイスを実装する1つのクラスを作成できます。

インターフェースを使用する場合の欠点の1つは、インターフェースを実装するクラスごとに実装を作成する必要があるため、コードが重複する場合があることですが、デリゲートなどの使用など、この問題に対する明確な解決策があります(不明) Javaと同等)とは何か。

継承よりもインターフェイスを使用する最大の理由は、実装コードの分離ですが、継承は依然として非常に有用であるため、悪とは考えないでください。

1
ryanzec

クラスから継承する場合は、そのクラスだけでなく、そのクラスのクライアントによって作成された暗黙のコントラクトも尊重する必要があります。ボブおじさんは、基本的な Square extends Rectangle ジレンマを使用して Liskov Substitution Principle に簡単に違反することがいかに簡単であるかの素晴らしい例を提供しています。インターフェースには実装がないため、隠されたコントラクトもありません。

0
Michael Brown