抽象クラスのインターフェース


30

同僚と私は、基本クラスとインターフェイスの関係について異なる意見を持っています。インターフェイスの実装が必要なときにそのクラスを使用できる場合を除き、クラスはインターフェイスを実装すべきではないと考えています。言い換えれば、私はこのようなコードを見たい:

interface IFooWorker { void Work(); }

abstract class BaseWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker, IFooWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

DbWorkerは、インスタンス化可能なインターフェイスの実装であるため、IFooWorkerインターフェイスを取得するものです。契約を完全に満たします。私の同僚はほぼ同じものを好む:

interface IFooWorker { void Work(); }

abstract class BaseWorker : IFooWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

基本クラスがインターフェイスを取得する場所。これにより、基本クラスのすべての継承者もそのインターフェイスになります。これは私を悩ませますが、「基本クラスがインターフェイスの実装として単独で立つことができない」以外の具体的な理由を思い付くことができません。

彼の方法と私の方法の長所と短所は何ですか?


あなたの提案は、ダイヤモンド継承に非常によく似ています。これは、さらに多くの混乱を引き起こす可能性があります。
スポーク

@スポケ:どうやって見るの?
ニーニング

コメントで提供したリンクの@Niingには、単純なクラス図と、それが何であるかについての簡単な1行の説明があります。継承構造は基本的にひし形のように描かれているため、「ダイアモンド」問題と呼ばれます(つまり、♦️)。
スポイケ

@Spoike:この質問がダイヤモンドの継承を引き起こすのはなぜですか?
ニーニング

1
@Niing:同じメソッドが呼び出されたBaseWorkerとIFooWorkerを継承するとWork、ダイヤモンドの継承の問題が発生します(この場合、両者はWorkメソッドの契約を満たします)。Javaでは、インターフェイスのメソッドを実装する必要があるWorkため、プログラムが使用するメソッドの問題は回避されます。ただし、C ++などの言語では、これが明確になります。
スポーク

回答:


24

インターフェイスの実装が必要なときにそのクラスを使用できる場合を除き、クラスはインターフェイスを実装すべきではないと考えています。

BaseWorkerその要件を満たします。BaseWorkerオブジェクトを直接インスタンス化できないからといって、コントラクトを満たすBaseWorker ポインターを持てないというわけではありません。実際、これが抽象クラスのポイントです。

また、投稿した単純化された例を見分けるのは困難ですが、問題の一部は、インターフェイスと抽象クラスが冗長であることです。から派生IFooWorkerない他のクラスを実装していない限りBaseWorker、インターフェイスはまったく必要ありません。インターフェースは、多重継承を容易にするいくつかの追加の制限を備えた単なる抽象クラスです。

簡単な例ではわかりにくいことですが、保護されたメソッドの使用、派生クラスからのベースの明示的な参照、およびインターフェイス実装を宣言する明確な場所の欠如は、代わりに継承を不適切に使用していることを警告する兆候です組成。その継承がなければ、あなたの質問全体が無意味になります。


1
+1はBaseWorker直接インスタンス化できないからといって、与えられたBaseWorker myWorkerものが実装されていないという意味ではありませんIFooWorker
アヴナーシャハル

2
インターフェイスが単なる抽象クラスであることに同意しません。通常、抽象クラスはいくつかの実装の詳細を定義します(そうでなければ、インターフェースが使用されます)。これらの詳細が自分の好みに合わない場合は、基本クラスのすべての使用法を別のものに変更する必要があります。インターフェイスは浅いです。依存関係と依存関係の間の「プラグアンドソケット」関係を定義します。そのため実装の詳細への密結合はもはや問題ではありません。インターフェイスを継承するクラスが自分の好みに合わない場合は、クラスを削除して別のものを完全に追加します。あなたのコードはあまり気にしないでしょう。
キース

5
どちらにも利点がありますが、基本実装を定義する抽象クラスがあるかどうかに関係なく、通常はインターフェイスを常に依存関係に使用する必要があります。インターフェースと抽象クラスの組み合わせは、両方の世界の最高のものを提供します。インターフェイスは浅い表面のプラグとソケットの関係を維持し、抽象クラスは便利な共通コードを提供します。インターフェースの下にあるこのレベルを自由に取り除いて置き換えることができます。
キース

「ポインタ」あなたは意味ですかオブジェクトのインスタンス(ポインタが参照しますか)?インターフェイスはクラスではなく、タイプであり、置換ではなく、多重継承の不完全な代替として機能できます。つまり、「クラスに複数のソースからの動作を含める」ことができます。
-samis

46

あなたの同僚に同意する必要があります。

どちらの例でも、BaseWorkerは抽象メソッドWork()を定義しています。これは、すべてのサブクラスがIFooWorkerの契約を満たすことができることを意味します。この場合、BaseWorkerはインターフェイスを実装する必要があり、その実装はサブクラスに継承されると思います。これにより、各サブクラスが実際にIFooWorker(DRY原則)であることを明示的に示す必要がなくなります。

Work()をBaseWorkerのメソッドとして定義していない場合、またはIFooWorkerにBaseWorkerのサブクラスが必要としない、または必要としない他のメソッドがある場合、(明らかに)実際にIFooWorkerを実装するサブクラスを示す必要があります。


11
+1は同じものを投稿するでしょう。申し訳ありませんが、この場合もあなたの同僚は正しいと思います。何かが契約の履行を保証する場合、保証がインターフェース、抽象クラス、または具象クラスのいずれによって履行されるかにかかわらず、契約を継承する必要があります。簡単に言えば、保証人は保証する契約を継承する必要があります。
ジミー・ホッファ

2
一般的には同意しますが、「ベース」、「標準」、「デフォルト」、「ジェネリック」などの単語はクラス名のコードの匂いであると指摘したいと思います。抽象基本クラスの名前がインターフェイスとほぼ同じであるが、イタチの単語の1つが含まれている場合、それは多くの場合、不正な抽象化の兆候です。インタフェースは何がするかを定義、型継承はそれ何であるかを定義します。IFooand がある場合、BaseFooそれIFooは適切にきめの細かいインターフェースではないかBaseFoo、構成がより良い選択である可能性のある継承を使用していることを意味します。
アーロンノート

@Aaronaught良い点ですが、IFooのすべてまたはほとんどの実装が継承する必要のあるもの(サービスへのハンドルやDAO実装など)が本当にあるかもしれません。
マシューフリン

2
そして、基本クラスを呼び出すべきではありませんMyDaoFooか、SqlFooまたはHibernateFooそれが実装するクラスのただ一つの可能な木だことを示すために、何かIFoo?基本クラスが特定のライブラリまたはプラットフォームに結合されており、名前にこれが記載されていない場合、それは私にとってさらに臭いです
...-Aaronaught

@Aaronaught-はい。私は完全に同意します。
マシューフリン

11

私は一般的にあなたの同僚に同意します。

モデルを見てみましょう。基本クラスはIFooWorkerメソッドの公開も強制しますが、インターフェイスは子クラスによってのみ実装されます。まず、冗長です。子クラスがインターフェイスを実装するかどうかに関係なく、BaseWorkerの公開メソッドをオーバーライドする必要があります。同様に、IFooWorker実装は、BaseWorkerからヘルプを取得するかどうかにかかわらず機能を公開する必要があります。

これにより、階層があいまいになります。「すべてのIFooWorkersはBaseWorkers」は必ずしも真の声明であるとは限らず、「すべてのBaseooerはIFooWorkers」でもありません。したがって、IFooWorkerまたはBaseWorkerの実装である可能性のあるインスタンス変数、パラメーター、または戻り値の型を定義する場合(最初に継承する理由の1つである一般的な公開機能を利用する)、これらはすべて包括的であることが保証されています。一部のBaseWorkerは、IFooWorker型の変数に割り当てられません。逆も同様です。

あなたの同僚のモデルは、使用と交換がはるかに簡単です。「すべてのBaseWorkersはIFooWorkersである」は今や真のステートメントです。任意のBaseWorkerインスタンスを任意のIFooWorker依存関係に与えることができ、問題はありません。「すべてのIFooWorkersはBaseWorkers」という反対の文は真実ではありません。これにより、BaseWorkerをBetterBaseWorkerに置き換えることができ、消費するコード(IFooWorkerに依存)が違いを認識する必要がなくなります。


私はこの答えの音が好きですが、最後の部分にはあまり従いません。BetterBaseWorkerはIFooWorkerを実装する独立した抽象クラスであるため、私の仮想プラグインは単に「おっと!Better Base Workerがついに出ました!」と言うことができます。BaseWorker参照をBetterBaseWorkerに変更して1日呼び出します(おそらく、冗長なコードの巨大な部分を取り除くことができるクールな新しい戻り値で遊んでください)。
アンソニー

多かれ少なかれ、はい。大きな利点は、代わりにBetterBaseWorkersに変更する必要があるBaseWorkerへの参照の数を減らすことです。ほとんどのコードは、IFooWorkerを使用して代わりに具象クラスを直接参照しないため、IFooWorkerの依存関係(クラスのプロパティ、コンストラクターまたはメソッドのプロパティ)に割り当てられているものを変更しても、IFooWorkerを使用するコードに違いはありません使用中。
キース

6

これらの回答に警告を追加する必要があります。基本クラスをインターフェイスに結合すると、そのクラスの構造に力が生じます。基本的な例では、2つを結合する必要はありませんが、すべての場合に当てはまるわけではありません。

Javaのコレクションフレームワーククラスを使用します。

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

QueueコントラクトがLinkedListによって実装されているという事実は、懸念をAbstractListにプッシュしませんでした。

2つのケースの違いは何ですか?BaseWorkerの目的は、常にIWorkerで操作を実装することでした(その名前とインターフェイスで伝えられるように)。目的AbstractListとのキューが発散しているが、前者のdescenantはまだ後者の契約を実装することができます。


これは半分の時間で発生します。彼は常に基本クラスにインターフェースを実装することを好み、私は常に最終的なコンクリートにインターフェースを実装することを好みます。あなたが提示したシナリオは頻繁に起こり、ベースのインターフェースが私を悩ます理由の一部です。
ブライアンベッチャー

1
そうです、この方法でのインターフェイスの使用は、多くの開発環境で見られる継承の過剰使用の側面と考えています。GoFがデザインパターンブックで述べたように、継承よりも構成を優先し、基本クラスからインターフェイスを除外することは、まさにその原則を促進する方法の1つです。
ミハイダニラ

1
警告が表示されますが、OPの抽象クラスには、インターフェイスに一致する抽象メソッドが含まれていることに注意してください。BaseWorkerは暗黙的にIFooWorkerです。明示的にすると、この事実がさらに使いやすくなります。ただし、AbstractListには含まれていない多数のメソッドがQueueに含まれているため、AbstractListは、たとえ子が含まれていてもキューではありません。AbstractListとQueueは直交しています。
マシューフリン

この洞察力をありがとう。私はOPのケースがそのカテゴリーに分類されることに同意しますが、より深いルールを求めてより大きな議論があったことを感じ、私が読んだ(そして自分自身さえ持っている)傾向について私の観察を共有したかったですしばらくの間)継承を過度に使用する、おそらくOOPのツールボックスのツールの1つであるためです。
ミハイダニラ

ダン、あれから6年。:)
ミハイダニラ

2

IFooWorker新しいメソッドを追加するなど、変更するとどうなりますか?

BaseWorkerインターフェースを実装する場合、デフォルトで、たとえ抽象であっても、新しいメソッドを宣言する必要があります。一方、インターフェイスが実装されていない場合は、派生クラスでのみコンパイルエラーが発生します。

そのため、私は、基本クラスがインタフェースを実装して作ると思い、私は以来、かもしれない派生クラスを触れることなく、基本クラスの新しいメソッドのためのすべての機能を実装することができます。


1

最初に、抽象基本クラスとは何か、インターフェイスとは何かを考えます。どちらを使用するか、いつ使用しないかを検討してください。

両方が非常に類似した概念であると人々が考えることは一般的です、実際、それは一般的なインタビューの質問です(2つの違いは??)

したがって、抽象基本クラスは、メソッドのデフォルトの実装であるインターフェイスではできないことを提供します。(他にもあります。C#では、たとえばインターフェイスではなく抽象クラスで静的メソッドを使用できます)。

例として、これの1つの一般的な使用法はIDisposableです。抽象クラスは、IDisposableのDisposeメソッドを実装します。これは、派生クラスの基本クラスがすべて自動的に破棄されることを意味します。その後、いくつかのオプションで遊ぶことができます。Disposeを抽象化し、派生クラスに強制的に実装させます。仮想デフォルト実装を提供します。そうしないようにするか、仮想化または抽象化せずに、BeforeDispose、AfterDispose、OnDispose、Disposingなどの仮想メソッドを呼び出します。

したがって、すべての派生クラスがインターフェイスをサポートする必要があるときはいつでも、基本クラスになります。1つまたはいくつかのみがそのインターフェイスを必要とする場合、派生クラスに進みます。

それは実際、すべてが単純化されすぎています。もう1つの方法は、派生クラスにインターフェースを実装することさえせず、アダプターパターンを介してそれらを提供することです。最近見たこの例は、IObjectContextAdapterにありました。


1

抽象クラスとインターフェースの区別を完全に把握するのにはまだ何年もかかります。基本的な概念を理解できたと思うたびに、stackexchangeに目を向けると、2歩戻ります。しかし、トピックに関するいくつかの考えとOPの質問:

最初:

インターフェースには2つの一般的な説明があります。

  1. インターフェースは、任意のクラスが実装できるメソッドとプロパティのリストであり、インターフェースを実装することにより、クラスは、そのクラスと「インターフェース」するときにそれらのメソッド(およびそのシグネチャ)とそれらのプロパティ(およびそのタイプ)が利用できることを保証しますそのクラスのオブジェクト。インターフェースは契約です。

  2. インターフェースは、何も/何もしない抽象クラスです。これらは、親クラスを意味するのとは異なり、複数実装できるため便利です。たとえば、私はBananaBreadクラスのオブジェクトになり、BaseBreadから継承することもできますが、それはIWithNutsインターフェイスとITastesYummyインターフェイスの両方を実装できないという意味ではありません。私は単なるパンではないので、IDoesTheDishesインターフェイスを実装することさえできますよね?

抽象クラスには2つの一般的な説明があります。

  1. 抽象クラスは、yknow、あるものの事は何ができるかではありません。それは、本質であり、実際にはまったくありません。お待ちください、これが役立ちます。ボートは抽象クラスですが、セクシーなプレイボーイヨットはBaseBoatのサブクラスになります。

  2. 私は抽象クラスの本を読みますが、たぶんあなたはその本を読むべきです。おそらくあなたはそれを手に入れず、もしあなたがその本を読んでいなければ間違ったことをするでしょう。

ちなみに、本シターズは、私がまだ混乱して歩いていても、常に印象的なようです。

第二:

SOで、誰かがこの質問のより単純なバージョンである「クラシックを使用する理由」を尋ねました。また、簡単な例として空軍パイロットを使用した回答もありました。着陸することはできませんでしたが、いくつかの素晴らしいコメントがありました。そのうちの1つには、IFlyableインターフェースにtakeOff、pilotEjectなどのメソッドが含まれていることが記載されていました。しかし重要です。インターフェースは、オブジェクト/クラスを直感的にするか、少なくともその感覚を与えます。インターフェイスは、オブジェクトやデータの利益のためではなく、そのオブジェクトと対話する必要があるもののためのものです。古典的なフルーツ->アップル->富士またはシェイプ->三角形-> 継承の同等の例は、子孫に基づいて特定のオブジェクトを分類学的に理解するための優れたモデルです。一般的な品質、動作、オブジェクトが何かのグループであるかどうか、消費者とプロセッサに通知します。間違った場所に置いた場合、システムにホースをかけ、感覚データ、特定のデータストアへのコネクタ、または給与計算に必要な財務データ。

特定のPlaneオブジェクトには、緊急着陸の方法がある場合とない場合がありますが、その緊急事態を想定すると、DumbPlaneで覆われた他のすべての飛行可能オブジェクトと同じように起きて、開発者がquickLand彼らはそれがすべて同じだと思ったからです。すべてのネジのメーカーが正しいタイツの独自の解釈を持っている場合、または私のテレビに音量減少ボタンの上に音量増加ボタンがない場合、私はどのようにイライラするのと同じように。

抽象クラスは、子孫オブジェクトがそのクラスとして修飾する必要があるものを確立するモデルです。あなたがアヒルのすべての資質を持っていない場合、IQuackインターフェイスが実装されていることは問題ではありません、あなたの単なる奇妙なペンギンです。インターフェイスは、他のことを確信できない場合でも意味をなすものです。インターフェースが確実に類似していたため、ジェフゴールドブラムとスターバックは両方ともエイリアンの宇宙船を飛ばすことができました。

第3:

私はあなたの同僚に同意します。なぜなら、最初から特定の方法を実施する必要がある場合があるからです。Active Record ORMを作成している場合、saveメソッドが必要です。これは、インスタンス化できるサブクラス次第ではありません。また、ICRUDインターフェイスが1つの抽象クラスに排他的に結合されないほど移植性がある場合、他のクラスで実装して、その抽象クラスの子孫クラスのいずれかに精通しているすべての人に信頼性と直感性を持たせることができます。

一方、すべてのリストタイプがキューインターフェイスを実装する(または実装する必要がある)わけではないため、抽象クラスへのインターフェイスの関連付けにジャンプしない場合の早い例がありました。あなたはこのシナリオが半分の時間で起こると言いました、それはあなたとあなたの同僚が両方とも半分の時間を間違えていることを意味します、したがって、最善のことは議論し、議論し、検討し、彼らが正しいと判明した場合、カップリングを承認し、受け入れることです。しかし、哲学が目前の仕事にとって最良の哲学ではない場合でも、哲学に従う開発者にならないでください。


0

あなたが提案するように、なぜDbWorker実装するべきかには別の側面がありIFooWorkerます。

同僚のスタイルの場合、何らかの理由で後でリファクタリングが発生しDbWorker、抽象BaseWorkerクラスを拡張しないと見なされるとDbWorkerIFooWorkerインターフェイスが失われます。これは、必ずしもそうとは限らないが、IFooWorkerインターフェースを期待している場合、それを消費するクライアントに影響を与える可能性があります。


-1

最初に、インターフェイスと抽象クラスを異なる方法で定義したいと思います。

Interface: a contract that each class must fulfill if they are to implement that interface
Abstract class: a general class (i.e vehicle is an abstract class, while porche911 is not)

あなたの場合、すべての労働者が実装work()するのは、それが契約で要求されているからです。したがって、基本クラスBaseworkerはそのメソッドを明示的にまたは仮想関数として実装する必要があります。すべてのワーカーが必要とする単純な事実のため、このメソッドを基本クラスに配置することをお勧めしますwork()。したがって、ベースクラスは(その型のポインターを作成できないとしても)関数としてそれを含むことは論理的です。

今、DbWorker満足しないIFooWorkerインターフェースを、このクラスがないこと、特定の方法がある場合work()、それは本当にから継承された定義をオーバーロードする必要がありますBaseWorker。それがインタフェースをIFooWorker直接実装するべきでない理由はこれが特別なものでないということDbWorkerです。同様のクラスを実装するDbWorkerたびにこれを行うと、DRYに違反することになります。

2つのクラスが異なる方法で同じ関数を実装する場合、最も一般的なスーパークラスを探し始めることができます。ほとんどの場合、あなたはそれを見つけるでしょう。そうでない場合は、見続けるか、あきらめて、基本クラスを作成するのに十分な共通点がないことを受け入れます。


-3

うわー、すべてのこれらの答えと、例のインターフェイスが何の目的も果たさないことを指摘する人はいません。同じメソッドに継承とインターフェースを使用しないのは無意味です。どちらかを使用します。このフォーラムには、それぞれの利点と、どちらか一方を必要とするシナリオについて説明する回答を含む質問がたくさんあります。


3
それは特許の虚偽です。インターフェースは、実装に関係なく、システムが動作することを期待されるコントラクトです。継承された基本クラスは、このインターフェイスの多くの可能な実装の1つです。十分に大規模なシステムには、さまざまなインターフェイス(通常はジェネリックで閉じられている)を満たす複数の基本クラスがあります。これがまさにこのセットアップで行われた理由であり、それらを分割すると便利でした。
ブライアンベッチャー16年

これは、OOPの観点からは非常に悪い例です。「基本クラスは実装の1つにすぎない」と言うように(インターフェイスがあるはずであるか、インターフェイスに意味がない)、IFooWorkerインターフェイスを実装する非ワーカークラスがあると言っています。次に、特化されたfooworkerである基本クラスがあり(barworkerも存在する可能性があると想定する必要があります)、基本的なワーカーでもない他のfooworkerがあります!それは後方です。複数の方法で。
マーティンマート

1
おそらく、インターフェイスの「Worker」という言葉にこだわっています。「データソース」はどうですか?IDatasource <T>がある場合、SqlDatasource <T>、RestDatasource <T>、MongoDatasource <T>を簡単に持つことができます。ビジネスは、ジェネリックインターフェイスのどのクロージャーが抽象データソースのクロージング実装に進むかを決定します。
ブライアンベッチャー16年

DRYのためだけに継承を使用し、いくつかの一般的な実装を基本クラスに置くことは理にかなっています。ただし、オーバーライドされたメソッドをインターフェイスメソッドに合わせることはありません。基本クラスには一般的なサポートロジックが含まれます。それらが並んでいる場合、それは継承があなたの問題をうまく解決し、インターフェースが何の役にも立たないことを示します。モデル化する共通の特性がsingkeクラスツリーに適合しないため、継承が問題を解決しない場合にインターフェイスを使用します。そして、はい、例の文言はそれをさらに間違っています。
マーティンマート
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.