web-dev-qa-db-ja.com

依存性注入を使用して一時的な結合を回避する方法は?

コンストラクターを介して依存関係を受け取る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.Initializeservice.DoSomethingを呼び出せるようにするには、最初にservice.DoOtherThingを呼び出す必要があるためです。

これらの問題を解決できる他のアプローチは何ですか?

動作の追加説明:

クライアントの各インスタンスには、クライアントの特定のコンテキストデータで初期化されたサービスの独自のインスタンスが必要です。そのため、そのコンテキストデータは静的ではなく、事前にわかっていないため、DIによってコンストラクターに挿入することはできません。

11
Dusan

初期化の問題に対処するには、いくつかの方法があります。

  • https://softwareengineering.stackexchange.com/a/334994/301401 で回答されているように、init()メソッドはコードのにおいです。オブジェクトの初期化はコンストラクターの責任です-結局、コンストラクターがあるのはそのためです。
  • 追加指定されたサービスは、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()で呼び出す必要があります。

ServiceClientによって提供されない依存関係がある場合、それらは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);
    }
}
_
18
pschill

ここには2つのオプションがあるようです

  1. 初期化コードをコンテキストに移動し、初期化されたコンテキストを挿入します

例えば。

public InitialisedContext Initialise()
  1. まだ実行されていない場合は、Executeの最初の呼び出しでInitializeを呼び出します。

例えば。

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Executeを呼び出したときにContextが初期化されていない場合は、例外をスローするだけです。 SqlConnectionと同様です。

コンテキストをパラメーターとして渡さないようにする場合は、ファクトリーの注入で問題ありません。この特定の実装だけがコンテキストを必要とし、それをインターフェイスに追加しないとしましょう

しかし、基本的に同じ問題があります。ファクトリがまだ初期化されたコンテキストを持っていない場合はどうでしょうか。

1
Ewan

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クラスが初期化される場合を除いて、クライアントコードが初期化手順を無視します。少なくとも、この不安定な初期化手順について知る必要があるアプリケーションの部分を制限します。

1
Greg Burghardt

インターフェイスを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かどうかを確認することで、プロパティによってコンテキストを設定できます。

0
Engineert

Misko Heveryが、あなたが直面した事件について非常に役立つブログ投稿を公開しています。 Serviceには、両方ともnewableinjectableが必要ですクラスと このブログ投稿 が役立つ場合があります。

0
Bold P.