web-dev-qa-db-ja.com

インターフェース分離原理を「極限」に適用しないのはなぜですか

クライアントは通常、1つのメソッドのみを使用することを提供しますが、メソッドは概念的に関連していますが、常にインターフェース分離の原則を極端に適用し、[多くの]単一メソッドインターフェースを持たないのはなぜですか?これに対する客観的なルールはありますか? 「おかしいな」とか「タイプが多すぎて読みにくく、管理が難しい」などではなく、論理的で明確なものです。 (それでも明確に契約に名前を付けたいので、機能は適切ではありませんか?).

例:

IGeometryManager
{
   Shape CreateTriangle();
   Shape CreateCircle();
   Shape CreateSquare();
   Shape CreateEllipse();
   Shape CreateCurve();
   Shape CreateLine();
   void RemoveAll();
}

結果:

ITriangleCreator
{
   Shape CreateTriangle();
}

ICircleCreator
{
   Shape CreateCircle();
}

ISquareCreator
{
   Shape CreateSquare();
}

IEllipseCreator
{
   Shape CreateEllipse();
}

ICurveCreator
{
   Shape CreateCurve();
}

ILineCreator
{
   Shape CreateLine();
}

IShapeRemover
{
   void RemoveAll();
}

これは関連するブログ投稿ですが、筆者のロジックに100%納得していません。 http://blog.ploeh.dk/2014/03/10/solid-the-next-step-is-functional

8
Den

この原理を極端に開発した人がいます。さらに、ドイツのソフトウェアエンジニアであるRalf Westphalが完全なプログラミングモデルを作成し、それを「イベントベースのコンポーネント」と呼び、 フロー設計 。実際、彼は「インターフェース形式」を使用せず、FuncまたはActionコントラクトのみを使用しており、これがおそらくより良い方法である理由について、多くの非常に良い議論を持っています。

彼はそれについてのほとんどの記事をドイツ語で公開していますが、 これは彼自身のアプローチではなく、彼のアプローチに関する英語の記事です 。昨年彼はそのトピックについての (安い)本 を出版した。

5
Doc Brown

いいえ、客観的なルールはありません。

あった客観的なルールがある場合、誰かがそれらを自動化する可能性があり、あなたは失業するでしょう。そのような決定は、常にそれ自体ではかなり明白な圧力間のトレードオフですが、状況によって相対的な強さは異なります。これまでのところ、そのような多次元最適化問題を適切に判断できるのは(一部の)人間だけです。

7
Kilian Foth

ほとんどのプログラミング言語とフレームワークでは、過度に分離されたインターフェイスにより、さまざまな機能の組み合わせを共有するオブジェクトを集約、構成、またはラップすることが困難になります。多くのタイプが多くのメソッドを定義するインターフェースを実装しているが、特定のインスタンスがそれらを実装することをどれだけうまく約束できるかを尋ねる手段も含んでいる場合、単一のラッパーまたは集約クラスは、そのようなすべてのタイプのインスタンスをラップまたは集約し、公開することができます。ラップされたインスタンスまたは集約されたインスタンスによってサポートされる機能の任意の組み合わせをクライアントに送信します。

代わりに、各クラスがサポートするメソッドのみを実装し、実装するすべてのメソッドを効果的にサポートすることが期待される場合、各ラッパークラスの作成者は、サポートするメソッドの固定セットを選択する必要があります。これらすべてのメソッドをサポートできないオブジェクトはラップできず、そのセットに含まれていないメソッドはクライアントから利用できません。限られたニーズを持つクライアントが限られた能力を持つオブジェクトをラップしたい場合、そして大きなニーズを持つクライアントがより大きい能力を持つオブジェクトをラップしたい場合、それらのクライアントには異なるラッパークラスが必要になります。ラッパークラスには、ラップされたメソッドごとに明示的なロジックを含める必要があるため、ラッパーの汎用ファミリーを使用してさまざまなユースケースを処理することはできませんでした。サポートされるメソッドのすべての組み合わせには、完全に別のラッパークラスが必要です。オブジェクトがさまざまな機能の組み合わせをサポートし、クライアントがさまざまな要件の組み合わせを持っている場合、必要なラッパーの数がまったく機能しなくなる可能性があります。

型システムにコンパイル時に特定の能力を必要とするオブジェクトが実際にその能力を持っていることを保証することは有用かもしれませんが、コンパイル時にすべてを検証しようとしてもうまくいかないという多くの状況があります。便利。あるインターフェースの実装が別のインターフェースに頻繁にトライキャストされる場合、それは両方のインターフェースのメンバーを単一のインターフェースに組み合わせる必要があることを示す良い兆候です。

3
supercat

インターフェースの設計は、インターフェースを使用するコンポーネント(コンシューマー)と、インターフェースを実装するコンポーネント(インプリメンター)に基づく必要があります。

実装者側

円だけを描画し、他の形状は描画しないクラスを書きたいと思いませんか?

多分あなたは各形状のためのハードコードされたアルゴリズムを備えたDefaultGeometryManagerを持っています。しかし、サークル用のものは遅いか欠陥があります。円を描くための専用の高度に最適化されたライブラリがありますが、DefaultGeometryManagerがこのライブラリに依存しないようにする必要があります。

DefaultGeometryManagerから継承して、circleメソッドをオーバーライドできます。しかし、このアプローチには限界があります。例えば。後で別の形状に似たようなことをしたい場合。

したがって、インターフェイスを分割し、IGeometryManagerに個々のインターフェイスから継承させることができます。各メソッドを専用の実装に委任するIGeometryManagerを実装するCompositeGeometryManagerを用意します。

LSPのおかげで、DefaultGeometryManagerはCompositeGeometryManagerの各依存関係の要件に一致します。そう:

defaultGeomegryManager = new DefaultGeometryManager();
circleCreator = new OptimizedCircleCreator();
geometryManager = new CompositeGeometryManager(defaultGeomegryManager, defaultGeomegryManager, defaultGeomegryManager, defaultGeomegryManager, defaultGeomegryManager, circleCreator);

各図形のメソッドシグネチャが同じである場合、代わりにIShapeCreatorインターフェイスを1つだけ持つことができます。しかし、私はこれはあなたの例には当てはまらないと言います。

デコレータ

デコレータクラスは、同時にコンシューマであり、プロバイダでもあります。通常は、メソッドの1つのみを考慮します。小さなインターフェイスの場合、デコレータは簡単に記述できます。

モックとテスト。

メソッドの数が少ないインターフェースは、明らかにモックするのが簡単です。

消費者:既知/内部

自分のインターフェイスがライブラリー内のコンシューマーのみを対象としている場合は、実際に必要なメソッドのみを備えた専用のインターフェイスを用意してもかまいません。または、ご提案のとおり、インターフェイスごとに1つのメソッド。このアプローチは、内部リファクタリングに最適です。

消費者:不明/外部/サードパーティ

パブリックAPIを提供する場合は、まだ知らないコンシューマー向けに設計する必要があります。おそらく、より使いやすい、よりリッチなインターフェースを提供したいでしょう。 「インターフェースごとに1つのメソッド」を使用すると、サードパーティのコードが正確なコンポーネントを各コンシューマに配信するのが難しくなります。

一方、コンシューマライブラリが依存しているライブラリを取り除き、それ自体で同じ機能を提供したい場合、リッチなインターフェイスが負担になります。

これとモックの引数は、パブリックAPIであっても「クラスごとに1つのメソッド」の引数と見なすことができます。しかし、よりリッチなインターフェースの全体的な有用性と快適さは、依然としてそれらを好ましい選択にしています。

ライブラリの混乱

一部の開発者は、ライブラリが非常に大きくなり(ファイル数/クラス数/インターフェース数)、すべての1回限りのインターフェースで不満を言うでしょう。

また、多くの内部リファクタリングの後、使用しなくなったがBCから削除できない残りのインターフェースとクラスが表示されます。

チームで作業している場合、これはあなたの共同開発者にとってはオフになるかもしれません。しかし、非常に多くのインターフェースとクラスを見るのは苛立たしいことかもしれませんが、実際には構造的な問題はありません。

個人的には、これは1つのクラス内に同じロジックを持つよりもはるかに好ましいと思います。

ネーミング

クラスapoproachごとに1つのメソッドを使用する場合、多くの名前を考え出す必要があるため、クラスとインターフェースの実際の単純な汎用命名パターンが必要です。

命名パターンは、将来の名前の衝突とあいまいさを防ぐ必要があります。

"Manager"や "Kernel"のような曖昧な用語は避けるべきですが、代わりに名前にメソッドの名前を多少反映させてください。

メソッド名はインターフェイス間で区別できる必要があります。そのため、名前の衝突なしに、継承によってインターフェイスを後で組み合わせることができます。例えば。 ICurveCreator :: create()およびILineCreator :: create()がある場合、IGeometryManagerで名前の衝突が発生します。

ジェネリック

言語がジェネリックスをサポートしている場合は、記述するインターフェースを少なくする必要があります。

結論

妥当なアプローチは、パブリックAPIに豊富な複合インターフェースを提供し、内部コンポーネント間のコントラクトとしてより小さいインターフェースを提供することです。

クラスごとに1つのメソッドまで下げることができますが、必ずしも必要ではありません。

クラスごとに1つのメソッドですべてのコードを開始できます。この方法では、多くの意思決定を回避し、IDEを使用すると、ジョブのオートコンプリートが非常に簡単になります。後でスケールアップして個々のコンポーネントを再結合することができるため、開発は非常に高速になります。

または、より大きなインターフェイスから始めて、必要に応じて徐々に分割します。

「次のステップは機能的です」

これは理論的には正しいです。しかし、これはあなたの言語でどれだけうまくサポートされているかによります。言語の「厳密なタイピング」機能の多くを失う可能性があります。

例えば。 PHPでは、インターフェースとクラスの言語機能は関数よりも豊富です。モダンPHPには、匿名関数と「呼び出し可能」タイプがあります。しかし、パラメータタイプのヒントでは、シグネチャを区別できません。

そして、たとえそうであっても:必要なシグネチャが単に「パラメータなしで文字列を返す関数」である場合はどうでしょうか。これはまだ非常に恣意的です。署名は技術契約です。インターフェースは、技術的契約とセマンティック契約の組み合わせです。

値オブジェクトはこの問題を軽減し、署名をより具体的にすることができます。

また、クラスは、インスタンス変数を編成するための(より冗長ではありますが)より快適な方法を提供します。

3
donquixote