web-dev-qa-db-ja.com

Cでインターフェース分離の原則を適用する方法は?

「M」と言うモジュールがあります。これには、「C1」、「C2」、「C3」と言うクライアントがいくつかあります。モジュールMの名前空間、つまりAPIの宣言とそれが公開するデータを、次のような方法でヘッダーファイルに割り当てたい-

  1. どのクライアントでも、必要なデータとAPIのみが表示されます。モジュールの名前空間の残りの部分はクライアントから隠されています。つまり、 インターフェース分離の原則 に従います。
  2. 宣言が複数のヘッダーファイルで繰り返されない、つまり違反しない [〜#〜] dry [〜#〜]
  3. モジュールMは、クライアントに依存していません。
  4. クライアントは、使用されていないモジュールMの一部に加えられた変更の影響を受けません。
  5. 既存のクライアントは、さらにクライアントを追加(または削除)しても影響を受けません。

現在、私はクライアントの要件に応じてモジュールの名前空間を分割することでこれに対処しています。たとえば、以下の画像には、3つのクライアントが必要とするモジュールの名前空間のさまざまな部分が示されています。クライアントの要件が重複しています。モジュールの名前空間は、4つの別々のヘッダーファイル 'a'、 '1'、 '2'および '3'に分かれています。

Module namespace partitioning

ただし、これは前述の要件の一部、つまりR3およびR5に違反しています。このパーティショニングはクライアントの性質に依存するため、要件3に違反しています。また、新しいクライアントを追加すると、このパーティションは変更され、要件5に違反します。上の画像の右側にあるように、新しいクライアントを追加すると、モジュールの名前空間は7つのヘッダーファイルに分割されます- 'a'、 'b'、 'c'、 '1'、 '2 *'、 '3 *'および '4'。 2つの古いクライアント用のヘッダーファイルが変更され、それによって再構築がトリガーされます。

不自然な方法でCのインターフェイス分離を実現する方法はありますか?
「はい」の場合、上記の例をどのように処理しますか?

私が想像する非現実的な仮説的な解決策は-
モジュールには、名前空間全体をカバーする1つのファットヘッダーファイルがあります。このヘッダーファイルは、Wikipediaページのように、アドレス可能なセクションとサブセクションに分かれています。各クライアントには、そのクライアント用に調整された特定のヘッダーファイルがあります。クライアント固有のヘッダーファイルは、ファットヘッダーファイルのセクション/サブセクションへのハイパーリンクの単なるリストです。また、ビルドシステムは、モジュールのヘッダーで指すセクションのいずれかが変更されている場合、クライアント固有のヘッダーファイルを「変更済み」として認識しなければなりません。

15
work.bin

インターフェースの分離は、一般的に、クライアントの要件に基づくべきではありません。それを実現するには、アプローチ全体を変更する必要があります。機能をcoherentグループにグループ化して、インターフェイスをモジュール化します。つまり、グループ化は、クライアントの要件ではなく、機能自体の一貫性に基づいています。その場合、I1、I2などのインターフェイスのセットが作成されます。クライアントC1はI2のみを使用できます。クライアントC2はI1やI5などを使用する場合があります。クライアントが複数のIiを使用する場合は問題ありません。インターフェイスを一貫性のあるモジュールに分解した場合、そこが問題の核心です。

繰り返しますが、ISPはクライアントベースではありません。それはインターフェースをより小さなモジュールに分解することについてです。これが適切に行われると、クライアントが必要なだけの機能を利用できるようになります。

このアプローチでは、クライアントはいくつでも増やすことができますが、Mは影響を受けません。各クライアントは、必要に応じてインターフェイスの1つまたはいくつかの組み合わせを使用します。クライアントCがI1とI3を含める必要があるが、これらのインターフェイスのすべての機能を使用する必要がない場合はありますか?はい、それは問題ではありません。使用するインターフェースの数が最も少ないだけです。

5
Nazar Merza

インターフェース分離原理 は次のように述べています:

使用しないメソッドに依存するよう強制されるクライアントはありません。 ISPは、非常に大きいインターフェースをより小さくより具体的なインターフェースに分割するため、クライアントは、関心のあるメソッドについてのみ知る必要があります。

ここには未回答の質問がいくつかあります。 1つは次のとおりです。

どれくらい小さい?

あなたは言う:

現在、私はクライアントの要件に応じてモジュールの名前空間を分割することでこれに対処しています。

私はこのマニュアルを ダックタイピング と呼んでいます。クライアントが必要とするものだけを公開するインターフェースを構築します。インターフェース分離の原則は、単に手動のダックタイピングではありません。

しかし、ISPは、再利用可能な「一貫性のある」ロールインターフェイスを単に求めるだけではありません。 「一貫した」役割のインターフェース設計では、独自の役割のニーズを持つ新しいクライアントの追加を完全に防ぐことはできません。

[〜#〜] isp [〜#〜] は、サービスへの変更の影響からクライアントを分離する方法です。これは、変更を加えるときにビルドを高速化するためのものです。もちろん、クライアントを壊さないなど、他の利点もありますが、それが主なポイントでした。サービスcount()関数のシグネチャを変更する場合、count()を使用しないクライアントを編集して再コンパイルする必要がない場合は問題ありません。

これが、インターフェース分離の原則を気にする理由です。それは私が信仰を重んじるものではありません。それは本当の問題を解決します。

したがって、それを適用する方法で問題が解決されるはずです。必要な変更の正しい例だけでは打ち負かすことができないISPを適用するための頭の痛い方法はありません。あなたはシステムがどのように変化しているかを見て、物事を静める選択をすることになっています。オプションを見てみましょう。

最初に自問してみてください。現在、サービスインターフェイスの変更は難しいですか。そうでない場合は、外に出て、落ち着くまで遊んでください。これは知的運動ではありません。治療が病気よりも悪くないことを確認してください。

  1. 多くのクライアントが同じ機能のサブセットを使用する場合、それは「一貫した」再利用可能なインターフェースを主張します。サブセットはおそらく、サービスがクライアントに提供している役割と考えることができる1つのアイデアに焦点を当てています。これがうまくいくといいですね。これは常に機能するとは限りません。

    1. 多くのクライアントが機能の異なるサブセットを使用している場合、クライアントが実際に複数の役割を通じてサービスを使用している可能性があります。これで問題ありませんが、役割がわかりにくくなります。それらを見つけて、それらをばらばらにしてみてください。これは、ケース1に戻るかもしれません。クライアントは、単に複数のインターフェースを介してサービスを使用します。サービスのキャストを開始しないでください。サービスをクライアントに複数回渡すことを意味する場合。それは機能しますが、サービスが分割する必要のある大きな泥の塊ではないかどうか疑問に思います。

    2. 多くのクライアントが異なるサブセットを使用しているが、クライアントが複数のサブセットを使用することを許可している場合でも役割が表示されない場合は、ダックタイピング以外のインターフェースを設計することはできません。インターフェースを設計するこの方法は、クライアントが使用していない1つの機能にもクライアントがさらされないことを保証しますが、新しいクライアントの追加には常に新しいインターフェースの追加が含まれることをほぼ保証しますが、サービスの実装は知る必要はありません。それについては、インターフェースを統合する役割インターフェース。私たちは単にある痛みを別の痛みと交換しました。

  2. 多くのクライアントが異なるサブセットを使用している場合、重複し、予測できないサブセットを必要とする新しいクライアントが追加されることが予想され、サービスを分割したくない場合は、より機能的なソリューションを検討します。最初の2つのオプションが機能せず、パターンに何も従わず、さらに変更が加えられるという悪い場所にいるため、各関数に独自のインターフェイスを提供することを検討してください。ここまで来ても、ISPに障害が発生したわけではありません。何かが失敗した場合、それはオブジェクト指向のパラダイムでした。単一メソッドインターフェイスは、極端にISPに従います。これはキーボード入力のかなりの部分ですが、突然インターフェイスが再利用可能になることがあります。繰り返しますが、これを行う前にこの問題を解決する簡単な方法がないことを確認してください。ただし、これはISPに準拠するソリューションであり、クライアントが本当に気にしないサービスの変更から隔離する必要があります。

そのため、実際には非常に小さくなります。

私はこの質問を、極端な場合にISPを適用するための課題として取り上げました。ただし、極端な状態は避けた方がよいことに注意してください。他の SOLID原則 を適用するよく考えられた設計では、通常、これらの問題はほとんど発生せず、問題にもなりません。


別の未回答の質問は次のとおりです。

これらのインターフェースは誰が所有していますか?

「ライブラリ」という考え方で設計されたインターフェースが何度も見られます。私たちは皆、monkey-see-monkey-doコーディングで罪を犯しています。そこでは、あなたが何かをしているだけです。私たちはインターフェースについても同じことを犯しています。

ライブラリのクラス用に設計されたインターフェイスを見ると、以前は考えていました。ああ、これらの人はプロです。これは、インターフェースを実行する正しい方法でなければなりません。私が理解できなかったのは、図書館の境界にはそれ自体のニーズと問題があるということです。一つには、ライブラリはそのクライアントの設計を完全に知らないということです。すべての境界が同じというわけではありません。また、同じ境界でも、交差する方法が異なる場合があります。

インターフェイスのデザインを確認する2つの簡単な方法を次に示します。

  • サービス所有のインターフェース。一部の人々は、サービスが実行できるすべてを公開するためにすべてのインターフェースを設計します。 IDEにリファクタリングオプションを見つけて、フィードするクラスを使用してインターフェースを作成することもできます。

  • クライアント所有のインターフェース。 ISPは、これは正しいことであり、所有するサービスは間違っていると主張しているようです。クライアントのニーズを念頭に置いて、すべてのインターフェースを分割する必要があります。クライアントはインターフェースを所有しているため、それを定義する必要があります。

それで、誰が正しいのですか?

プラグインを検討してください:

enter image description here

ここでインターフェースを所有しているのは誰ですか?クライアント?サービス?

両方がわかります。

ここの色はレイヤーです。赤のレイヤー(右)は、緑のレイヤー(左)については何も認識していません。緑のレイヤーは、赤のレイヤーに触れることなく変更または交換できます。そうすれば、緑の層を赤の層に差し込むことができます。

何について何を知っているはずなのか、何を知っておくべきでないのかを知るのが好きです。私にとって、「何について何を知っているか」は、最も重要なアーキテクチャの質問です。

いくつかの語彙を明確にしましょう:

[Client] --> [Interface] <|-- [Service]

----- Flow ----- of ----- control ---->

クライアントは使用するものです。

サービスは使用されるものです。

Interactorはたまたま両方です。

ISPは、クライアントのインターフェースを分割すると言います。いいでしょう、ここに適用しましょう:

  • Presenter(サービス)はOutput Port <I>インターフェースに指示するべきではありません。インターフェイスは、Interactor(ここではクライアントとして機能)が必要とするものに限定する必要があります。つまり、インターフェイスはInteractorについて知っており、ISPをフォローするには、それに応じて変更する必要があります。そして、これは結構です。

  • Interactor(ここではサービスとして機能)はInput Port <I>インターフェースに指示するべきではありません。インターフェイスは、Controller(クライアント)が必要とするものに限定する必要があります。つまり、インターフェイスはControllerについて知っており、ISPをフォローするには、それに応じて変更する必要があります。そして、これはではありません

赤のレイヤーは緑のレイヤーを認識していないため、2つ目は問題ありません。では、ISPは間違っているのでしょうか?まあ...ちょっと。絶対的な原則はありません。これは、サービスが実行できるすべてのことをインターフェイスに表示することを好む間抜けが正しいことが判明した場合です。

少なくとも、Interactorがこのユースケースのニーズ以外に何も実行しない場合は、これらは適切です。 Interactorが他のユースケースの処理を行う場合、Input Port <I>がそれらについて知る必要がある理由はありません。 Interactorが1つのユースケースに集中できない理由がわからないため、これは問題ではありませんが、問題が発生します。

しかし、input port <I>インターフェースはControllerクライアントにそれ自体を従属させることができず、これを真のプラグインにすることができません。これは「ライブラリ」境界です。まったく異なるプログラミングショップが、赤い層が公開されてから数年後に緑の層を作成している可能性があります。

「ライブラリ」の境界を越えており、反対側にインターフェイスを所有していない場合でもISPを適用する必要があると感じた場合は、インターフェイスを変更せずに狭める方法を見つける必要があります。

これを取り除く1つの方法は、アダプターです。 ControlerのようなクライアントとInput Port <I>インターフェースの間に配置します。アダプターはInteractorInput Port <I>として受け入れ、作業を委任します。ただし、グリーンレイヤーが所有する1つまたは複数のロールインターフェイスを通じて、Controllerのようなクライアントが必要とするものだけが公開されます。アダプターはISP自体には従いませんが、Controllerのようなより複雑なクラスがISPを楽しむことができます。これは、Controllerのようなクライアントよりもアダプターが少なく、それらを使用するアダプターがあり、ライブラリーの境界を越えて、公開されているにもかかわらずライブラリーの変更が停止しないという異常な状況にある場合に役立ちます。 。 Firefoxを見てください。今、これらの変更はあなたのアダプターを壊すだけです。

これはどういう意味ですか?正直なところ、あなたが何をすべきかを説明するのに十分な情報をあなたが提供していないことを意味します。 ISPに従っていないことが問題の原因かどうかはわかりません。これをフォローしても問題が発生しないかどうかはわかりません。

私はあなたが簡単な指針を探していることを知っています。 ISPはそうしようとします。しかし、それは言われないままにしておく。私はそれを信じています。はい、正当な理由なしに、クライアントが使用しないメソッドに依存するように強制しないでください!

プラグインを受け入れる何かを設計するなどの正当な理由がある場合は、ISPの原因に従わない問題(クライアントを壊さずに変更するのは難しい)とそれらを軽減する方法(Interactorまたは少なくともInput Port <I>は1つの安定した使用例に焦点を当てています)。

3
candied_orange

宣言で提供されるのと同じ情報が常に定義で繰り返されます。それはこの言語が機能する方法です。また、複数のヘッダーファイルで宣言を繰り返しても、DRYに違反しません。これはかなり一般的に使用されている手法です(少なくとも標準ライブラリでは)。

ドキュメントまたは実装を繰り返すと、DRYに違反します

クライアントのコードが私によって書かれていない限り、私はこれを気にしないでしょう。

1

したがって、この点:

existent clients are unaffected by the addition (or deletion) of more clients.

あなたがYAGNIである別の重要な原則に違反していることをあきらめます。何百人ものクライアントがいるときは気にします。何か前もって考えてみると、このコードのために追加のクライアントがないことがわかります。

二番目

 partitioning depends on the nature of clients

なぜコードがDIを使用しない、依存関係の逆転、何もない、ライブラリの何もクライアントの性質に依存してはならないのです。

最終的には、コードの下に追加のレイヤーが必要であるように見えます。DIを使用すると、DRYに打ち勝つことができます(DIは、前面コードはこの追加レイヤーにのみ依存し、クライアントは前面インターフェースにのみ依存します)。
これは、od現実のものです。したがって、別のモジュールの下のyorモジュールレイヤーで使用するものと同じものを作成します。このようにして、下のレイヤーを使用すると、次のことが実現します。

どのクライアントでも、必要なデータとAPIのみが表示されます。モジュールの残りの名前空間はクライアントから隠されています。つまり、インターフェース分離の原則に準拠しています。

はい

複数のヘッダーファイルで宣言が繰り返されていない、つまりDRYに違反していない。モジュールMは、クライアントに依存していません。

はい

クライアントは、使用されていないモジュールMの一部に加えられた変更の影響を受けません。

はい

既存のクライアントは、さらにクライアントを追加(または削除)しても影響を受けません。

はい

1
Mateusz

私は混乱を否認します。しかし、あなたの実用的な例は私の頭の中で解決策を描いています。私が自分の言葉で言えば、モジュールMのすべてのパーティションには、すべてのクライアントと多対多の排他的な関係があります。

サンプル構造

M.h      // fat header
 - P1    // Partition 1
 - P2    // ... 2
   - P21 // ... 2 section 1
 - P3    // ... 3
C1.c     // Client 1 (Needs to include P1, P3)
C2.c     // ... 2 (Needs to include P2)
C3.c     // ... 3 (Needs to include P1, P21, P3)

M.h

#ifdef P1
#define _PREF_ P1_             // Define Prefix ("PREF") = P1_
 void _PREF_init();            // Some partition specific function
#endif /* P1 */

#ifdef P2
#define _PREF_ P2_
 void _PREF_init();
#endif /* P2 */

#if defined(P21) || defined (P2) // Part 2.1
#define _PREF_ P2_1_
 void _PREF_oddone();
#endif /* P21 */

#ifdef P3
#define _PREF_ P3_
 void _PREF_init();
#endif /* P3 */

M.c

M.cファイルでは、実際に#ifdefsを使用する必要はありません。クライアントファイルが使用する関数が定義されている限り、.cファイルに入力したものがクライアントファイルに影響を与えないためです。

#include "M.h"
#define _PREF_ P1_        
void _PREF_init() { ... };

#define _PREF_ P2_
void _PREF_init() { ... }

#define _PREF_ P2_1_
void _PREF_oddone() { ... }

#define _PREF_ P3_
void _PREF_init() { ... }

C1.c

#define P1     // "invite" P1
#define P3     // "invite" P3
#include "M.h" // Open the door, but only the invited come in.

void main()
{
    P1_init();
    //P2_init();
    //P2_1_oddone();
    P3_init();
}

C2.c

#define P2
#include "M.h

void main()
{
    //P1_init();
    P2_init();
    P2_1_oddone();
    //P3_init();
}

C3.c

#define P1
#define P21
#define P3  
#include "M.h" 

void main()
{
    P1_init();
    //P2_init();
    P2_1_oddone();
    P3_init();
}

繰り返しますが、これがあなたが求めていることかどうかはわかりません。だから、一粒の塩と一緒に飲んでください。

0