データベースのISessionContext、ログ用のILogManager、および別のサービスとの通信に使用されるIServiceを必要とするコアクラスがたくさんあります。すべてのコアクラスで使用されるこのクラスに依存性注入を使用したい。
2つの可能な実装があります。 3つのクラスすべてのIAmbientContextを受け入れるか、3つのクラスすべてのクラスに注入するコアクラス。
public interface ISessionContext
{
...
}
public class MySessionContext: ISessionContext
{
...
}
public interface ILogManager
{
}
public class MyLogManager: ILogManager
{
...
}
public interface IService
{
...
}
public class MyService: IService
{
...
}
最初の解決策:
public class AmbientContext
{
private ISessionContext sessionContext;
private ILogManager logManager;
private IService service;
public AmbientContext(ISessionContext sessionContext, ILogManager logManager, IService service)
{
this.sessionContext = sessionContext;
this.logManager = logManager;
this.service = service;
}
}
public class MyCoreClass(AmbientContext ambientContext)
{
...
}
2番目のソリューション(アンビエントコンテキストなし)
public MyCoreClass(ISessionContext sessionContext, ILogManager logManager, IService service)
{
...
}
この場合、Wichが最良のソリューションですか?
「ベスト」はここでは主観的すぎます。そのような決定と共通して、それは何かを達成するための2つの等しく有効な方法の間のトレードオフです。
AmbientContext
を作成し、それを多くのクラスに注入すると、潜在的に各クラスに必要以上の情報を提供することになります(たとえば、クラスFoo
はISessionContext
のみを使用できますが、 ILogManager
とISession
についても話されています)。
それぞれをパラメーターを介して渡す場合は、各クラスに、知っておく必要があることだけを伝えます。ただし、パラメーターの数は急速に増加する可能性があり、非常に多くの非常に繰り返しの多いパラメーターを持つコンストラクターやメソッドが多すぎて、コンテキストクラスによって簡略化できる場合があります。
したがって、2つのバランスを取り、状況に応じて適切なものを選択する必要があります。クラスが1つとパラメーターが3つしかない場合は、個人的にはAmbientContext
を気にしません。私にとって、転換点はおそらく4つのパラメーターです。しかし、それは純粋な意見です。あなたの転換点はおそらく私のものとは異なるので、あなたにとって正しいと思うものに行きます。
質問の用語は、サンプルコードと実際には一致しません。 Ambient Context
は、依存関係のインターフェースを受け入れるためにすべてのクラスを汚染することなく、モジュールの任意のクラスから依存関係をできるだけ簡単に取得するために使用されるパターンですが、制御の反転の概念は維持されます。このような依存関係は、通常、ロギング、セキュリティ、セッション管理、トランザクション、キャッシング、監査に特化しているため、そのアプリケーションの横断的な懸念に対応できます。 ILogging
、ISecurity
、ITimeProvider
をコンストラクタに追加するのはどういうわけか面倒です。ほとんどの場合、すべてのクラスが同時にすべてを必要とするわけではないので、私はあなたの必要性を理解しています。
ISession
インスタンスの寿命がILogger
のインスタンスと異なる場合はどうなりますか?たぶん、ISessionインスタンスはすべてのリクエストとILoggerで一度作成されるべきです。したがって、コンテナ自体ではない1つのオブジェクトによってこれらの依存関係をすべて管理することは、このスレッドで説明されているライフタイム管理とローカリゼーションなどの問題があるため、正しい選択とは言えません。
問題のIAmbientContext
は、すべてのコンストラクターを汚染しないという問題を解決しません。コンストラクタのシグネチャで使用する必要がありますが、今回は1回だけです。
したがって、最も簡単な方法は、クロスセクションの依存関係を処理するためにコンストラクター注入または他の注入メカニズムを使用せず、静的呼び出しを使用することです。このパターンは、フレームワーク自体によって実装されることがよくあります。 IPrincipal
インターフェースの実装を返す静的プロパティであるThread.CurrentPrincipalを確認します。また、設定可能であるため、必要に応じて実装を変更することができます。したがって、それには連動していません。
MyCore
は次のようになります
public class MyCoreClass
{
public void BusinessFeature(string data)
{
LoggerContext.Current.Log(data);
_repository.SaveProcessedData();
SessionContext.Current.SetData(data);
...etc
}
}
このパターンと可能な実装は、Mark Seemannがこの article で詳しく説明しています。使用するIoCコンテナー自体に依存する実装がある場合があります。
上記と同じ理由で、AmbientContext.Current.Logger
、AmbientContext.Current.Session
を避けたい場合。
ただし、この問題を解決する他のオプションがあります。コンテナにこの機能またはAOPがある場合は、デコレータ、動的インターセプトを使用します。アンビエントコンテキストは、そのクライアントが依存関係を隠蔽するという事実のため、最後の手段となるはずです。インターフェースがDateTime.Now
やConfigurationManager.AppSettings
のような静的な依存関係を使用するという衝動を本当に模倣している場合は、引き続きアンビエントコンテキストを使用しますが、このニーズが頻繁に発生します。しかし、結局のところ、コンストラクタの注入は、これらのユビキタスな依存関係を得るためにそれほど悪い考えではないかもしれません。
AmbientContext
は避けます。
まず、クラスがAmbientContext
に依存している場合は、実際にそれが何を行うかわかりません。ネストされた依存関係のどれを使用するかを理解するには、その依存関係の使用を調べる必要があります。また、依存関係の1つが実際には複数のネストされた依存関係を表す可能性があるため、依存関係の数を調べて、クラスが過度に実行しているかどうかを確認することもできません。
次に、複数のコンストラクター依存関係を回避するためにそれを使用している場合、このアプローチは、他の開発者(自分を含む)がそのアンビエントコンテキストクラスに新しいメンバーを追加することを奨励します。次に、最初の問題が悪化します。
第3に、AmbientContext
への依存関係のモックは、すべてのメンバーをモックするか、必要なものだけをモックするかを判断し、それらのモックを返すモック(またはtest doubles。)これにより、ユニットテストの書き込み、読み取り、維持が難しくなります。
第4に、まとまりに欠け、単一責任原則に違反します。それが「AmbientContext」のような名前を持っているのはそのためです。なぜなら、それは多くの無関係なことをし、それが何をするかに従ってそれに名前を付ける方法がないからです。
また、インターフェースメンバーを必要としないクラスにインターフェースメンバーを導入することにより、インターフェース分離の原則に違反する可能性があります。
2番目(インターフェースラッパーなし)
中間クラスでカプセル化する必要があるさまざまなサービス間に何らかの相互作用がない限り、コードを複雑にし、「インターフェイスのインターフェイス」を導入するときの柔軟性を制限するだけです