典型的なアプローチよりも優れているように思われるSpring IOC環境での依存関係管理についての考えがありますが、そこに参照や例が見当たらないため、わかりませんアイデアは非常にシンプルですが、典型的なアプローチと比較して説明します。アイデアが有効かどうか、正式な理論名があるかどうか、またはすでに本について話している本があるかどうかを確認してください。
典型的なSpringアプリケーションでは、クラスA
に依存関係としてクラスB
がある場合、B
のインターフェースをA
に挿入します。たとえば、ProductManager
に依存するクラスProductService
があるとします。
class ProductManager {
private ProductService productService;
public ProductManager(ProductService productService) {
this.productService = productService;
}
public void foo() {
... // some logic
// somewhere in this method
String result = productService.bar1();
... // some logic
}
}
interface ProductService {
String bar1();
void bar2();
}
class ProductServiceImpl implements ProductService {
@Override
public String bar1() { ... }
@Override
public void bar2() { ... }
}
@Configuration
public class ApplicationConfiguration {
@Bean
public ProductManager productManager(ProductService productService) {
return new ProductManager(productService);
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
class ProductManagerTest {
@Autowire
private ProductManager classUnderTest;
@MockBean
private ProductService productService;
public void testFoo() {
when(productSerivce.bar1()).thenReturn("bar");
// do the test
}
}
このアプローチでは、ProductManager
はProductService
に直接依存します。これには、前のセクションで説明するいくつかの問題があります。
ProductManager
とProductService
がお互いを完全に知らないようにしたいと思います。 ProductManager
は、独自に定義されたPort
にのみ依存します。 ProductService#bar1()
を構成クラスのポートに接続します。
interface ProductManagerPort {
String baz();
}
class ProductManager {
private ProductManagerPort port;
public ProductManager(ProductManagerPort port) {
this.port = port;
}
public void foo() {
... // some logic
// somewhere in this method
String result = port.baz();
... // some logic
}
}
// Notice we don't use interface anymore
class ProductService {
public String bar1() { ... }
public void bar2() { ... }
}
@Configuration
public class ApplicationConfiguration {
@Bean
public ProductManager productManager(ProductService productService) {
// in more complex case, we can create an adaptor class instead of using a lambda
return new ProductManager(() -> productService.bar1());
}
}
class ProductManagerTest {
@Test
public void testFoo() {
ProductManager classUnderTest = new ProductManager(this::mockBaz);
// do the test
}
private String mockBaz() { return "bar"; }
}
このアイデアを思いついた理由を説明するために、以下のトピックで2つのアプローチを比較します。
典型的なアプローチでは、ProductManager
はProductService
に強く依存しています。依存関係はインターフェイスを介してバインドされていますが、ProductService
の実装の詳細をProductManager
から隠すのに役立ちますが、ProductManager
はProductService
は一般的に機能します。
各クラスのコードを作成する2人の開発者を割り当てたとします。JohnはProductManager
を書き込み、MarryはProductService
を処理します。両方の人は、自分の作業を行う前にインターフェースに同意する必要があります。 Johnは、ProductService#bar1()
の動作を想定しており、その想定に基づいてメソッドProductManager#foo()
を記述します。マリーは、実装中に契約に影響する新しい何かを見つけたとき、ジョンに通知します。次に、ジョンはコードを新しい仮定をサポートするように作り直す必要があります。
私のアイデアのアプローチでは、ProductManager
とProductService
は完全に独立しています。 ProductManager
は独自のPort
を宣言します。ジョンは、メアリーがPort
の実装中に何らかの問題を見つけるのではないかと心配することなく、ProductService
で自分の想定を維持できます。一方、メアリーは、彼女のデザインの変更がだれでも気になることを恐れる必要はありません。
ProductService
をProductManager
に統合するには、ProductServiceToProductManagerPortAdaptor
を実装するProductManagerPort
を作成します。ジョンとメアリーの仮定に不一致がある場合は、アダプターで調整されます。上記のコード例では、アダプターのラムダを使用して、アダプターの可能な簡略化を例示しています。
典型的なアプローチでは、ProductService
をインターフェイスにして、実装をProductServiceImpl
に配置する必要があります。具象クラスとの依存関係の配線を避けるために、そうすることを余儀なくされています。
My Ideaのアプローチでは、配線はクラス独自のポートを介して行われるため、この制限はありません。具体的なクラスを使用して単純なコードを記述し、真の抽象化が本当に必要なインターフェイスを使用できます。
インターフェース分離の原則(ISP)は、クライアントが使用しないメソッドに依存することを強制されるべきではないと述べています。
コンポーネント間のコンポーネントの配線は、この原則に違反しやすいものです。たとえば、ProductManager
はProductService#bar1()
にのみ依存しますが、同じコンポーネント内にパックされているという理由だけでProductService#bar2()
について知る必要があります。実装の詳細を読んで、ProductService#bar2()
がProductManager
で使用されているかどうかを確認する必要があります。
テストを書くとき、典型的なアプローチでは、モックフレームワークを使用して、不要なモックの未使用のメソッドProductService#bar2()
をモックしないように強制されます。私のアイデアアプローチでは、モックフレームワークを使用せずに、ProductManager
が実際に必要なだけモックする必要があります。
次のプロジェクトでこのパターンを使用することをチームに奨励したいのですが、自分のアイデアをサポートする参照が見つかりません。誰かがすでにこのアイデアを検討しており、何らかの制限のためにそれを拒否していると思います。
アプリケーションでこのパターンを使用した経験がある場合、または標準的なコンセプトがある場合は、共有してください。私は Hexagonal Architecture を調べましたが、それはコーディングパターンよりもシステムアーキテクチャについてのようです。
2つのアプローチの構造は実質的に同じですが、設計プロセスに異なる方法でアプローチしたため、その違いが重要になります。
最初のソリューションでは、インターフェースは「デフォルト」実装のすべての機能を記述します。そのため、ほとんど役に立たない間接参照のように見えます。正しく注記したように、これによりISPに誤って違反し、システムに不要なカップリングを追加することも簡単になります。ただし、そのようなアプローチは、かなり優れたツールサポートを備えています。
2番目のソリューションでは、オブジェクトの使用方法に基づいてインターフェースがガイドされました。それはそれがどうあるべきか、そしてどのようにOOPが最もうまく機能するかです。Javaが最もよく機能するので、アダプタを導入する必要がありますクラス。これらのアダプターは、ラムダまたはメソッド参照を使用しても表示されませんが、単一メソッドのインターフェースでのみ機能します。
使用法に基づくインターフェース定義は、以下の場所で行われます。
Hexagonal/Onion/Clean Architectureでは、依存関係が内側に流れ、インターフェイスがポイントではなく使用ポイントで定義されることにすでに気づいています。実装の。これはアーキテクチャレベルのパターンですが、同じ原則が小規模でも当てはまります。
希望的思考によるプログラミングはこれの小規模な変形です:問題をより小さく、より簡単な部分に分解することにより、問題をトップダウンで解決します。その小さな問題の解決策がすでに存在するかのようにコードを記述しましょう。その後、必要な機能を何らかの方法で実装します。オブジェクトが必要な場合は、最初にインターフェイスを記述して、オブジェクトに必要なものを示し、後でそのオブジェクトを提供する方法を確認できます。この手法はSICPによって普及しました。
[〜#〜] tdd [〜#〜]もこれに関連付けます。 TDDの実践者は、失敗する可能性のある最も単純なテストを繰り返し記述し、合格するように最も単純なコードを記述してから、ソリューションを適切な設計にリファクタリングします。これらの各ステップで、まだ実装されていないインターフェースの観点からソリューションを表現することは、多くの場合、最も簡単なアプローチです。その後、次のTDDの反復時に具体化できます。
一部の言語のオブジェクトシステムは、既存の型を新しいインターフェイスに適合させるのに適しています。行くかさび。
このようなインターフェースまたは設計アプローチは、コードの各部分を小さくシンプルに保つのに役立ち、それらが使用されるコンテキストに関連するインターフェースを維持します。ただし、このようなアプローチでは、実際にはほとんど機能しないオブジェクトが多く生成される傾向があります。これは、特にJavaのような言語で、サポート宣言が通常独自のファイルを取得する場合に、保守性を損なう可能性があります。呼び出す必要のあるすべての単一メソッドに対してインターフェースを宣言して宣言することも可能です。それはもう役に立ちません。
したがって、次のような場合にこのような戦略を適用するのがおそらく最善です。
しかし最も重要なこと:
依存関係をmanageしないでください。
クラスAが電子メールを送信する必要があり、クラスBが電子メールを送信できる場合、クラスAはクラスBに依存し、抽象化やリダイレクトの量は関係をなくします。
あなたが書くとき:
My Ideaアプローチでは、ProductManagerとProductServiceは完全に独立しています。
ごめんなさい。関係はまだそこにあり、磁器の後ろにそれを隠すだけです。
これらのポートとアダプタの構築と保守は「従来の」ソリューションよりも簡単ではないことがわかるので、これらのアダプタを自動的に生成するシステムを考え出そうとします。たぶん、ポートを定義するためにいくつかのドメイン固有の言語を利用することさえありますか?おめでとうございます。インターフェイスを発明しました。最終的には 内部プラットフォーム効果 の主な例になります。
内部プラットフォームの影響は、ソフトウェアアーキテクトが使用するソフトウェア開発プラットフォームのレプリカ(多くの場合、貧弱なレプリカ)になるようにカスタマイズ可能なシステムを作成する傾向です。これは一般に非効率的であり、そのようなシステムはアンチパターンの例と見なされることがよくあります。
オブジェクトを正しく使用していません。
「従来の」設計は、基本的に手順を持ち、他の場所からのデータを処理する一連の「マネージャー」と「サービス」を使用することから始まります。この典型的な設計では、これらの間に実際の(つまり、概念的、つまりビジネス関連の)関係はなく、純粋に技術的なものです。したがって、あなたは使用していません依存関係はすべて価値があるため、(当然のことながら)完全に非表示にする必要があります。
より良い設計se依存関係の一部としてstory。依存関係は、読者にビジネス関連の情報、ソフトウェアの理解を深めるための接続または関係を読者に伝えるためのもう1つのツールです。これらの設計では、これらの依存関係がアプリケーションの一部であるため、これらの依存関係を非表示にする必要はありません。
したがって、問題はあなたがsing依存関係ではないということです、あなたはそれらを強制的に持っています。これは、それらをさらに隠すための技術的な解決策につながります。
依存性注入のmechanismに関する限り、これらはどちらもほとんど同じものです。唯一の違いは、それをどのように進めるかです。つまり、これらは両方とも依存性注入の形式です。しかし、(おそらく)セマンティクスに重要な違いがあります。
一般に、依存関係の注入を依存関係の逆転と組み合わせる必要があります。そこには、2つのコンポーネントがあります。1 そして、他の2つのコンポーネントの両方が依存するある種のabstractionです。 (ところで、抽象化はインターフェースによって表される必要はありません)。この抽象化は、考えられるさまざまな依存関係の実装の一般化を表す必要があります。この抽象化の設計方法に関する一般的な規定はありません。これは、ドメインの理解に依存するためです(アプリケーションの進化に伴って変更される可能性もあります)。しかし、それは比較的安定した概念に基づいている必要があり(変更の頻度はそれに依存するコンポーネントよりも少ないはずです)、実行可能である必要があります(技術的に実現可能でない場合、抽象化は適切ではありません。たとえば、パフォーマンスが許容できないほど低下します) )。
ここで、2つのケースの主な違いは、この抽象化がどのように概念化されているかです。最初のケースでは、bar1()およびbar2()メソッドを使用して、さまざまな依存関係の実装をすべて効果的に表現および実現できることが期待されます。 2番目のケースでは、抽象化は、依存関係がbaz()メソッドを介して効果的に表現および実現できると想定しています。それは重要なビットです。継承またはラッパーを使用する天気は、二次的な懸念事項です。
どちらの抽象化が優れているかは、モデル化しようとしているドメイン(および使用しているツールと言語のコンテキスト)に対して、これら2つのどちらがうまく機能するかに完全に依存します。それはあなたとあなたのチームが考えなければならないことです。ドメインのこの部分(およびコードのこの部分の変更パターン)の理解が深まるまで、その決定を延期することもできます。自分を間違った抽象化に閉じ込めたくないのです。
補足:最初のアプローチでは、少なくとも一見したところ、ProductServiceインターフェースは実際にはProductServiceImplの抽象化を提供しません。どちらも同じメソッドのセットを持っているためです。それが嫌いな理由の一部だと思います。しかし、これを考慮してください。 ProductServiceImplがこれまでのProductService実装の唯一の例である場合、有用な抽象化を思い付くのに十分な情報が不足しているポイントにいる可能性があるため、コードがどのように変化するかを確認するのを待ちたい場合があります(そして、まったく変更されます)、そして他の実装はどのように見えますか?一方、すべての変更が発生することが判明する可能性がありますビハインド ProductServiceImplクラス(つまり、独自の内部詳細または独自の依存関係)、つまり、ソフトウェアが成長するそのクラスの「背後」。その場合、ProductServiceインターフェースを完全に廃棄し、代わりにProductServiceImplクラスを抽象化として使用することもできます。