コンストラクターを介して依存関係を受け取るService
があるが、使用する前にカスタムデータ(コンテキスト)で初期化する必要があるとします。
public interface IService
{
void Initialize(Context context);
void DoSomething();
void DoOtherThing();
}
public class Service : IService
{
private readonly object dependency1;
private readonly object dependency2;
private readonly object dependency3;
public Service(
object dependency1,
object dependency2,
object dependency3)
{
this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
}
public void Initialize(Context context)
{
// Initialize state based on context
// Heavy, long running operation
}
public void DoSomething()
{
// ...
}
public void DoOtherThing()
{
// ...
}
}
public class Context
{
public int Value1;
public string Value2;
public string Value3;
}
今-コンテキストデータは事前にわからないので、依存関係として登録できず、DIを使用してサービスに挿入できません
クライアントの例は次のとおりです。
public class Client
{
private readonly IService service;
public Client(IService service)
{
this.service = service ?? throw new ArgumentNullException(nameof(service));
}
public void OnStartup()
{
service.Initialize(new Context
{
Value1 = 123,
Value2 = "my data",
Value3 = "abcd"
});
}
public void Execute()
{
service.DoSomething();
service.DoOtherThing();
}
}
ご覧のとおり、一時的な結合と初期化メソッドコードの匂いが関係しています。後でservice.Initialize
とservice.DoSomething
を呼び出せるようにするには、最初にservice.DoOtherThing
を呼び出す必要があるためです。
これらの問題を解決できる他のアプローチは何ですか?
動作の追加説明:
クライアントの各インスタンスには、クライアントの特定のコンテキストデータで初期化されたサービスの独自のインスタンスが必要です。そのため、そのコンテキストデータは静的ではなく、事前にわかっていないため、DIによってコンストラクターに挿入することはできません。
初期化の問題に対処するには、いくつかの方法があります。
Client
コンストラクターのドキュメントコメントに初期化する必要があり、サービスが初期化されていない場合はコンストラクターでスローする必要があります。これにより、IService
オブジェクトを提供する人に責任が移ります。ただし、あなたの例では、Client
だけがInitialize()
に渡される値を認識しています。その方法を維持したい場合は、以下をお勧めします。
IServiceFactory
を追加し、Client
コンストラクターに渡します。次に、serviceFactory.createService(new Context(...))
を呼び出して、クライアントで使用できる初期化済みIService
を取得できます。ファクトリーは非常に単純にすることができ、init()メソッドを回避し、代わりにコンストラクターを使用することもできます。
_public interface IServiceFactory
{
IService createService(Context context);
}
public class ServiceFactory : IServiceFactory
{
public Service createService(Context context)
{
return new Service(context);
}
}
_
クライアントでは、OnStartup()
も初期化メソッドです(異なる名前を使用するだけです)。したがって、可能であれば(Context
データを知っている場合)、ファクトリをClient
コンストラクタで直接呼び出す必要があります。それが不可能な場合は、IServiceFactory
を保存してOnStartup()
で呼び出す必要があります。
Service
にClient
によって提供されない依存関係がある場合、それらはServiceFactory
を通じてDIによって提供されます。
_public interface IServiceFactory
{
IService createService(Context context);
}
public class ServiceFactory : IServiceFactory
{
private readonly object dependency1;
private readonly object dependency2;
private readonly object dependency3;
public ServiceFactory(object dependency1, object dependency2, object dependency3)
{
this.dependency1 = dependency1;
this.dependency2 = dependency2;
this.dependency3 = dependency3;
}
public Service createService(Context context)
{
return new Service(context, dependency1, dependency2, dependency3);
}
}
_
ここには2つのオプションがあるようです
例えば。
public InitialisedContext Initialise()
例えば。
public async Task Execute()
{
//lock context
//check context is not initialised
// init if required
//execute code...
}
コンテキストをパラメーターとして渡さないようにする場合は、ファクトリーの注入で問題ありません。この特定の実装だけがコンテキストを必要とし、それをインターフェイスに追加しないとしましょう
しかし、基本的に同じ問題があります。ファクトリがまだ初期化されたコンテキストを持っていない場合はどうでしょうか。
Initialize
メソッドは、実装の詳細であるため、IService
インターフェイスから削除する必要があります。代わりに、Serviceの具象インスタンスを取得し、そのインスタンスでinitializeメソッドを呼び出す別のクラスを定義します。次に、この新しいクラスはIServiceインターフェイスを実装します。
public class ContextDependentService : IService
{
public ContextDependentService(Context context, Service service)
{
this.service = service;
service.Initialize(context);
}
// Methods in the IService interface
}
これにより、ContextDependentService
クラスが初期化される場合を除いて、クライアントコードが初期化手順を無視します。少なくとも、この不安定な初期化手順について知る必要があるアプリケーションの部分を制限します。
インターフェイスをdbコンテキストおよび初期化メソッドに依存しないでください。具体的なクラスコンストラクターで行うことができます。
public interface IService
{
void DoSomething();
void DoOtherThing();
}
public class Service : IService
{
private readonly object dependency1;
private readonly object dependency2;
private readonly object dependency3;
private readonly object context;
public Service(
object dependency1,
object dependency2,
object dependency3,
object context )
{
this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
// context is concrete class details not interfaces.
this.context = context;
// call init here constructor.
this.Initialize(context);
}
protected void Initialize(Context context)
{
// Initialize state based on context
// Heavy, long running operation
}
public void DoSomething()
{
// ...
}
public void DoOtherThing()
{
// ...
}
}
そして、あなたの主な質問の答えはProperty Injectionになります。
public class Service
{
public Service(Context context)
{
this.context = context;
}
private Dependency1 _dependency1;
public Dependency1 Dependency1
{
get
{
if (_dependency1 == null)
_dependency1 = Container.Resolve<Dependency1>();
return _dependency1;
}
}
//...
}
このように、すべての依存関係をProperty Injectionで呼び出すことができます。しかし、それは膨大な数になる可能性があります。その場合は、それらにコンストラクタインジェクションを使用できますが、nullかどうかを確認することで、プロパティによってコンテキストを設定できます。
Misko Heveryが、あなたが直面した事件について非常に役立つブログ投稿を公開しています。 Service
には、両方ともnewableとinjectableが必要ですクラスと このブログ投稿 が役立つ場合があります。