次の図を作成して、一般的に教えられているような関心事の典型的な分離を示します-
ここでClassA
はClassB
を介してISomeInterface
を間接的に使用します。もちろん、ClassB
が存在することを知らないことを確認します。インターフェイス内のメソッドのみがClassB
実装します。この懸念の分離に関するすべての情報はここで終わります。classA
がClassB
に結合せずに実際にインターフェイスを使用する方法を見つけることはできません。
もちろん、インターフェースだけをインスタンス化することはできません。インターフェース自体には実装や機能はありません。では、ClassA
はどのようにインターフェイスを使用するのでしょうか。
現在頭に浮かぶ方法は2つだけです-
1)ClassA
は次のことを行います。
ISomeInterface obj = new ClassB();
ここでは、ClassB
のメンバーを直接呼び出すのではなく、インターフェイスメンバーのみを呼び出すようにします。ただし、問題はClassB
がインスタンス化によってClassA
にリークしていることです。
2)ClassA
はインターフェースのみに依存し、次のコンストラクターを使用してclassB
オブジェクトを他の場所に渡す責任を委任します。
class ClassA {
ISomeInterface obj;
ClassA(ISomeInterface obj) {
this.obj = obj;
}
}
もちろん、これはClassA
をClassB
から完全に分離しますが、someone、somewhere、ISomeInterface(ClassB
など)の実装をインスタンス化し、それをオブジェクトとしてClassA
に渡す必要があります。
私が見つけることができるすべてのチュートリアルと説明は、この最後の重要な詳細を省きます。この最後の重要なことを行う責任があるのは誰ですか?それはどこかで発生しなければなりません。そして、これがClassA
とClassB
の両方に結合しているのでしょうか?
ClassA
はインターフェイスのみに依存し、classB
オブジェクトを他の場所に渡す責任を委任します
これがアイデアです。インターフェイスを使用してClassA
をClassB
から分離している場合ISomeInterface
...
(
ClassA
)がClassB
を知らないようにする
次に、ClassA
がClassB
をインスタンス化しないようにします。代わりに、インターフェイスによって型指定されたオブジェクトを受け取る必要があります(たとえば、コンストラクターで受け取ることができます)。
これはカプセル化にどのように役立ちますか?カプセル化は無料ではありません。ただし、ClassA
はそのインターフェースを介してのみClassB
を使用するため、その使用に十分なインターフェースを定義する必要があります。理想的には、使用されていないインターフェイスの部分も削除することをお勧めします。次に、ClassB
が何を公開する必要があるかが正確にわかります。 優れたインターフェースを設計する必要があります。
ただし、誰かがどこかでISomeInterface(
ClassB
など)の実装をインスタンス化し、それをオブジェクトとしてClassA
に渡す必要があるため、他の場所で「ドルを渡す」だけです。
はい。
それはどこかで起きなければならない。そして、これが
ClassA
とClassB
の両方に結合しているのでしょうか?
はい。
カップリングは避けられません。あなたはそれをゼロにすることを試みていません。あなたはそれを低く保とうとしている。それはあなたが払わなければならないコストと考えてください...カップリングが多すぎると、メンテナンスが困難になります。ただし、メンテナンスがゼロエフォートタスクになることはありません。
私は、コンストラクターに依存性注入を行うことを前提に続けます
ClassA
オブジェクトを取得する方法の単一の真の情報源が必要です。つまり、毎回ClassB
のオブジェクトを渡してコンストラクタを呼び出す必要はありません。それをカプセル化して再利用したいとします。
将来的にClassB
をClassC
に置き換える必要がある場合、それを変更する場所が1つになります。 ClassB
またはClassC
を選択するロジックがある場合、そのロジックが配置される場所は1つだけです。インスタンスをプールする必要がある場合は、そこでもプールできます。
したがって、ClassA
の正しい実装にバインドされたISomeInterface
のオブジェクトを提供する責任を負うコードが作成されます。 そしてそれがそのコードの唯一の責任です。そのコードの一部をファクトリーと呼ぶことができます。
注:各クラスのファクトリを作成することは目標になりません。必要なものだけが必要です。
何をしているのかによっては、ClassA
をClassB
で作成したい場合と、ClassA
をClassC
で作成したい場合があります。 。実際、これは外部入力に依存する可能性があります。
別の例として、キャラクターがいるビデオゲームがあるとします。ローカルユーザーによって制御される場合もあれば、ネットワークを介して接続されたプレーヤーによって制御される場合もあれば、AIによって制御される場合もあります。この場合、3つの有効な実装があり、それらはすべて共存できます。
別の例としては、ローカルストレージまたは別のマシンで引き続きプレイできるクラウドサービスに進行状況を保存できるビデオゲームがある可能性があります。つまり、進行状況を保存するために2つの実装があり、どちらを使用するかはユーザー入力に依存します。
はい、複雑なパターンを適用してファクトリを処理できます。実際、ファクトリーはコードの断片であり、あいまいにしています。ただし、これを指摘しておきます。ライブラリを作成している場合...ファクトリパターン専用のライブラリを作成することが目的でない場合は、アプリケーションの開発者に何も提供せずに、使用するファクトリパターンを決定させます。
ファクトリー・コードを配置する正確な場所は、使用しているアプリケーションの種類によって異なります。シンプルなコンソールおよびデスクトッププロジェクトの場合、main
に依存関係を注入するすべてを実行できます。より複雑なもの(ネットワークサーバーなど)を実行している場合は、ファクトリソリューションが必要になることがあります。通常、どのコードが入力を処理するかを決定した後でそれを使用します。依存関係の注入が行われた後、コードは使用したファクトリソリューションを知る必要はありません。
ファクトリソリューションとは、サービスロケータ、IoCコンテナ…ファクトリメソッド、抽象ファクトリ…などすべてを意味します。どんなパターンを選んでも。 それを行うライブラリを選択した場合、どう思いますか?依存関係です! Muahahaha。だから、ええ、依存関係の注入が行われた後は、コードに認識させたくないでしょう。
抽象化することは良い™です。実際、それは単一の責任の原則によって自然に起こります。 ちなみに、あなたは懸念の分離ではなく、「単一責任の原則」を意味していると思います。
レイヤーがインターフェイスを介して下位レイヤーとのみ対話できると彼らが言ったとき、それらは抽象インターフェイスタイプではなく、APIのようなインターフェイスだと思います。
ただし、すべてのレイヤートークを外部システムの抽象化の概念と組み合わせることができます…
ソフトウェアエンジニアは、プロジェクトの範囲について経営幹部の決定を下す必要があります。これにより、システムの一部と外部の要素が決定されます。
線を引きます。建築ライン。行の左側で、外部システムを処理するコードを記述します。これには、外部システムを抽象化するアダプターの作成が含まれます。そのコードは交換可能である必要があり、必要に応じて将来簡単に交換できるようにする必要があります。
注:すべての依存関係をアダプターでラップすることはお勧めしません。外部システムのみ。したがって、いいえ、使用するすべてのライブラリ用のアダプタを作成する必要があるとも言っています。また、ランタイムから分離するというばかげた考えもありません。それらには変化する独自の理由があることは事実ですが、あなたは単一の責任の原則に従い、それで十分です。これは、IOと同様に外部システムに関するものです。 また、外部とは何かを決定することを言っています。
したがって、右側のコードが左側のコードに依存しないようにする必要があります(外部の理由により変更が必要になる可能性があります)。ただし、左側のコードは右側のコード(ユーザーの制御下にある)に依存する場合があります。
したがって、(左側にある)アダプターにインターフェース(右側に存在する)を実装させ、アーキテクチャー行の右側にある残りのコードはインターフェースのみに依存します…
ただし、誰かがアダプタをインスタンス化する必要があります。そのコードはどこに行きますか?建築ラインの左側にあります。なぜなら、右側のコードは左側のコードに依存することができないからです。したがって、アダプターをインスタンス化するコード(左側)は左側に配置する必要があります。
驚いたことに、オペレーティングシステムは外部システムです。 main
は、オペレーティングシステムからのイベント(つまり、プログラムの開始)を処理するメソッドです。また、オペレーティングシステムは外部であるため、main
は行の左側にあり、アダプターをインスタンス化して、右側のコードを呼び出すことができます。まあ、それは実際には工場と呼ばれるでしょう。
同様に、フォームとイベントハンドラーがある場合、イベントハンドラーは左側にあります。または、Webサーバーがある場合、ルーターは左側にあります。そのコード(main
、イベントハンドラー、ルーター)でファクトリを呼び出し、依存関係を挿入して、実行フローを右側に移動させます。
裏側。右側のコードでは、外部システムを呼び出すことができます。これはインターフェースを介して可能であることを覚えておいてください。これを行う別の方法があることを指摘したいと思います。実行の流れは左側から始まるため、多くの場合、右側から戻ることができ、左側はより多くのコードを実行できます。
ここで何を説明していますか?建築ラインのアイデアは、クリーンコードから来ています。
私は「関数型コア、命令型シェル」のアイデアからインスピレーションを得ています。純粋な関数は純粋な関数しか呼び出せません。ただし、不純な関数は、純粋な関数と不純な関数を呼び出すことができます(右に純粋、左に不純)。
ボーナスおしゃべり:async
コードは伝播する傾向があることに気づくかもしれません。 async
メソッドは同期メソッドとawait
他のasync
メソッドを呼び出すことができるためです。ただし、同期はasync
メソッドを待つことができません。ただし、建築線を使用して左側に配置できます(右側に同期、左側にasync
)。
上位層と下位層の話ではなく、なぜ左と右と言うのですか?データベースとユーザーインターフェイスの両方を外部システムとして扱うからです。ただし、クラシック層アーキテクチャでは、データベースは下位層であり、UIは上位層であると言われます。
また、内部レイヤ間の通信は、同じ標準に準拠する必要はありません。具体的なファサードを提供するか、「上位」のレイヤーが下位のタイプをインスタンス化できるようにすることは、システム内で許容されます。 ただし、線をさらに描画したい場合もあります。
あなたの混乱はおそらく、背後に実際の使用シナリオがなく、無意味なクラス名とインターフェース名に焦点を合わせていることが原因です。具体例を作ってみましょう(私はC#を好みますが、Javaのような他の言語でもそれほど違いはありません)。
.NETフレームワークのIComparable
インターフェースは次のようになります(簡略化されています)。
interface IComparable
{
int CompareTo (object obj);
}
インターフェイスはSystem
アセンブリに属しています。
ArrayList
(System.Collections
内の.Netフレームワークの一部でもある)のようなクラスがあり、このインターフェースを実装するオブジェクト、特にSort
にいくつかの機能を提供します。 ArrayList
は、ClassA
に対応します。
したがって、明らかに、これらのクラスは何年も前に記述されていたため、今日新しく作成されたクラスについては何も知りません。
ここで、Balloon
を実装するクラスIComparable
を使用して新しいプログラムを記述します(たとえば、バルーンをそのボリュームで比較することにより)。これはあなたの質問のClassB
です。だから
ArrayList
の1つのインスタンスとBalloon
オブジェクトの複数のインスタンスを作成するプログラム
Balloon
オブジェクトをArrayList
に入れます
また、Sort
メソッドを呼び出します。これは機能しますが、Balloon
クラスを以前に見たことはありません。
したがって、はい、クラスBの派生とインスタンス化の「負担」は、他の誰かに(インターフェースと既存のクラスAを利用するコードに)渡され、これは完全に理にかなっています。
最後の注記:インターフェイス、戦略パターン、テンプレートメソッドパターン、またはオープンクローズドプリンシプルに関する多くの質問は、1つのライブラリがサードパーティベンダーによって提供され、そのlibのユーザーは簡単に変更できます。
これはひどく考えすぎです。例として、Goがそれをどのように行うかを調べます。そのアプローチはすべての不必要な残骸を取り除きます。長年に渡り、このアイデアの周りには非常に多くのものが積み上げられており、これらのアイデアのほとんどはトーチングすることができます。
インターフェイスは、存在することが保証されているメソッドに関する約束に過ぎません。コンパイラーが存在しないメソッド呼び出しを拒否できるようにします。一部の言語では、クラスにインターフェースを実装することを宣言する必要があります。 Goなどの他の言語では、インターフェースメソッドを持つHAS構造体を渡す場合、そのインターフェースタイプに構造体を割り当てることは有効です。この選択は、1つまたは2つのメソッドインターフェイスを宣言し、変更したくないレガシー構造体を渡すことができることを意味します。
Goでは、構造だけです。彼らはそれらに接続されたメソッドを持つことができます。一部のメンバーをプライベートに、他のメンバーをパブリックにすることができます。しかし、それは一種の不必要なことであり、何かを取得して修正する必要がある場合には、逆効果になることがあります。すべてのパブリックメンバーで構造体を使用するだけの場合、実際に必要な最小数のメソッドを持つインターフェイスに構造体を割り当てるだけで、構造体(したがってすべての構造体メンバー)を非表示にできます。
データ構造自体をカプセル化しようとすることは、これらの構造がディスク上またはネットワーク上のデータ構造と一致する必要がある場合、非常に非生産的です。
コードを「カプセル化」したままにしたい場合は、ダースのメソッドを定義する可能性のある構造体を渡していることを認識してください。したがって、これら2つのメソッドを使用してインターフェースを作成するだけです。これで、参照にバインドされた2つのメソッドにのみ結合されます。
Java IOクラスとインターフェースのようなものを考えると、それらには多くの場合、あまりにも多くのメソッドを持つインターフェースがあり、必要なために不便です。クラスを変更してインターフェースを宣言します。サードパーティのライブラリライブラリが一緒になって独自のインターフェースを作成し、そこに構造体インスタンスを割り当てる方がはるかに優れています。