私の同僚は、基本クラスとインターフェースのどちらを作成するかを選択するための経験則を考え出しました。
彼は言う:
実装しようとしているすべての新しいメソッドを想像してみてください。それらのそれぞれについて、これを考慮してください:このメソッドは、exactlyの複数のクラスによって実装されますが、anychange?答えが「はい」の場合、基本クラスを作成します。他のすべての状況では、インターフェースを作成します。
例えば:
クラス
cat
とdog
を検討してください。これらはクラスmammal
を拡張し、単一のメソッドpet()
を持っています。次に、クラスalligator
を追加します。これは何も拡張せず、単一のメソッドslither()
を持っています。ここで、
eat()
メソッドをそれらすべてに追加します。
eat()
メソッドの実装がcat
、dog
およびalligator
でまったく同じである場合、基本クラスを作成する必要があります(たとえば、animal
)、このメソッドを実装します。ただし、
alligator
での実装が少し異なる場合は、IEat
インターフェイスを作成し、mammal
とalligator
で実装する必要があります。
彼は、この方法はすべてのケースをカバーすると主張していますが、私には過度に単純化しているようです。
この経験則に従う価値はありますか?
これは良い経験則ではないと思います。コードの再利用が心配な場合は、PetEatingBehavior
を実装できます。これは、猫と犬の食事機能を定義する役割です。その後、IEat
とコードを一緒に再利用できます。
最近では、継承を使用する理由はますます少なくなっています。大きな利点の1つは、使いやすさです。 GUIフレームワークを取ります。このためのAPIを設計する一般的な方法は、巨大な基本クラスを公開し、ユーザーがオーバーライドできるメソッドを文書化することです。したがって、基本クラスによって「デフォルト」の実装が提供されるため、アプリケーションが管理する必要がある他のすべての複雑なことを無視できます。ただし、必要に応じて適切なメソッドを再実装することで、引き続きカスタマイズできます。
APIのインターフェースに固執する場合、ユーザーは通常、簡単な例を機能させるためにやらなければならない仕事が増えます。ただし、IEat
に含まれる情報がMammal
基本クラスよりも少ないという単純な理由から、インターフェースを備えたAPIは、多くの場合、結合が少なく、保守が容易です。したがって、消費者はより弱い契約に依存し、より多くのリファクタリングの機会を得ます。
Rich Hickeyを引用するには:Simple!= Easy
この経験則に従う価値はありますか?
それはまともな経験則ですが、私が違反した場所はかなりたくさんあります。
公平を期するために、私は(抽象的な)基本クラスを同僚よりもはるかに多く使用しています。私は防御的なプログラミングとしてこれを行います。
私(多くの言語)にとって、抽象基本クラスは、基本クラスが1つしか存在しない可能性があるという主要な制約を追加します。インターフェースではなく基本クラスを使用することを選択することで、「これはクラス(および任意の継承者)のコア機能であり、他の機能とwilly-nillyを混在させることは不適切です。インターフェースは反対です:「これはオブジェクトのいくつかの特性であり、必ずしもその中核的な責任ではありません。」.
これらの種類の設計の選択は、コードをどのように使用すべきかについて実装者に暗黙のガイドを作成し、拡張によってコードを記述します。コードベースへの影響が大きいため、設計を決定する際にはこれらのことをより強く考慮する傾向があります。
友人の単純化の問題は、メソッドを変更する必要があるかどうかを判断することが非常に難しいことです。スーパークラスとインターフェースの背後にある考え方を伝えるルールを使用することをお勧めします。
機能とは何かを考えて何をすべきかを理解するのではなく、作成しようとしている階層に目を向けてください。
ここに私の教授が違いを教えた方法があります:
スーパークラスはis a
およびインターフェースはacts like
またはis
。だから犬is a
哺乳類およびacts like
食べる人。これは、哺乳類がクラスであり、食べる人がインターフェースであるべきであることを私たちに伝えます。 is
の例はThe cake is eatable
またはThe book is writable
(eatable
とwritable
の両方のインターフェイスを作成)。
このような方法論を使用すると、合理的にシンプルで簡単になり、コードが何をするかではなく、構造と概念に合わせてコーディングするようになります。これにより、メンテナンスが容易になり、コードが読みやすくなり、デザインの作成が簡単になります。
コードが実際に何を言っているかに関係なく、このような方法を使用すると、5年後に戻って同じ方法を使用してステップをたどり、プログラムがどのように設計され、どのように相互作用するかを簡単に理解できます。
ちょうど私の2セント。
Exiztのリクエストに応じて、コメントをより長い回答に拡張しています。
人々が犯す最大の間違いは、インターフェースが単に空の抽象クラスであると信じることです。インターフェースは、プログラマーが「この規則に従う限り、あなたが何を私に与えてもかまわない」と言うための方法です。
.NETライブラリはすばらしい例です。たとえば、IEnumerable<T>
を受け入れる関数を作成すると、「私は気にしないhowyou areデータを保存しています。foreach
ループを使用できることを知りたいだけです。
これにより、非常に柔軟なコードが作成されます。突然、統合されるために必要なことは、既存のインターフェースのルールに従うことです。インターフェースの実装が困難または混乱している場合、それはおそらく、丸い穴に正方形のペグを突き刺そうとしているヒントです。
しかし、その後、「コードの再利用についてはどうですか?継承はすべてのコード再利用の問題の解決策であり、継承を使用することで、一度に記述してどこでも使用できるようになり、孤児を救出する過程で髄膜炎を治すことができると、CSの教授は教えてくれました。上昇する海から来て、これ以上涙などがなくなるだろうなど」
「コードの再利用」という言葉の響きが好きだからといって継承を使用するのはかなり悪い考えです。 Googleによるコードスタイリングガイド は、この点をかなり簡潔にします。
多くの場合、構成は継承よりも適切です。 ... [B]サブクラスを実装するコードがベースとサブクラスの間に分散しているため、実装を理解するのがより困難になる場合があります。サブクラスは仮想ではない関数をオーバーライドできないため、サブクラスは実装を変更できません。
継承が必ずしも答えではない理由を説明するために、MySpecialFileWriter†というクラスを使用します。継承がすべての問題の解決策であると盲目的に信じる人は、FileStream
のコードを複製しないように、FileStream
から継承しようとするべきだと主張するでしょう。賢い人々は、これが愚かであることを認識しています。 (ローカル変数またはメンバー変数として)クラスにFileStream
オブジェクトを配置し、その機能を使用するだけです。
FileStream
の例は不自然に思えるかもしれませんが、そうではありません。まったく同じ方法で同じインターフェイスを実装する2つのクラスがある場合、複製される操作をカプセル化する3つ目のクラスが必要です。あなたの目標は、レゴのようにまとめることができる自己完結型の再利用可能なブロックであるクラスを書くことです。
これは、継承がすべてのコストで回避されるべきであるという意味ではありません。考慮すべき点はたくさんありますが、ほとんどは「構成vs.継承」という質問を研究することでカバーされます。私たち自身のスタックオーバーフロー いくつかの良い答えがあります この件について。
結局のところ、同僚の感情には、情報に基づいた意思決定を行うために必要な深さや理解が欠けています。主題を調べて、自分で理解してください。
†継承を説明するとき、誰もが動物を使用します。それは役に立たない。 11年間の開発では、Cat
という名前のクラスを作成したことがないので、例として使用しません。
例がめったに意味をなさない、特に車や動物の場合はそうではありません。動物の食べる方法(または食品を加工する方法)は、その継承に実際には関連付けられていません。すべての動物に適用される単一の食事方法はありません。これは、その物理的機能(いわば入力、処理、出力の方法)により依存しています。哺乳類は肉食動物、草食動物、雑食動物などで、鳥、哺乳動物、魚は昆虫を食べることができます。この 動作 は、特定のパターンでキャプチャできます。
この場合、次のように動作を抽象化する方がよいでしょう。
public interface IFoodEater
{
void Eat(IFood food);
}
public class Animal : IFoodEater
{
private IFoodProcessor _foodProcessor;
public Animal(IFoodProcessor foodProcessor)
{
_foodProcessor = foodProcessor;
}
public void Eat(IFood food)
{
_foodProcessor.Process(food);
}
}
または、FoodProcessor
が互換性のある動物(ICarnivore : IFoodEater
、MeatProcessingVisitor
)。生きている動物のことを考えると、消化経路を取り除いて一般的なものに置き換えることを考えるのを妨げる可能性がありますが、あなたは実際に彼らを支持しています。
これにより、動物が基本クラスになり、さらに良いことに、新しい種類の食品とそれに対応するプロセッサーを追加するときに二度と変更する必要がないため、この特定の動物クラスを作ることに集中することができますとてもユニークに取り組んでいます。
ですから、はい、基本クラスはその場所にあり、経験則は適用されます(経験則であるため、多くの場合、常にではありません)。しかし、分離できる動作を探し続けます。
インターフェースは実際には契約です。機能はありません。実装するのは実装者次第です。契約を履行する必要があるため、選択の余地はありません。
抽象クラスは実装者に選択肢を提供します。誰もが実装する必要のあるメソッドを用意するか、オーバーライドできる基本機能をメソッドに提供するか、またはすべての継承者が使用できる基本機能を提供するかを選択できます。
例えば:
public abstract class Person
{
/// <summary>
/// Inheritors must implement a hello
/// </summary>
/// <returns>Hello</returns>
public abstract string SayHello();
/// <summary>
/// Inheritors can use base functionality, or override
/// </summary>
/// <returns></returns>
public virtual string SayGoodBye()
{
return "Good Bye";
}
/// <summary>
/// Base Functionality that is inherited
/// </summary>
public void DoSomething()
{
//Do Something Here
}
}
あなたの経験則は基本クラスでも処理できると思います。特に、多くの哺乳類はおそらく同じものを「食べる」ので、ほとんどの継承者が使用できる仮想のeatメソッドを実装して例外的なケースをオーバーライドできるため、基本クラスを使用する方がインターフェイスよりも優れている可能性があります。
「食べること」の定義が動物によって大きく異なる場合は、おそらく、そしてインターフェースがより良いでしょう。これは時々灰色の領域であり、個々の状況に依存します。
番号。
インターフェイスは、メソッドの実装方法に関するものです。メソッドの実装方法に基づいてインターフェースをいつ使用するかを決定している場合は、何か問題があります。
私にとって、インターフェースと基本クラスの違いは、要件がどこにあるかについてです。一部のコードが特定のAPIとこのAPIから出現する暗黙の動作を備えたクラスを必要とする場合、インターフェースを作成します。実際にそれを呼び出すコードなしでインターフェースを作成することはできません。
一方、基本クラスは通常、コードを再利用する方法として意図されているため、この基本クラスを呼び出すコードに煩わされることはほとんどなく、この基本クラスが属するオブジェクトの階層のみを考慮します。
私はこの質問への回答を自分で見て、他の人が何を言わなければならないかを確認していますが、これについて考えたいと思う3つのことを言います。
人々はインターフェイスにあまりにも多くの信仰を持ち、クラスにはあまりにも多くの信仰を入れています。インターフェースは基本的に非常に骨抜きにされた基本クラスであり、軽量なものを使用すると、過剰なボイラープレートコードなどにつながる場合があります。
メソッドをオーバーライドし、その中でsuper.method()
を呼び出して、残りのボディに基本クラスに干渉しないことを実行させるだけで、何も問題はありません。これは Liskov Substitution Principle に違反していません。
経験則として、一般に何かがベストプラクティスであるとしても、ソフトウェアエンジニアリングにおいてこれほど厳格で独断的であることは悪い考えです。
それで、私は塩の粒で言われたことをとります。
異なる実装が必要な場合は、常に抽象メソッドを持つ抽象基本クラスを持つことができるため、これは実際には良い経験則ではありません。すべての継承者がそのメソッドを持つことが保証されていますが、それぞれが独自の実装を定義しています。技術的には、抽象メソッドのセットのみを含む抽象クラスを作成でき、基本的にはインターフェースと同じです。
基本クラスは、いくつかのクラスで使用できる状態または動作の実際の実装が必要な場合に役立ちます。同じフィールドとメソッドを持ち、同じ実装を持つクラスがいくつかある場合は、おそらくそれを基本クラスにリファクタリングできます。相続方法に注意する必要があります。基本クラスの既存のすべてのフィールドまたはメソッドは、特に基本クラスとしてキャストされる場合、継承クラスで意味を持つ必要があります。
インターフェイスは、同じ方法でクラスのセットとやり取りする必要がある場合に役立ちますが、実際の実装を予測したり、気にする必要はありません。たとえば、コレクションインターフェイスのようなものを考えてみましょう。主は、誰かがアイテムのコレクションを実装することを決定する可能性のある無数の方法をすべて知っています。リンクされたリスト?配列リスト?スタック?キュー?このSearchAndReplaceメソッドは関係ありません。追加、削除、およびGetEnumeratorを呼び出すことができるものを渡す必要があるだけです。
私はこれに向けて言語的で意味論的なアプローチを取りたいと思います。 DRY
のインターフェースがありません。これは、それを実装する抽象クラスと一緒に集中化のメカニズムとして使用できますが、DRYを対象としたものではありません。
IAnimal
インターフェースは、ワニ、猫、犬と動物の概念の間のオールオアナッシング契約です。それが本質的に言っていることは、「あなたが動物になりたいなら、あなたはこれらすべての属性と特徴を持たなければならないか、あなたはもう動物ではない」ということです。
反対側の継承は、再利用のメカニズムです。この場合、適切な意味論的推論があると、どの方法をどこに行えばよいかを判断するのに役立つと思います。たとえば、すべての動物が眠る可能性がありますが、同じように眠りますが、基本クラスは使用しません。最初にSleep()
メソッドをIAnimal
インターフェースに重点を置いて(セマンティック)、動物になるために必要なものとして配置します。次に、猫、犬、ワニの基本抽象クラスを使用します自分を繰り返さないプログラミング手法として。
これが世の中で最も良い経験則であるかどうかはわかりません。 abstract
メソッドを基本クラスに配置して、各クラスに実装させることができます。すべてのmammal
は食べなければならないので、それを行うことは理にかなっています。次に、各mammal
は1つのeat
メソッドを使用できます。私は通常、オブジェクト間の通信にインターフェイスを使用するか、クラスを何かとしてマークするマーカーとしてのみ使用します。インターフェイスを使いすぎると、ずさんなコードになると思います。
私はまた、@ Panzercrisisの経験則は一般的なガイドラインにすぎないことに同意します。石に書いておくべきものではありません。間違いなくそれがうまくいかないときがあるでしょう。