インターフェイス分離の原則の2つの矛盾する定義–どちらが正しいですか?


14

ISPに関する記事を読むとき、ISPには2つの矛盾する定義があるようです。

最初の定義(参照によると123)、ISPは、インターフェイスを実装するクラスは、彼らが必要としない機能を実装することを強制されるべきではないと述べています。したがって、太いインターフェイスIFat

interface IFat
{
     void A();
     void B();
     void C();
     void D();
}

class MyClass: IFat
{ ... }

より小さなインターフェースに分割しISmall_1ISmall_2

interface ISmall_1
{
     void A();
     void B();
}

interface ISmall_2
{
     void C();
     void D();
}

class MyClass:ISmall_2
{ ... }

このよう以来、私のMyClass唯一の方法、それが必要とする(実装することが可能であるD()とのC()またのためのダミー実装を提供することを余儀なくされることなく、) A()B()およびC()

しかし、2番目の定義に従った(参照12はナザールMerzaによって答えは、ISPはと述べている)MyClient上のメソッドを呼び出しますMyService上の方法を知っておくべきではないMyService、それは必要としないこと。つまり、とMyClientの機能のみが必要な場合、代わりにC()D()

class MyService 
{
    public void A();
    public void B();
    public void C();
    public void D();
}

/*client code*/      
MyService service = ...;
service.C(); 
service.D();

私たちは分離する必要があります MyService'sメソッドをクライアント固有のインターフェイスにするます。

public interface ISmall_1
{
     void A();
     void B();
}

public interface ISmall_2
{
     void C();
     void D();
}

class MyService:ISmall_1, ISmall_2 
{ ... }

/*client code*/
ISmall_2 service = ...;
service.C(); 
service.D();

したがって、前者の定義では、ISPの目標は「IFatインターフェイスを実装するクラスの寿命を短縮する」ことであり、後者の場合、ISPの目標は「MyServiceのメソッドを呼び出すクライアントの寿命を短縮する」ことです。

ISPの2つの異なる定義のうち、実際に正しいのはどれですか?

@MARJAN VENEMA

1。

したがって、IFatをより小さなインターフェイスに分割する場合、どのメソッドが最終的にどのメンバーであるかに基づいてISmallインターフェイスを決定する必要があります。

同じインターフェース内に凝集メソッドを配置することは理にかなっていますが、ISPパターンでは、クライアントのニーズがインターフェースの「凝集性」より優先されると考えました。言い換えると、ISPで特定のクライアントが必要とするメソッドを同じインターフェイス内にまとめる必要があると考えましたが、それは、そのインターフェイスから、まとまりのために同じインターフェイス内に置くべきメソッドを除外することを意味しますか?

したがって、そこにしかコールに必要するクライアントの多くがあった場合にCutGreensはなく、またGrillMeat、我々は唯一置くべきISPパターンに付着するCutGreens内部ICookもではなく、GrillMeat2つの方法が非常に粘着性であっても、!

2。

あなたの混乱は、最初の定義に隠された仮定、つまり実装クラスがすでに単一の責任原則に従っているという隠された仮定から生じていると思います。

「SRPに従っていないクラスを実装する」とは、実装IFatするクラスまたはISmall_1/ を実装するクラスを指しISmall_2ますか?私はあなたが実装するクラスを参照していると仮定しますIFatか?もしそうなら、なぜ彼らはまだSRPに従っていないと思いますか?

ありがとう


4
両方が同じ原則によって提供される複数の定義が存在できないのはなぜですか?
ボブソン

5
これらの定義は互いに矛盾していません。
マイクパートリッジ

1
もちろん、クライアントのニーズがインターフェースの凝集性より優先されることはありません。この「ルール」の方法を使用して、場所を問わず、まったく意味をなさない単一のメソッドインターフェイスを作成できます。ルールに従うのをやめて、これらのルールが作成された目標を考え始めます。「SRPに従っていないクラス」では、例で特定のクラスについて話していませんでした。もう一度読んでください。最初の定義は、インターフェイスがISPをフォローしておらず、クラスがSRPをフォローしている場合にのみ、インターフェイスを分割します。
マルジャンヴェネマ

2
2番目の定義は、実装者を気にしません。呼び出し元の観点からインターフェイスを定義し、実装者が既に存在するかどうかについての仮定を行いません。おそらく、ISPに従ってこれらのインターフェイスを実装するようになると、当然、SRPを作成するときにSRPに従うことになります。
マルジャンヴェネマ

2
どのようなクライアントが存在し、どのような方法が必要かを事前にどのように知っていますか?できません。事前に知ることができるのは、インターフェースがどれほどまとまりがあるかです。
Tulainsコルドバ

回答:


6

両方とも正しい

ISP(Interface Segregation Principle)の目的は、インターフェイスを小さく、集中させることです。すべてのインターフェイスメンバーは、非常に高い凝集力を持つ必要があります。両方の定義は、「ジャックオブオールトレードズマスターオブノン」インターフェースを避けることを目的としています。

インターフェース分離とSRP(単一責任原則)には、同じ目標があります。それは、小さくて凝集性の高いソフトウェアコンポーネントを確保することです。それらは互いに補完し合っています。インターフェースの分離により、インターフェースが小さく、焦点が合っており、凝集性が高いことが保証されます。単一の責任原則に従うことにより、クラスが小さく、集中し、高度に凝集していることが保証されます。

あなたが言及する最初の定義は実装者に焦点を当て、2番目はクライアントに焦点を当てています。@ user61852に反して、私は実装者ではなく、インターフェイスのユーザー/呼び出し元であると見なします。

あなたの混乱は、最初の定義に隠された仮定、つまり実装クラスがすでに単一の責任原則に従っているという隠された仮定から生じていると思います。

私にとって、インターフェイスの呼び出し元としてクライアントを使用する2番目の定義は、目的を達成するためのより良い方法です。

分離

あなたの質問であなたは述べています:

このように、MyClassは必要なメソッド(D()およびC())のみを実装できるため、A()、B()およびC()のダミー実装も強制されません。

しかし、それは世界をひっくり返しています。

  • インターフェイスを実装するクラスは、実装するインターフェイスで必要なものを指示しません。
  • インターフェイスは、実装クラスが提供するメソッドを指示します。
  • インターフェースの呼び出し元は、実際には、インターフェースに提供するためにインターフェースに必要な機能、したがって実装者が提供すべき機能を指示するものです。

したがって、IFatより小さなインターフェイスに分割する場合、どのメソッドが最終的にどのISmallインターフェイスになるかは、メンバーの凝集度に基づいて決定する必要があります。

次のインターフェースを検討してください。

interface IEverythingButTheKitchenSink
{
     void DoDishes();
     void CleanSink();
     void CutGreens();
     void GrillMeat();
}

どの方法を使用しますICookか?その理由は何ですか?あなたがたまたまそれと他のいくつかのことを行うクラスを持っているが、他のメソッドのようなものは何もないのでCleanSink、あなたは一緒に組み立てますGrillMeatか?または、次のようなさらに2つの凝集したインターフェースに分割しますか?

interface IClean
{
     void DoDishes();
     void CleanSink();
}

interface ICook
{
     void CutGreens();
     void GrillMeat();
}

インターフェイス宣言ノート

インターフェースの定義は、できれば独立したユニット内にある必要がありますが、呼び出し側または実装側と絶対に共存する必要がある場合は、実際に呼び出し側と一緒にする必要があります。それ以外の場合、呼び出し元は実装者に直接依存し、インターフェイスの目的を完全に無効にします。参照:基本クラスと同じファイルでインターフェイスを宣言することは良い習慣ですか?プログラマーとなぜ実装するのではなく、それらを使用するクラスとのインターフェイスを配置する必要がありますか?StackOverflowで。


1
私が行った更新を見ることができますか?
EdvRusj

「呼び出し元は実装者に直接依存関係を取得します」... DIP(依存関係の反転の原則)に違反した場合のみ、呼び出し元の内部変数、パラメーター、戻り値などがDIPの義務としてtype ICookではなくtypeであるSomeCookImplementor場合、に依存する必要はありませんSomeCookImplementor
Tulainsコルドバ

@ user61852:インターフェース宣言と実装者が同じユニットにいる場合、その実装者にすぐに依存関係を取得します。必ずしも実行時ではありませんが、最も確かにプロジェクトレベルで、単にそれが存在するという事実によって。プロジェクトは、それまたはそれが使用するものなしではコンパイルできなくなります。また、依存性注入は、依存性反転の原理とは異なります。野生のDIPに
マージャンヴェネマ

この質問プログラマーのコード例を再利用しました。programs.stackexchange.com/ a / 271142/61852、すでに受け入れられた後に改善します。私はあなたに例の正当な信用を与えました。
Tulainsコルドバ

@ user61852クール:)(およびクレジットのおかげで)
マージャンヴェネマ

14

Gang of Fourのドキュメントで使用されている「クライアント」という単語と、サービスの利用者のような「クライアント」を混同しています。

Gang of Fourの定義で意図されている「クライアント」は、インターフェースを実装するクラスです。クラスAがインターフェースBを実装する場合、AはBのクライアントであると言います。それ以外の場合、「クライアントは使用しないインターフェースを実装することを強制されるべきではありません」(消費者のように)「クライアント」ドンので、意味がありませんが何も実装しません。このフレーズは、「クライアント」を「実装者」と見なした場合にのみ意味があります。

「クライアント」が、大きなインターフェースを実装する別のクラスのメソッドを「消費」(呼び出し)するクラスを意味する場合、気になる2つのメソッドを呼び出して残りを無視することで、残りから切り離すのに十分です使用しない方法。

原則の精神は、関連するメソッドのセットのみを考慮する場合、インターフェイス全体に準拠するために、「クライアント」(インターフェイスを実装するクラス)がダミーメソッドを実装する必要がないことです。

また、1箇所で行われた変更により影響が小さくなるように、カップリングの量をできるだけ少なくすることも目的としています。インターフェースを分離することにより、結合を減らします。

この問題は、インターフェースが多すぎて、メソッドを1つではなく複数のインターフェースに分割する必要がある場合に発生します。

どちらのコード例でも問題ありません。2番目の例では、「クライアント」が「別のクラスによって提供されるサービス/メソッドを消費/呼び出すクラス」を意味すると仮定しています。

あなたが与えた3つのリンクで説明されている概念に矛盾はありません。

SOLIDトークで「クライアント」が実装者であることを明確にしてください。


しかし、@ pdrによれば、すべてのリンクのコード例はISPに準拠していますが、ISPの定義は、「クライアント(別のクラスのメソッドを呼び出すクラス)をサービスについてよりも詳細に分離する」クライアント(実装者)が使用しないインターフェイスの実装を強制されるのを防ぎます。」
EdvRusj

1
@EdvRusj私の答えは、有名なGang of FourにいたときにMartin自身が書いたObject Mentor(Bob Martin enterprise)Webサイトのドキュメントに基づいています。ご存じのとおり、Gnag of FourはMartinを含むソフトウェアエンジニアのグループであり、SOLIDの頭字語を作り出し、原則を特定して文書化しました。docs.google.com/a/cleancoder.com/file/d/...
Tulainsコルドバ

あなたは@pdrに同意しないので、ISPの最初の定義(私の元の投稿を参照)はより快適だと思いますか?
-EdvRusj

@EdvRusj両方とも正しいと思います。しかし、2番目の方法では、クライアント/サーバーのメタファーを使用することにより、不必要な混乱を追加します。1つを選択する必要がある場合は、最初の公式のGang of Fourを使用します。しかし、結局のところ、SOLID原則の精神である結合と不要な依存関係を減らすことが重要です。どちらが正しいかは関係ありません。重要なことは、振る舞いに応じてインターフェースを分離することです。それで全部です。しかし、疑わしい場合は、元のソースに移動してください。
Tulainsコルドバ

3
「クライアント」がSOLIDトークの実装者であるというあなたの主張に私は同意しません。1つは、プロバイダー(実装者)が提供(実装)しているクライアントを呼び出すことは言語的にナンセンスです。これを伝えるSOLIDの記事も見たことがありませんが、単に見落としているかもしれません。最も重要なのは、インターフェイスの実装者を、インターフェイスに何を含めるかを決定するものとして設定することです。それは私には意味がありません。インターフェースの呼び出し元/ユーザーは、インターフェースから必要なものを定義し、そのインターフェースの実装者(複数)はそれを提供するようにバインドされています。
マルジャンヴェネマ

5

ISPは、クライアントが必要以上にサービスについて知ることからクライアントを隔離することについてです(たとえば、無関係な変更からサービスを保護する)。2番目の定義は正しいです。私の読書では、これらの3つの記事のうち1つだけが別の方法(最初の記事)を示唆しており、それは単に間違っています。(編集:いいえ、間違っていない、誤解を招くだけです。)

最初の定義は、LSPとより密接にリンクしています。


3
ISPでは、クライアントが使用しないインターフェイスコンポーネントを消費することを強制されるべきではありません。LSPでは、呼び出し元のコードがメソッドAを必要とするため、SERVICESにメソッドDの実装を強制しないでください。これらは矛盾せず、補完的です。
pdr

2
@ EdvRusj、ClientAが呼び出すInterfaceAを実装するオブジェクトは、実際にはクライアントBが必要とするInterfaceBを実装するオブジェクトとまったく同じである場合があります。同じクライアントが同じオブジェクトを異なるクラスとして見る必要があるまれな場合、コードは通常「タッチ」。ある目的ではA、他の目的ではBとして見ることになります。
エイミーブランケンシップ

1
@EdvRusj:ここでインターフェースの定義を再考すると役立つかもしれません。C#/ Javaの用語では、必ずしもインターフェイスではありません。クライアントAがラッパークラスAXを使用してサービスXと「インターフェイス」するように、いくつかの単純なクラスがラップされた複雑なサービスを持つことができます。したがって、AとAXに影響を与える方法でXを変更すると、 BXとBに影響を与えることを強制
pdr

1
@EdvRusj:AとBの両方がXを呼び出しているのか、一方がYを呼び出しているのか、もう一方がZを呼び出しているのかは気にしないと言う方が正確です。これがISPの基本的なポイントです。したがって、どの実装を使用するかを選択し、後で簡単に変更できます。ISPはどちらのルートも優先しませんが、LSPとSRPは優先します。
pdr

1
@EdvRusjいいえ、クライアントAはサービスXをサービスyに置き換えることができます。どちらもインターフェースAXを実装します。Xおよび/またはYは他のインターフェースを実装できますが、クライアントがそれらをAXとして呼び出す場合、それらの他のインターフェースは気にしません。
エイミーブランケンシップ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.