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


11

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によって注入することはできません。

回答:


17

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

  • /software//a/334994/301401で回答されているように、init()メソッドはコード臭です。オブジェクトの初期化はコンストラクターの責任です-結局コンストラクターがあるのはそのためです。
  • 追加指定されたサービスはClientコンストラクターのdocコメントに初期化され、サービスが初期化されていない場合、コンストラクターにスローさせる必要があります。これにより、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、DIによって提供されますServiceFactory

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);
    }
}

1
最後に私が思ったように、最後に... ServiceFactoryでは、ファクトリー自体でコンストラクターDIを使用して、サービスコンストラクターに必要な依存関係を使用しますか、それともサービスロケーターがより適していますか?
斗山

1
@DusanはService Locatorを使用しません。場合Service以外の依存関係があるContextによって提供されていないだろう、Client彼らはにDIを介して提供することができるServiceFactoryに渡されるServiceときcreateServiceに呼び出されます。
ミンダー氏

@Dusan異なるサービスに異なる依存関係を提供する必要がある場合(つまり、このサービスではdependency1_1が必要ですが、次のサービスではdependency1_2が必要です)、このパターンが機能する場合は、ビルダーパターンと呼ばれる類似のパターンを使用できます。Builderを使用すると、必要に応じて、時間の経過とともにオブジェクトを断片的に設定できます。その後、あなたはこれを行うことができます... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);そしてあなたの部分的に設定されたサービスを残し、そして後でService s = partial.context(context).build()
アーロン

1

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

ここには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を呼び出すときにコンテキストが初期化されていない場合は、例外をスローします。SqlConnectionと同様。

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

しかし、ファクトリーがまだ初期化されたコンテキストをまだ持っていない場合はどうなるのでしょうか。


0

インターフェイスを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;
            }
        }

        //...
    }

このようにして、プロパティインジェクションによってすべての依存関係を呼び出すことができます。しかし、それは膨大な数になる可能性があります。その場合、コンストラクタインジェクションを使用できますが、プロパティがnullであるかどうかをチェックすることで、プロパティによってコンテキストを設定できます。


わかりました、しかし、...クライアントの各インスタンスは、異なるコンテキストデータで初期化されたサービスの独自のインスタンスを持つ必要があります。そのコンテキストデータは静的ではなく、事前に知られていないため、コンストラクターでDIによって注入することはできません。次に、クライアントの他の依存関係と一緒にサービスのインスタンスを取得/作成するにはどうすればよいですか?
斗山

コンテキストを設定する前に静的コンストラクターが実行されることはありませんか?コンストラクターで初期化すると例外が発生するリスクがあります
ユアン

(サービス自体を注入するのではなく)指定されたコンテキストデータを使用してサービスを作成および初期化できる注入ファクトリーに傾倒していますが、より良いソリューションがあるかどうかはわかりません。
斗山

@ユアンあなたは正しいです。私はそれに対する解決策を見つけようとします。しかし、その前に、今のところそれを削除します。
エンジニア

0

Misko Heveryには、あなたが直面した事例に関する非常に役立つブログ投稿があります。あなたは両方ともあなたのクラスのために新しく注入可能なものを必要とします、Serviceそしてこのブログ投稿はあなたを助けるかもしれません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.