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


15

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

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

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

モジュール名前空間の分割

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

Cのインターフェイス分離を非自発的な方法で実現する方法はありますか?
はいの場合、上記の例をどのように扱いますか?

私が想像する非現実的な仮想ソリューションは次のようになります-
モジュールには、名前空間全体をカバーする1つの太いヘッダーファイルがあります。このヘッダーファイルは、ウィキペディアページのようなアドレス可能なセクションとサブセクションに分かれています。各クライアントには、特定のヘッダーファイルが用意されています。クライアント固有のヘッダーファイルは、ファットヘッダーファイルのセクション/サブセクションへのハイパーリンクの単なるリストです。また、ビルドシステムは、モジュールのヘッダーでポイントするセクションのいずれかが変更された場合、クライアント固有のヘッダーファイルを「変更された」ものとして認識する必要があります。


1
なぜこの問題はCに特有なのですか?Cには継承がないためですか?
ロバートハーベイ

また、ISPに違反すると、設計が改善されますか?
ロバートハーベイ

2
CはOOPの概念(インターフェースや継承など)を本質的にサポートしていません。粗雑な(ただし創造的な)ハッキングで対処します。インターフェイスをシミュレートするためのハックを探しています。通常、ヘッダーファイル全体がモジュールへのインターフェイスです。
work.bin 16

1
structインターフェースが必要なときにCで使用するものです。確かに、方法は少し難しいです。これは面白いと思うかもしれません:cs.rit.edu/~ats/books/ooc.pdf
ロバートハーベイ

とを使用structして同等のインターフェースを思いつくことができませんでしたfunction pointers
work.bin 16

回答:


5

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

この場合も、ISPはクライアントベースではありません。インターフェースをより小さなモジュールに分解することです。これが適切に行われた場合、クライアントが必要な最小限の機能にさらされることも保証されます。

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


あなたは確かにばらばらの、または重複しないグループを意味していたと思いますか?
Doc Brown

はい、ばらばらで重複していません。
ナザールメルザ

3

インターフェイスの棲み分け原理はこう述べています。

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

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

どれくらい小さい?

あなたは言う:

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

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

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

ISPは、サービスへの変更の影響からクライアントを隔離する方法です。変更を加えたときにビルドを高速化することを目的としていました。確かに、クライアントを壊さないなど、他の利点もありますが、それが主なポイントでした。サービスcount()関数のシグネチャを変更する場合、使用count()しないクライアントを編集および再コンパイルする必要がないと便利です。

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

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

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

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

  2.  

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

    2. 多くのクライアントが異なるサブセットを使用しているが、クライアントが複数を使用することを許可するロールさえ表示されない場合、インターフェースを設計するためにアヒルを入力することほど良いものはありません。インターフェースを設計するこの方法により、クライアントは使用していない1つの関数にもさらされないことが保証されますが、新しいクライアントの追加には、サービス実装が知る必要のない新しいインターフェースの追加が必ず含まれることがほぼ保証されますそれについては、役割インターフェースを集約するインターフェースが行います。私たちは、ある痛みを別の痛みと交換しただけです。

  3. 多くのクライアントが異なるサブセットを使用する場合、オーバーラップし、予測できないサブセットを必要とする新しいクライアントが追加されることが予想されます。サービスを分割したくない場合は、より機能的なソリューションを検討してください。最初の2つのオプションは機能せず、パターンに従わないものがあり、さらに変更が加えられる悪い場所にいるので、各機能を独自のインターフェイスにすることを検討してください。ここで終わるということは、ISPが失敗したということではありません。何かが失敗した場合、それはオブジェクト指向のパラダイムでした。単一メソッドインターフェイスは、極端にISPに従います。これはかなりのキーボード入力ですが、これによりインターフェイスが突然再利用可能になることがあります。繰り返しになりますが、

だから、彼らは確かに非常に小さくなります。

私はこの質問を、最も極端な場合にISPを適用するための挑戦とみなしました。ただし、極端なことは避けるのが最善です。他のSOLID原則を適用するよく考えられた設計では、これらの問題は通常発生せず、ほとんど問題になりません。


別の未回答の質問:

これらのインターフェースの所有者は誰ですか?

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

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

以下に、インターフェース設計を調べるための2つの簡単な方法を示します。

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

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

だから誰が正しい?

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

ここに画像の説明を入力してください

ここのインターフェースの所有者は誰ですか?クライアント?サービス?

両方が判明。

ここの色はレイヤーです。赤いレイヤー(右)は緑のレイヤー(左)について何も知らないはずです。緑のレイヤーは、赤のレイヤーに触れることなく変更または置換できます。これにより、緑のレイヤーを赤のレイヤーにプラグインできます。

何について何を知るべきか、何を知らないかを知るのが好きです。私にとって、「何が何を知っているのか」は、最も重要な建築上の疑問です。

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

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

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

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

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

Interactor たまたま両方です。

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

  • Presenter(サービス)はOutput Port <I>インターフェースに指示しないでください。インターフェースはInteractor(ここではクライアントとして機能する)必要なものに絞り込む必要があります。つまり、インターフェイスInteractorはISP について知っており、ISPに従うには、それに合わせて変更する必要があります。そしてこれは大丈夫です。

  • Interactor(ここではサービスとして動作します)は、Input Port <I>インターフェースに指示するべきではありません。インターフェイスはController(クライアント)が必要とするものに狭める必要があります。つまり、インターフェイスControllerはISP について知っており、ISPに従うには、それに合わせて変更する必要があります。そして、これは大丈夫ではありません

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

少なくとも、Interactorこのユースケースが必要とするもの以外に何もしなければ、彼らは正しい。Interactorが他のユースケースのために物事を行う場合、これInput Port <I>について知る必要がある理由はありません。なぜInteractor1つのユースケースだけに焦点を当てることができないのかわからないため、これは問題ではありませんが、何かが起こります。

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

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

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

これはどういう意味ですか?正直なところ、あなたが何をすべきかを伝えるのに十分な情報を提供していないことを意味します。ISPに従わないことが問題の原因であるかどうかはわかりません。それに従うことで、さらに問題が発生しないかどうかはわかりません。

あなたがシンプルなガイド原則を探しているのを知っています。ISPはそれを目指しています。しかし、多くのことは言われていません。私はそれを信じています。はい、正当な理由がない限り、クライアントが使用しないメソッドに依存することを強制しないでください!

プラグインを受け入れるように何かを設計するなど、正当な理由がある場合は、ISPの原因に従わない問題(クライアントに影響を与えずに変更するのは難しい)、およびそれらを緩和する方法(1つの安定した状態に保つInteractorか、少なくともInput Port <I>焦点を合わせる)に注意してください使用事例)。


入力いただきありがとうございます。複数のクライアントを持つサービス提供モジュールがあります。その名前空間には論理的に一貫した境界がありますが、クライアントはこれらの論理的な境界を越えてカットする必要があります。そのため、論理境界に基づいて名前空間を分割しても、ISPには役立ちません。したがって、質問の図に示すように、クライアントのニーズに基づいて名前空間を分割しました。ただし、これはクライアントに比較的依存し、クライアントを比較的頻繁に追加/削除できるため、クライアントに依存し、クライアントをサービスに結合する方法が貧弱になりますが、サービスの変更は最小限になります。
work.bin

私は現在、完全な名前空間のように、太いインターフェイスを提供するサービスに傾いています。クライアント固有のアダプターを介してこれらのサービスにアクセスするのはクライアント次第です。Cの用語では、クライアントが所有する関数ラッパーのファイルになります。サービスを変更すると、アダプターの再コンパイルが強制されますが、必ずしもクライアントは再コンパイルされません。.. <
contd

<contd> ..これにより、ビルド時間が最小限に抑えられ、実行時のコストでクライアントとサービスのカップリングが「ゆるい」状態に保たれ(中間ラッパー関数の呼び出し)、コードスペースが増加し、スタックの使用量が増加し、おそらくより多くのマインドスペースが増加します(プログラマー)アダプターの保守。
work.bin

私の現在のソリューションは現在私のニーズを満たしています。新しいアプローチはより多くの努力を必要とし、YAGNIに違反する可能性があります。各方法の長所と短所を比較検討し、ここでどちらの方法を選択するかを決めなければなりません。
work.bin

1

だからこの点:

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

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

第二

 partitioning depends on the nature of clients

あなたのコードがDIを使用していないのはなぜですか、依存関係の逆転、何もない、あなたのライブラリの何もあなたのクライアントの性質に依存するべきではありません。

最終的には、重複するもののニーズを満たすためにコードの下に追加のレイヤーが必要なように見えます(DIの場合、正面向きのコードはこの追加レイヤーのみに依存し、クライアントは正面向きのインターフェイスのみに依存します)。
これは本当のことです。したがって、別のモジュールの下のモジュール層で使用するものと同じものを作成します。このようにして、下にレイヤーを作成します。

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

はい

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

はい

クライアントは、モジュールMで使用されていない部分で行われた変更の影響を受けません。

はい

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

はい


1

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

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

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


0

私は混乱を否認します。しかし、あなたの実際の例は私の頭の中に解決策を描きます。私は自分の言葉に置くことができる場合は、次のモジュール内のすべてのパーティションを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)

Mh

#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 */

マック

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

#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();
}

繰り返しますが、これがあなたが求めているものかどうかはわかりません。だから塩の粒でそれを取る。


Mcはどのように見えますか?定義P1_init() P2_init()ますか?
work.bin

@ work.bin Mcは、関数間の名前空間を定義することを除いて、単純な.cファイルのように見えると思います。
Sanchke Dellowar

C1とC2の両方が存在すると仮定すると-何P1_init()P2_init()リンクするには?
work.bin

Mh / Mcファイルでは、プリプロセッサ_PREF_は最後に定義されたものに置き換えられます。そう_PREF_init()なりますP1_init()ので、最後の#define文の。次に、次の定義ステートメントはPREFをP2_に等しく設定し、生成しP2_init()ます。
Sanchke Dellowar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.