7

instanceCreator コンテキスト (aka ) を使用して単一の登録項目を登録できますFunc<T>が、RegisterAll では同じ許可が得られないようです。

TL;DR - 受け入れられた回答を見つけて、更新 2 を確認してください (または、この質問の更新 3 までスキップしてください)。

これは私がやりたいことです:

container.RegisterAll<IFileWatcher>(
    new List<Func<IFileWatcher>>
    {
        () => new FileWatcher(
            @".\Triggers\TriggerWatch\SomeTrigger.txt",
            container.GetInstance<IFileSystem>()),
        () => new FileWatcher(
            @".\Triggers\TriggerWatch\SomeOtherTrigger.txt",
            container.GetInstance<IFileSystem>())
    });

以前の Stack Overflow answer for multiple registrationsに基づいて拡張機能を追加しようとしましたが、最後のものが勝つようです:

public static class SimpleInjectorExtensions
{
    public static void RegisterAll<TService>(this Container container,
        IEnumerable<Func<TService>> instanceCreators) 
        where TService : class
    {           
        foreach (var instanceCreator in instanceCreators)
        {
            container.RegisterSingle(typeof(TService),instanceCreator);
        }

        container.RegisterAll<TService>(typeof (TService));
    }
}

そもそもなぜ存在する必要があるのか​​ にも興味がありRegisterAllます。これは、私が使用した 5 つの依存性注入コンテナーのうち、違いを生む最初の依存性注入コンテナーです。Resolve<IEnumerable<TService>>他のものは、サービスに対して複数のタイプを登録し、 (autofac) またはGetAllInstances<TService>(SimpleInjector と Ninject の両方)を呼び出してそれらをすべてロードすることを可能にします。

アップデート

より明確にするために、個々のアイテムを処理するコンポジットに渡すことができるアイテムのリストを作成しようとしています。スケジュール、トリガー、およびイベント (Rx) に基づいて実行されるようにすべて登録されるタスクのグループに分類されるため、上記と同じ問題が発生します。レジスタをすべて削除して、他のものをいくつか取り除くには:

container.Register<ITask>(() => new FileWatchTask(
    container.GetInstance<IFileSystem>(),
    container.GetInstance<IMessageSubscriptionManagerService>(),
    configuration,
    container.GetAllInstances<IFileWatcher>()));

以前に登録されたファイル ウォッチャーのすべてのインスタンスを取得していることがわかります。

私が知る必要があるのは、この問題の簡単な回避策と、いつ実装されるか (実装されない場合は、実装されない理由) です。また、Simple Injector の設計の現在の制限を考慮すると、これが不可能であることも認めます。私が受け入れられないのは、ツールの制限を満たすためにアーキテクチャを変更および適応させる必要があるということです。

更新 2

OCP (Open Closed Principle、別名 SOLID の O) について話しましょう。また、SimpleInjector がこの特定の原則を場合によって破る方法について私が感じている印象について話しましょう。

Open Closed Principle とは、拡張に対してオープンであり、変更に対してクローズであるということです。これが意味することは、ソース コードを変更せずにエンティティの動作を変更できるということです。

ここで、ここに関連する例に移りましょう。

var tasks = container.GetAllInstances<ITask>();
foreach (var task in tasks.OrEmptyListIfNull())
{
  //registers the task with the scheduler, Rx Event Messaging, or another trigger of some sort  
  task.Initialize();
}

それがどれほどきれいであるかに注意してください。ただし、これを行うには、インターフェイスのすべてのインスタンスを登録できる必要があります。

container.RegisterAll<ITask>( 
  new List<Func<ITask>>{
    () => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()),
    () => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()),
    () => new DefaultImportFilesTask(container.GetInstance<IFileSystem>())
  }
);

右?したがって、ここでの教訓は、これは優れており、OCP に適合しているということです。登録されているアイテムを追加または削除するだけで、タスク ランナーの動作を変更できます。拡張用に開き、変更用に閉じています。

次に、以下の回答で提案されている方法に焦点を当ててみましょう (最終的にこの質問に回答する 2 回目の更新の前)。これは、著者がより良いデザインであるという印象を与えています。

メンテナからの回答が言及している登録の優れた設計から始めましょう。私が得ている視点は、SimpleInjector で動作するように ITask をより柔軟にするために、コードを犠牲にしなければならないということです。

container.Register<ITask<SomeGeneric1>(() => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()));
container.Register<ITask<SomeGeneric2>(() => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()));
container.Register<ITask<SomeGeneric3>(() => new DefaultImportFilesTask(container.GetInstance<IFileSystem>()));

それでは、それによってデザインがどのように変化するかを見てみましょう。

var task1 = container.GetInstances<ITask<SomeGeneric1>();
task1.Initialize();
var task2 = container.GetInstances<ITask<SomeGeneric2>();
task2.Initialize();
var task3 = container.GetInstances<ITask<SomeGeneric3>();
task3.Initialize();

ああ。コンテナー登録にアイテムを追加または削除するたびに、コードの別のセクションも更新する必要があることがわかります。1 回の変更で 2 か所の変更、複数の設計上の問題を解決しています。

なぜコンテナにこれを要求しているのかと言うかもしれません。これはスタートアップ領域にありますが、そうでない場合は調べてみましょう。

したがって、コンストラクター注入を使用して、これがなぜ悪いのかを説明します。まず、私の例をコンストラクション インジェクションとして見てみましょう。

public class SomeClass {
    public SomeClass(IEnumerable<ITask> tasks){}
}

素敵できれい。

さて、受け入れられた回答の見解についての私の理解に戻りましょう(更新2の前に):

public class SomeClass {
    public SomeClass(ITask<Generic1> task1,
                     ITask<Generic2> task2,
                     ITask<Generic3> task3
                     ) {}
}

ああ。コードの複数の領域を編集しなければならないたびに、この設計がどれほど貧弱であるかについては始めません。

ここでの教訓は何ですか?私は世界で最も賢い男ではありません。私は複数のフレームワークを維持しています (または維持しようとしています :))、他の人よりも多くのことを知っているふりをしたり、他の人よりもよく知っているふりをしたりしません。私のデザイン感覚がゆがんでいるのかもしれませんし、私がまだ考えもしていない未知の方法で他の人を制限しているのかもしれません. 著者がデザインに関するアドバイスをするのは善意であると確信していますが、場合によっては、特に私たちが何をしているのかを知っている私たちにとっては、迷惑な (そして少し見下すような) ものに遭遇するかもしれません.

アップデート 3

そのため、質問はメンテナーからの更新 2 で回答されました。私は RegisterAll を使用しようとしていました。これは、私が使用できるとは思いもよらなかったためですRegister<IEnumerable<T>>(残念ながら、ドキュメントではこれが指摘されていませんでした)。今では完全に当​​たり前のように思えますが、他の IoC フレームワークから移行しようとしている人は荷物を抱えており、この驚くべきデザインの簡素化を見逃す可能性があります。他に 4 つの DI コンテナーを持っていたので、見逃してしまいました。うまくいけば、彼はそれをドキュメントに追加するか、もう少しうまく呼び出すでしょう.

4

1 に答える 1

17

最初の例 (を使用List<Func<IFileWatcher>>) から、一時的なファイルウォッチャーのコレクションを登録したいことがわかりました。つまり、リストを反復するたびに、新しいファイル ウォッチャー インスタンスを作成する必要があります。もちろん、これはリストを 2 つの (シングルトンの) ファイルウォッチャー (常に返される同じインスタンス) に登録することとは大きく異なります。ただし、拡張メソッドではそれらをシングルトンとして登録しているように見えるため、質問にはあいまいさがあります。私の答えの残りについては、一時的な動作が必要であると仮定します。

が作成される一般的な使用例はRegisterAll、共通インターフェースの実装のリストを登録することです。たとえば、イベントがIEventHandler<CustomerMoved>発生したときにすべてをトリガーする必要がある複数の実装を持つアプリケーションです。CustomerMovedその場合、RegisterAllメソッドにインスタンスのリストをSystem.Type指定すると、コンテナーはそれらの実装の配線を完全に制御します。コンテナーが作成を制御しているため、コレクションは「コンテナー制御」と呼ばれます。

ただし、作成をコンテナに戻すだけです。RegisterAllつまり、デフォルトでは、リストによって一時的なインスタンスが作成されます (未登録の具象型は一時的なものとして解決されるため)。これは一見ぎこちなく思えますが、選択したライフスタイルで各アイテムを明示的に登録できるため、さまざまなライフスタイルの要素を含むリストを登録できます。RegisterAllまた、抽象化 (たとえば)を指定することもできます。これtypeof(IService)は、要求がコンテナーに戻されるため、同様に機能します。

ただし、ユースケースは異なります。まったく同じタイプの要素のリストを登録する必要がありますが、それぞれが異なる構成値を持っています。さらに難しいことに、シングルトンではなくトランジェントとして登録したいようです。RegisterAll型のリストを渡さないがIEnumerable<TService>、コンテナはそれらの型を作成して自動配線しないため、これを「コンテナ非制御」コレクションと呼びます。

簡単に言うと、これをどのように登録しますか? これを行うには複数の方法がありますが、個人的には次の方法が気に入っています。

string[] triggers = new[]
{
    @".\Triggers\TriggerWatch\SomeTrigger.txt",
    @".\Triggers\TriggerWatch\SomeOtherTrigger.txt"
};

container.RegisterAll<IFileWatcher>(
    from trigger in triggers
    select new FileWatcher(trigger,
        container.GetInstance<IFileSystem>())
);

IEnumerable<T>ここでは、メソッドを使用してLINQ クエリ (単なる ) を登録しますRegisterAll。誰かが を解決するたびIEnumerable<IFileWatcher>に同じクエリが返されますが、そのクエリの選択には が含まれているためnew FileWatcher、反復時に新しいインスタンスが常に返されます。この効果は、次のテストを使用して確認できます。

var watchers = container.GetAllInstances<IFileWatcher>();
var first1 = watchers.First();
var first2 = watchers.First();
Assert.AreNotEqual(first1, first2, "Should be different instances");
Assert.AreEqual(first1.Trigger, first2.Trigger);

このテストが示すように、コレクションを 1 回解決しますが、それを反復する (コレクションを反復する) たび.First()に、新しいインスタンスが作成されますが、両方のインスタンスの@".\Triggers\TriggerWatch\SomeTrigger.txt"値は同じです。

ご覧のとおり、これを効果的に行うことを妨げる制限はありません。ただし、別の方法で考える必要があるかもしれません。

また、そもそもなぜ RegisterAll が存在する必要があるのか​​にも興味があります。

これは非常に明確な設計上の決定です。他のほとんどのコンテナーでは、同じタイプの一連の登録を行うことができ、コレクションを要求すると、すべての登録が返されることは正しいです。これに関する問題は、誤ってタイプを再度登録することが容易であり、これを防止したかったことです。

さらに、すべてのコンテナーには、コレクションを要求する代わりに単一のインスタンスを要求するときに登録が返されるという異なる動作があります。最初の登録を返すものもあれば、最後の登録を返すものもあります。このあいまいさも避けたいと思いました。

最後になりましたが、同じタイプのアイテムのコレクションを登録することは、通常は例外であることに注意してください。私の経験では、開発者が同じ抽象化の複数のタイプを登録したい場合、90% の確率で、設計にあいまいさが生じます。コレクションの登録を明示的にすることで、これを目立たせたいと考えました。

私が受け入れられないのは、何らかのツールの制限を満たすためにアーキテクチャを変更および適応させる必要があるということです。

私はこれに同意します。ツールではなく、アーキテクチャが主導的であるべきです。それに応じてツールを選択する必要があります。

ただし、Simple Injector には多くの制限があり、それらの制限のほとんどは、ユーザーにクリーンなデザインを持たせるように意図的に選択されていることに注意してください。たとえば、コード内でSOLID 原則の 1 つに違反するたびに、問題が発生します。コードを柔軟に保ち、テストを読みやすくし、コンポジション ルートを保守しやすくするのに問題が生じるでしょう。実際、これはすべての DI コンテナーに当てはまりますが、Simple Injector の場合はさらに当てはまります。これは意図的なものであり、開発者が SOLID 原則の適用に関心がなく、特定の状況で機能する DI コンテナーが必要な場合、Simple Injector はその仕事に最適なツールではない可能性があります。たとえば、Simple Injector をレガシー コード ベースに適用するのは困難な場合があります。

これにより、Simple Injector の設計についてある程度の見通しが得られることを願っています。

アップデート

代わりにシングルトンが必要な場合、これはさらに簡単です。次のように登録できます。

var fs = new RealFileSystem();

container.RegisterSingle<IFileSystem>(fs);

container.RegisterAll<IFileWatcher>(
    new FileWatcher(@".\Triggers\TriggerWatch\SomeTrigger.txt", fs),
    new FileWatcher(@".\Triggers\TriggerWatch\SomeOtherTrigger.txt", fs)
);

更新 2

RegisterAll<T>(Func<T>)コレクションを遅延作成するためのサポートを明示的に要求しました。RegisterSingle<IEnumerable<T>>(Func<IEnumerable<T>>)実際、ここでわかるように、を使用するだけで、これは既にサポートされています。

container.RegisterSingle<IEnumerable<IFileWatcher>>(() =>
{
    return
        from 
    var list = new List<IFileWatcher>
    {
        new FileWatcher(@".\Triggers\TriggerWatch\SomeTrigger.txt", container.GetInstance<IFileSystem>()),
        new FileWatcher(@".\Triggers\TriggerWatch\SomeOtherTrigger.txt", container.GetInstance<IFileSystem>())        
    };

    return list.AsReadOnly();
});

実際、RegisterAll<T>(IEnumerable<T>)は最終的に を呼び出す便利なオーバーロードですRegisterSingle<IEnumerable<T>>(collection)

読み取り専用リストを明示的に返すことに注意してください。これはオプションですが、アプリケーション コードによってコレクションが変更されるのを防ぐ追加の安全メカニズムです。コレクションを使用RegisterAll<T>すると、読み取り専用イテレータに自動的にラップされます。

using の唯一のRegisterSingle<IEnumerable<T>>問題は、 を呼び出したときにコンテナーがコレクションを反復処理しないことですcontainer.Verify()。ただし、コレクションの要素が初期化に失敗すると、への呼び出しGetInstance<IEnumerable<IFileWatcher>>も失敗し、それにより への呼び出しも失敗するため、これは問題にはなりませんVerify()

更新 3

あなたのデザインが間違っているという印象を与えてしまったら、申し訳ありません。私はこれを知る方法がありません。一部の機能が欠落している理由を明示的に尋ねられたので、この背後にある理論的根拠を説明するために最善を尽くしました. しかし、それは私が知る方法がないので、あなたのデザインが悪いと思うという意味ではありません.

メンテナの優れた設計の観点から見たものに戻りましょう

なぜこれが私の良いデザインの見方だと思うのか、よくわかりません。システムにタスクをSomeClass追加するたびに変更する必要があるコンストラクターを使用することは、間違いなく良い設計ではありません。これについては安全に同意できます。それはOCPを壊します。私は誰にもそのようなことをするようにアドバイスするつもりはありません。多くの引数を持つコンストラクターを持つことに加えて、少なくともデザインの匂いがします。Simple Injector の次のマイナー リリースでは、多くの場合 SRP 違反を示しているため、依存関係が多すぎる型に関する診断警告が追加されます。しかし、Simple Injector がガイダンスを提供することで開発者を「支援」しようとしている様子をもう一度見てください。

それでも、私はジェネリック インターフェイスの使用を推進しています。これは、Simple Injector の設計が特に最適化されているケースです。ITask インターフェイスは、この良い例です。その場合、ITask<T>は多くの場合、実行したいビジネス動作の抽象化であり、Tは実行する操作のすべてのパラメーターを保持するパラメーター オブジェクトです (メッセージ ハンドラーを使用したメッセージとして見ることができます)。ただし、これは、コンシューマーが特定のパラメーター セット (特定のバージョンのT) を使用して操作を実行する必要がある場合 (たとえば、 を実行したい場合) にのみ役立ちますITask<ShipOrder>。パラメータを指定せずにすべてのタスクのバッチを実行しているため、 に基づく設計ITask<T>はおそらく厄介です。

しかし、それが適切であると仮定しましょう。これを仮定して、Simple Injector がこの場合にどのように最適化されるかを説明します。この更新の最後に、Simple Injector があなたのケースでどのように役立つかをお見せしますので、息を止めてください. コード サンプルでは、​​一般的なタスクを次のように登録します。

container.Register<ITask<SomeGeneric1>(() => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()));
container.Register<ITask<SomeGeneric2>(() => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()));
container.Register<ITask<SomeGeneric3>(() => new DefaultImportFilesTask(container.GetInstance<IFileSystem>()));

タスク実装のコンストラクターを変更するたびに、このコードを変更する必要があるため、これはシステム内のすべてのタスクを登録するかなり面倒な方法です。Simple Injector を使用すると、コンストラクターを参照して型を自動配線できます。つまり、Simple Injector を使用すると、このコードを次のように単純化できます。

container.Register<ITask<SomeGeneric1>, FileWatchTask>();
container.Register<ITask<SomeGeneric2>, DefaultFtpTask>();
container.Register<ITask<SomeGeneric3>, DefaultImportFilesTask>();

これはすでにはるかに保守しやすく、パフォーマンスが向上し、後でコンテキストベースの注入などの他の興味深いシナリオを追加できます (Simple Injector がオブジェクト グラフ全体を制御しているため)。これは、Simple Injector で物事を登録するための推奨される方法です (可能であれば Func の使用を避けてください)。

それでも、タスクが中心要素であるアーキテクチャを使用している場合、おそらく新しいタスクの実装をかなり定期的に追加するでしょう。これにより、登録行が数十行になり、タスクを追加するたびにこのコードに戻って行を追加する必要があります。ただし、Simple Injector には、これを 1 行のコードに縮小できるバッチ登録機能があります。

// using SimpleInjector.Extensions;
container.RegisterManyForOpenGeneric(typeof(ITask<>), typeof(ITask<>).Assembly);

この行を呼び出すことにより、コンテナーはITask<T>インターフェイスのアセンブリにあるすべての実装を検索し、それらを登録します。これはリフレクションを使用して実行時に行われるため、新しいタスクがシステムに追加されたときに行を変更する必要はありません。

OCP について話しているので、IMO Simple Injector は OCP を強力にサポートしています。ある時点で、他のすべてのフレームワークを凌駕しています。OCP について考えるとき、私は特に 1 つの特定のパターン、デコレータ パターンについて考えます。デコレータ パターンは、OCP を適用するときに使用する非常に重要なパターンです。たとえば、ビジネス ロジック自体の一部を変更して分野横断的な問題を追加するべきではありませんが、クラスをデコレータでラップすることで追加するのが最善です。Simple Injector を使用すると、次の 1 行のコードでデコレータを追加できます。

// using SimpleInjector.Extensions;
container.RegisterDecorator(typeof(ITask<>), typeof(TransactionTaskDecorator<>));

これにより、(transient)が解決されたときにTransactionTaskDecorator<T>すべての実装がラップされることが保証されます。ITask<T>これらのデコレーターはコンテナーのパイプラインに統合されています。つまり、独自の依存関係を持つことができ、イニシャライザーを持つことができ、特定のライフスタイルを持つことができます。そして、デコレータは簡単に積み重ねることができます:

container.RegisterDecorator(typeof(ITask<>), typeof(TransactionTaskDecorator<>));
container.RegisterDecorator(typeof(ITask<>), typeof(DeadlockRetryTaskDecorator<>));

これにより、すべてのタスクがトランザクション デコレータでラップされ、そのトランザクション デコレータが再びデッドロック リトライ デコレータでラップされます。また、デコレータを条件付きで適用することもできます:

container.RegisterDecorator(typeof(ITask<>), typeof(ValidationTaskDecorator<>),
    context => ShouldApplyValidator(context.ServiceType));

また、デコレーターにジェネリック型の制約がある場合、ジェネリック型の制約が一致すると、Simple Injector は自動的にデコレーターを適用します。これについて何もする必要はありません。また、Simple Injector は式ツリーを生成し、それらをデリゲートにコンパイルするため、これはすべて 1 回限りのコストです。これは無料という意味ではありませんが、支払いは解決ごとではなく 1 回のみです。

Simple Injector ほど簡単かつ柔軟にデコレータを追加できる DI ライブラリは他にありません。

これが Simple Injector の真価を発揮するところですが、それではあまり役に立ちません :-)。この場合、一般的なインターフェースは役に立ちませんが、それでも、あなたの場合でも、登録をより保守しやすくすることができるかもしれません。システムに多くのタスク実装がある場合 (つまり、3 つよりはるかに多い場合)、次のようなことを自動化できる可能性があります。

var taskTypes = (
    from type in typeof(ITask).Assemby.GetTypes()
    where typeof(ITask).IsAssignableFrom(type)
    where !type.IsAbstract && !type.IsGenericTypeDefinition
    select type)
    .ToList();

// Register all as task types singleton
taskTypes.ForEach(type => container.Register(type, type, Lifestyle.Singleton));

// registers a list of all those (singleton) tasks.
container.RegisterAll<ITask>(taskTypes);

または、Simple Injector 2.3 以降では、インスタンスをメソッドRegistrationに直接渡すことができます。RegisterAll

var taskTypes =
    from type in typeof(ITask).Assemby.GetTypes()
    where typeof(ITask).IsAssignableFrom(type)
    where !type.IsAbstract && !type.IsGenericTypeDefinition
    select type;

// registers a list of all those (singleton) tasks.
container.RegisterAll(typeof(ITask),
    from type in taskTypes
    select Lifestyle.Singleton.CreateRegistration(type, type, container));

ただし、これは、これらすべてのタスク実装に単一のパブリック コンストラクターがあり、すべてのコンストラクター引数が解決可能であることを前提としています (int や string などの構成値はありません)。そうでない場合は、フレームワークのデフォルトの動作を変更する方法がありますが、これについて何か知りたい場合は、その議論を新しい SO の質問に移動することをお勧めします。

繰り返しになりますが、あなたをいらいらさせてしまったら申し訳ありませんが、他の多くの人を助ける機会を逃すよりも、一部の開発者を悩ませています :-)

于 2013-03-09T11:06:31.187 に答える