「メソッドを変更せずに再利用する場合は、メソッドを基本クラスに配置し、それ以外の場合はインターフェースを作成する」というのは良い経験則ですか?


10

私の同僚は、基本クラスとインターフェイスのどちらを作成するかを選択するための経験則を考え出しました。

彼は言う:

実装しようとしているすべての新しいメソッドを想像してみてください。それらのそれぞれについて、このことを考慮してください。この方法は、内の複数のクラスによって実装されます正確にせずに、このフォームの任意の変化?答えが「はい」の場合は、基本クラスを作成します。他のすべての状況では、インターフェースを作成します。

例えば:

クラスを検討catしてdogクラスを拡張し、mammal単一の方法を持っているがpet()。次に、alligator何も拡張せず、1つのメソッドを持つクラスを追加しslither()ます。

次に、eat()それらすべてにメソッドを追加します。

実装した場合eat()の方法は、のためにまったく同じになりcatdogそしてalligator、私たちは、基本クラス(LETだと言う、作成する必要がありanimal、このメソッドを実装します)。

ただし、実装がalligator少しでも異なる場合は、IEatインターフェースを作成して作成mammalおよびalligator実装する必要があります。

彼はこの方法がすべてのケースをカバーすると主張しますが、私には過度に単純化しているようです。

この経験則に従う価値はありますか?


27
alligatorさんの実装eat、それは受け入れることで、もちろん異なり、catおよびdogパラメータとして。
sbichenko 2013年

1
実装を共有する抽象基本クラスが本当に必要だが、適切な拡張性のためにインターフェイスを使用する必要がある場合は、代わりに特性を使用できます。つまり、言語がこれをサポートしている場合です。
アモン

2
自分を隅に描くときは、ドアに最も近いところにいるのが最善です。
JeffO 2013年

10
人々が犯す最大の間違いは、インターフェースは単に空の抽象クラスであると信じることです。インターフェースは、プログラマーが「この規則に従う限り、あなたが何を私に与えてもかまわない」と言う方法です。次に、コードの再利用は、(理想的には)合成によって達成されます。あなたの同僚は間違っています。
riwalk 2013年

2
私のCS教授は、スーパークラスはである必要がis aあり、インターフェースはacts likeまたはであると教えましたis。だから犬のis a哺乳類とacts like食べる人。これは、哺乳類がクラスであり、食べる人がインターフェースであるべきであることを私たちに伝えます。それは常に非常に役立つガイドでした。追記:例isだろうThe cake is eatableThe book is writable
MirroredFate 2013年

回答:


13

これは良い経験則ではないと思います。コードの再利用が心配な場合はPetEatingBehavior、猫と犬の食事機能を定義する役割を実装できます。その後、IEat一緒にコードを再利用できます。

最近では、継承を使用する理由はますます少なくなっています。大きな利点の1つは、使いやすさです。GUIフレームワークを取ります。このためのAPIを設計する一般的な方法は、巨大な基本クラスを公開し、ユーザーがオーバーライドできるメソッドを文書化することです。したがって、「デフォルト」の実装は基本クラスによって提供されるため、アプリケーションが管理する必要がある他のすべての複雑なことを無視できます。ただし、必要に応じて適切なメソッドを再実装することで、引き続きカスタマイズできます。

APIのインターフェースに固執する場合、ユーザーは通常、簡単な例を機能させるためにやらなければならない仕事が増えます。しかし、インターフェースを備えたAPI IEatは、Mammal基本クラスよりも情報が少ないという単純な理由により、結合が少なく、保守が容易であることがよくあります。したがって、消費者はより弱い契約に依存するようになり、より多くのリファクタリングの機会などが得られます。

Rich Hickeyを引用するには:Simple!= Easy


PetEatingBehavior実装の基本的な例を教えていただけますか?
sbichenko 2013年

1
@exiztまず、名前を選ぶのが苦手です。だから、PetEatingBehaviorおそらく間違ったものです。これはおもちゃの例にすぎないため、実装は提供できません。しかし、リファクタリングの手順を説明できます。これには、猫/犬がeatメソッド(歯のインスタンス、胃のインスタンスなど)を定義するために使用するすべての情報を取得するコンストラクタがあります。それは彼らのeat方法で使用される猫と犬に共通のコードのみを含みます。PetEatingBehaviorメンバーを作成し、それをdog.eat/cat.eatに転送するだけです。
Simon Bergot 2013年

これがOPを継承から遠ざける唯一の答えだとは信じられません:(継承はコードの再利用のためではありません!
Danny Tuppeny 2013年

1
@DannyTuppenyどうして?
sbichenko 2013年

4
@exiztクラスからの継承は重要です。あなたは何かに(おそらく永続的な)依存関係を取っています、それは多くの言語では1つしか持つことができません。コードの再利用は、この頻繁にのみ使用される基本クラスを使い果たすにはかなり簡単なことです。他のクラスと共有したいメソッドになってしまったが、他のメソッドを再利用しているため(または多態的に使用される「実際の」階層の一部であるため)、すでに基本クラスがある場合はどうなるでしょうか?ClassAがClassBのタイプである場合は継承を使用します(たとえば、CarがVehicleから継承する場合)。内部の実装の詳細を共有する場合ではありません:-)
Danny Tuppeny 2013年

11

この経験則に従う価値はありますか?

それはまともな経験則ですが、私が違反するかなりの数の場所を知っています。

公平を期すために、私は(抽象的な)基本クラスを同僚よりもはるかに多く使用しています。私は防御的なプログラミングとしてこれを行います。

私(多くの言語)にとって、抽象基本クラスは、基本クラスが1つしか存在しない可能性があるという主要な制約を追加します。インターフェースではなく基本クラスを使用することを選択すると、「これはクラス(および任意の継承者)のコア機能であり、他の機能と無邪気に混在させることは不適切です」と言っています。インターフェースは反対です:「これはオブジェクトのいくつかの特性であり、必ずしもその中核的な責任ではありません。」

これらの種類の設計の選択は、コードをどのように使用すべきかについて実装者に暗黙のガイドを作成し、拡張によってコードを記述します。コードベースへの影響が大きいため、設計を決定する際にはこれらのことをより強く考慮する傾向があります。


1
3年後、この答えをもう一度読んで、これが大きなポイントであることがわかります。インターフェイスではなく基本クラスを使用することを選択すると、「これはクラス(および継承者)のコア機能であり、 willy-nillyを他の機能と混在させることは不適切です。」
sbichenko

9

友人の単純化の問題は、メソッドを変更する必要があるかどうかを判断するのが非常に難しいことです。スーパークラスとインターフェースの背後にある考え方を伝えるルールを使用することをお勧めします。

機能とは何かを考えて何をすべきかを理解するのではなく、作成しようとしている階層に注目してください。

ここに私の教授が違いを教えた方法があります:

スーパークラスはでis aあり、インターフェースはacts likeまたはisです。だから犬のis a哺乳類とacts like食べる人。これは、哺乳類がクラスであり、食べる人がインターフェースであるべきであることを私たちに伝えます。例は、isあろうThe cake is eatable、またはThe book is writable(両方行うeatablewritableインタフェース)。

このような方法論を使用すると、かなり単純で簡単であり、コードが何をするかではなく、構造と概念に合わせてコーディングする必要があります。これにより、メンテナンスが容易になり、コードが読みやすくなり、デザインの作成が簡単になります。

コードが実際に何を言っているかに関係なく、このような方法を使用すると、5年後に戻って同じ方法を使用してステップをたどり、プログラムの設計方法とプログラムの相互作用を簡単に理解できます。

ちょうど私の2セント。


6

exiztのリクエストに応じて、コメントをより長い回答に拡張しています。

人々が犯す最大の間違いは、インターフェースは単に空の抽象クラスであると信じることです。インターフェースは、プログラマーが「この規則に従う限り、あなたが私に何を与えてもかまわない」と言う方法です。

.NETライブラリは素晴らしい例です。たとえば、を受け入れる関数を作成する場合IEnumerable<T>、「データをどのように格納するは気にしませんforeach。ループを使用できることを知りたいだけです。

これにより、非常に柔軟なコードが作成されます。突然、統合されるために必要なことは、既存のインターフェースのルールに従うことです。インターフェースの実装が困難または混乱している場合、それはおそらく、丸い穴に正方形のペグを突き刺そうとしているヒントです。

「コードの再利用についてはどうでしょうか。CSの教授は、継承がすべてのコード再利用の問題の解決策であり、継承を使用することで、どこにでも書けばどこでも使用できるようになり、孤児を救出する過程で髄膜炎を治すことができると教えてくれました。波が海から来て、これ以上涙がなく、等々」

「コードの再利用」という言葉の響きが好きだからといって継承を使用するのはかなり悪い考えです。Googleコードスタイリングガイドは、この点をかなり簡潔にしています。

多くの場合、構成は継承よりも適切です。... [B]サブクラスを実装するコードがベースとサブクラスの間に分散しているため、実装を理解するのがより困難になる場合があります。サブクラスは仮想ではない関数をオーバーライドできないため、サブクラスは実装を変更できません。

継承が必ずしも答えではない理由を説明するために、MySpecialFileWriter†というクラスを使用します。継承がすべての問題の解決策であると盲目的に信じる人は、のコードFileStreamを複製しないように、から継承しようとするべきだと主張するでしょうFileStream。賢い人々は、これが愚かであることを認識しています。あなたは持っている必要がありFileStream、あなたのクラスのオブジェクトを(ローカルまたはメンバ変数など)とその機能を使用しています。

FileStream例は不自然に見えるかもしれませんが、そうではありません。まったく同じ方法で同じインターフェースを実装する2つのクラスがある場合、複製される操作をカプセル化する3番目のクラスが必要です。あなたの目標は、レゴのようにまとめることができる自己完結型の再利用可能なブロックであるクラスを書くことです。

これは、継承がすべてのコストで回避されるべきであるという意味ではありません。考慮すべき点はたくさんありますが、ほとんどは「組成vs継承」という質問を研究することでカバーされます。私たち自身のスタックオーバーフローには、この問題に関するいくつかの良い答えがあります。

結局のところ、同僚の感情には、情報に基づいた意思決定を行うために必要な深さや理解が欠けています。主題を調べて、自分で理解してください。

†継承を説明するとき、誰もが動物を使用します。それは役に立たない。11年間の開発では、という名前のクラスを作成したCatことがないので、例として使用しません。


したがって、私があなたを正しく理解していれば、依存性注入は継承と同じコード再利用機能を提供します。しかし、それでは継承を何に使うのでしょうか?ユースケースを提供しますか?(私は本当にそれを感謝しますFileStream
sbichenko 2013年

1
@exizt、いいえ、同じコードの再利用機能は提供していません。実装を適切に封じ込めるため、(多くの場合)より優れたコード再利用機能を提供します。クラスは、既存の関数をオーバーロードすることによって機能を暗黙的に取得して機能の半分を完全に変更するのではなく、オブジェクトとパブリックAPIを介して明示的に対話します。
riwalk 2013年

4

例がめったに意味をなさないことは、特に車や動物の場合はそうではありません。動物の食べる方法(または食品を加工する方法)は、その継承に実際には関連付けられていません。すべての動物に適用される単一の食事方法はありません。これは、その物理的機能(いわば入力、処理、および出力の方法)により依存しています。哺乳類は肉食動物、草食動物、雑食動物などで、鳥、哺乳動物、魚は昆虫を食べることができます。この動作は、特定のパターンでキャプチャできます。

この場合、次のように動作を抽象化する方がよいでしょう。

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 : IFoodEaterMeatProcessingVisitor)。生きている動物のことを考えると、消化経路を引き裂いて、それを一般的なものに置き換えることを考えるのを妨げる可能性がありますが、あなたは本当に彼らを支持しています。

これはあなたの動物をベースクラスにし、さらに良いことに、新しいタイプのフードとそれに対応するプロセッサーを追加するときに二度と変更する必要がないので、この特定の動物クラスを作ることに集中することができますとてもユニークに取り組んでいます。

ですから、はい、基本クラスはその場所にあり、経験則は適用されます(経験則であるため、多くの場合、常にではありません)。しかし、分離できる動作を探し続けます。


1
IFoodEaterは一種の冗長です。他に食べ物を食べられると思いますか?ええ、例としての動物はひどいものです。
陶酔の

1
肉食性植物はどうですか?:)真剣に、この場合にインターフェイスを使用しないことの1つの主要な問題は、動物と食べるという概念を密接に結び付けていることであり、Animal基本クラスが適切でない新しい抽象化を導入するのは、はるかに多くの作業です。
Julia Hayward

1
ここでは「食べる」というのは間違った考えだと思います。IConsumerインターフェイスについてはどうですか。肉食動物は肉を、草食動物は植物を、植物は日光と水を消費します。

2

インターフェースは実際には契約です。機能はありません。実装するのは実装者次第です。契約を履行する必要があるため、選択の余地はありません。

抽象クラスは実装者に選択肢を提供します。誰もが実装する必要のあるメソッドを用意するか、オーバーライドできる基本機能をメソッドに提供するか、またはすべての継承者が使用できる基本機能を提供するかを選択できます。

例えば:

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メソッドを実装して例外的なケースをオーバーライドできるため、基本クラスを使用する方がインターフェイスよりも優れている可能性があります。

「食べること」の定義が動物によって大きく異なる場合は、おそらくインターフェイスがより良いでしょう。これは時々灰色の領域であり、個々の状況に依存します。


1

番号。

インターフェイスは、メソッドの実装方法に関するものです。メソッドの実装方法に基づいてインターフェースを使用するという決定に基づいている場合は、何か間違っていることになります。

私にとって、インターフェースと基本クラスの違いは、要件がどこにあるかについてです。一部のコードが特定のAPIとこのAPIから出現する暗黙の動作を備えたクラスを必要とする場合、インターフェースを作成します。実際にそれを呼び出すコードなしでインターフェースを作成することはできません。

一方、基本クラスは通常、コードを再利用する方法として意図されているため、この基本クラスを呼び出すコードに煩わされることはほとんどなく、この基本クラスが属するオブジェクトの階層のみを考慮します。


待って、なぜすべての動物に再実装eat()するのですか?我々は作ることができ、それを実装して、私たちをすることはできませんか?しかし、要件についてのあなたのポイントを理解しました。mammal
sbichenko 2013年

@exizt:ごめんなさい、あなたの質問を間違って読んだ。
陶酔の

1

異なる実装が必要な場合は、常に抽象メソッドを持つ抽象基本クラスを持つことができるため、これは実際には良い経験則ではありません。すべての継承者がそのメソッドを持つことが保証されていますが、それぞれが独自の実装を定義しています。技術的には、抽象メソッドのセットのみを含む抽象クラスを作成することができ、基本的にはインターフェースと同じです。

基本クラスは、いくつかのクラスで使用できる状態または動作の実際の実装が必要な場合に役立ちます。フィールドとメソッドが同じで、実装が同じクラスが複数ある場合は、おそらくそれを基本クラスにリファクタリングできます。相続方法に注意する必要があります。基本クラスの既存のすべてのフィールドまたはメソッドは、特に基本クラスとしてキャストされる場合、継承クラスで意味を持つ必要があります。

インターフェイスは、同じ方法でクラスのセットとやり取りする必要がある場合に役立ちますが、実際の実装を予測したり、気にする必要はありません。たとえば、コレクションインターフェイスのようなものを考えてみましょう。主は、誰かがアイテムのコレクションを実装することを決定する可能性のある無数の方法をすべて知っています。リンクされたリスト?配列リスト?スタック?キュー?このSearchAndReplaceメソッドは関係ありません。単にAdd、Remove、およびGetEnumeratorを呼び出すことができるものを渡す必要があります。


1

私はこの質問への回答を自分で見て、他の人が何を言わなければならないかを確認していますが、これについて考えたいと思う3つのことを言います。

  1. 人々はインターフェイスにあまりにも多くの信仰を持ち、クラスにはあまりにも多くの信仰を入れています。インターフェースは基本的に非常に骨抜きにされた基本クラスであり、軽量なものを使用すると過剰なボイラープレートコードなどにつながる場合があります。

  2. メソッドのオーバーライド、メソッドsuper.method()内での呼び出し、および本体の残りの部分に基本クラスに干渉しないことのみを実行させることには何の問題もありません。これはリスコフ代替原則に違反しません。

  3. 経験則として、一般的に何かがベストプラクティスであるとしても、ソフトウェアエンジニアリングにおいてこれほど厳格で独断的であることは悪い考えです。

それで、私は塩の粒で言われたことをとります。


5
People put way too much faith into interfaces, and too little faith into classes.おかしい。私はその逆が本当だと思いがちです。
Simon Bergot 2013年

1
「これはリスコフ代替原則に違反しない。」しかし、それはLSPの違反になることは非常に近いです。申し訳ありませんが、安全であることをお勧めします。
陶酔の

1

私はこれに向けて言語的で意味論的なアプローチを取りたいと思います。のインターフェースがありませんDRY。これは、それを実装する抽象クラスと一緒に集中化のメカニズムとして使用できますが、DRYを対象としたものではありません。

インターフェースは契約です。

IAnimalインターフェースは、ワニ、猫、犬と動物の概念の間のオールオアナッシング契約です。それが本質的に言っていることは、「あなたが動物になりたいなら、あなたはこれらすべての属性と特徴を持たなければならないか、あなたはもう動物ではない」ということです。

反対側の継承は再利用のメカニズムです。この場合、適切な意味論的推論があると、どの方法をどこに行えばよいかを判断するのに役立つと思います。たとえば、すべての動物が眠る可能性がありますが、同じように眠りますが、基本クラスは使用しません。最初に、Sleep()メソッドがIAnimal動物になるために必要なもの(意味論)を強調する方法でインターフェイスに配置し、次に、猫、犬、ワニの基本抽象クラスをプログラミング技術として使用して、自分自身を繰り返さないようにします。


0

これが世の中で最も良い経験則であるかどうかはわかりません。あなたは置くことができabstract、基本クラスでメソッドを各クラスがそれを実装しています。mammal誰もが食べなければならないので、それをすることは理にかなっています。次に、それぞれmammalが1つのeat方法を使用できます。私は通常、オブジェクト間の通信にのみ、またはクラスを何かとしてマークするマーカーとしてのみインターフェイスを使用します。インターフェイスを使いすぎると、ずさんなコードになると思います。

私はまた、@ Panzercrisisに同意します。石に書いておくべきものではありません。間違いなく、それが機能しない場合があります。


-1

インターフェースはタイプです。それらは実装を提供しません。それらはそのタイプを処理するためのコントラクトを簡単に定義します。このコントラクトは、インターフェイスを実装するすべてのクラスが満たす必要があります。

インターフェイスと抽象/基本クラスの使用は、設計要件によって決定されるべきです。

単一のクラスはインターフェースを必要としません。

2つのクラスが関数内で交換可能な場合、それらは共通のインターフェースを実装する必要があります。同じように実装されている機能はすべて、インターフェースを実装する抽象クラスにリファクタリングできます。区別する機能がない場合、インターフェースはその時点で冗長と見なされる可能性があります。しかし、2つのクラスの違いは何ですか?

抽象クラスを型として使用すると、機能が追加され、階層が拡張されるため、一般的に面倒です。

継承よりも構成を優先する-これはすべてなくなります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.