DIコンテナーを介して作成されたオブジェクトを初期化するためのパターンはありますか


147

Unityにオブジェクトの作成を管理させようとしているのですが、実行時までわからないいくつかの初期化パラメーターが必要です。

現時点で私がそれを行う方法を考えることができる唯一の方法は、インターフェイスにInitメソッドを持つことです。

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

次に、それを(Unityで)使用するには、次のようにします。

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

このシナリオでは、runTimeParamparamはユーザー入力に基づいて実行時に決定されます。ここでの些細なケースは単にの値を返しますrunTimeParamが、実際にはパラメーターはファイル名のようなものであり、initializeメソッドはファイルに対して何かを行います。

これにより、多くの問題が発生します。つまり、このInitializeメソッドはインターフェースで使用でき、複数回呼び出すことができます。実装にフラグを設定し、を繰り返し呼び出したときに例外をスローするのInitializeは、かなり不格好です。

インターフェイスを解決する時点では、の実装について何も知りたくありませんIMyIntf。しかし、私が欲しいのは、このインターフェースが特定の1回の初期化パラメーターを必要とするという知識です。オブジェクトを作成するときに、何らかの方法でこの情報を使用してインターフェイスに注釈(属性)を付け、それらをフレームワークに渡す方法はありますか?

編集:インターフェースについてもう少し説明しました。


9
DIコンテナーを使用する意味がありません。依存関係はあなたのために解決されることになっています。
ピエールテン2009

必要なパラメーターはどこから入手していますか?(構成ファイル、db、??)
Jaime

runTimeParamユーザー入力に基づいて実行時に決定される依存関係です。これの代替策は、初期化用と値の格納用の2つのインターフェースに分割することですか?
イゴールゼバカ

IoCの依存関係は、通常、IoC初期化フェーズで決定できる他の参照型クラスまたはオブジェクトへの依存関係を指します。クラスが機能するために必要な値がいくつかしかない場合は、ここでクラスのInitialize()メソッドが便利になります。

このアプローチを適用できる100のクラスがアプリにあると想像してください。次に、クラスに100のファクトリクラス+ 100のインターフェイスを追加で作成する必要があります。Initialize()メソッドを使用しているだけで問題は解決します。

回答:


276

特定の依存関係を構築するためにランタイム値が必要な場所であれば、Abstract Factoryがソリューションです。

インターフェイスにInitializeメソッドがあると、Leaky Abstractionの匂いがします。

あなたの場合、私はあなたがそれをどのように使う必要があるかでIMyIntfインターフェースをモデル化するべきだと言うでしょう-あなたはその実装を作成するつもりではありません。これは実装の詳細です。

したがって、インターフェースは次のようになります。

public interface IMyIntf
{
    string RunTimeParam { get; }
}

次に、抽象ファクトリを定義します。

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

これIMyIntfFactoryで、次のIMyIntfような具象インスタンスを作成する具象実装を作成できます。

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

これにより、キーワードを使用してクラスの不変条件保護できることに注目してくださいreadonly。臭い初期化メソッドは必要ありません。

IMyIntfFactory実装は、このような単純なことがあります

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

IMyIntfインスタンスが必要なすべてのコンシューマーでは、コンストラクターインジェクションをIMyIntfFactory介して要求するだけで依存関係を取得できます。

その塩の価値があるDIコンテナーは、IMyIntfFactory正しく登録すれば、インスタンスを自動的にワイヤリングできます。


13
問題は、メソッド(Initializeなど)がAPIの一部であるのに対し、コンストラクターはそうでないことです。blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann

12
さらに、Initializeメソッドは時間的カップリングを示します:blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann

2
@Darlene 私の本のセクション8.3.6で説明されているように、遅延初期化されたデコレーターを使用できる場合があります。私のプレゼンテーションのBig Object Graphs Up Frontでも同様の例を示しています。
Mark Seemann 2013

2
@MarkファクトリーのMyIntf実装の作成でrunTimeParam(読み取り:IoCで解決する必要のある他のサービス)以上が必要な場合でも、ファクトリーでこれらの依存関係を解決する必要があります。これを解決するためにこれらの依存関係をファクトリーのコンストラクターに渡す@PhilSandlerの回答が好きです-それもあなたの考えですか?
ジェフ

2
また素晴らしいものですがこの他の質問に対するあなたの答えは本当に私のポイントに達しました。
ジェフ

15

通常、この状況が発生した場合は、設計を再検討して、ステートフル/データオブジェクトと純粋なサービスを混在させるかどうかを決定する必要があります。ほとんどの場合(すべてではありません)、これら2つのタイプのオブジェクトを分離しておく必要があります。

コンストラクターで渡されるコンテキスト固有のパラメーターが必要な場合、コンストラクターを介してサービスの依存関係を解決するファクトリーを作成し、ランタイムパラメーターをCreate()メソッド(またはGenerate()メソッドのパラメーターとして受け取ります。 )、Build()、またはファクトリーメソッドに名前を付けたもの)。

持っセッターまたは初期化()メソッドがされている一般的に、あなたがreから誰かを停止することですつまり何(それらを呼び出すと、彼らはあまりにも多くの実装の状態の開けないようにするために「記憶」に必要として、悪いデザインであると考えられ-initializeまたはsetterを呼び出しますか?)。


5

また、Modelオブジェクトに基づいてViewModelオブジェクトを動的に作成している環境でこの状況に数回遭遇しました(この他のStackoverflow投稿で非常によく説明されています)。

インターフェイスに基づいてファクトリを動的に作成できるようにするNinject拡張機能が気に入りました

Bind<IMyFactory>().ToFactory();

Unityで同様の機能を直接見つけることはできませんでした。そのため、IUnityContainerに独自の拡張機能を記述しました。これにより、あるタイプの階層から別のタイプの階層に基本的にマッピングする既存のオブジェクトのデータに基づいて新しいオブジェクトを作成するファクトリを登録できます:UnityMappingFactory @ GitHub

単純さと読みやすさを目標に、個々のファクトリー・クラスまたはインターフェース(リアルタイム・セーバー)を宣言せずに、マッピングを直接指定できる拡張機能になりました。通常のブートストラッププロセス中にクラスを登録する場所にマッピングを追加するだけです...

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

次に、CIのコンストラクターでマッピングファクトリインターフェイスを宣言し、そのCreate()メソッドを使用します ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

追加のボーナスとして、マップされたクラスのコンストラクターの追加の依存関係もオブジェクトの作成中に解決されます。

明らかに、これですべての問題が解決するわけではありませんが、これまでのところかなりうまくいったので、共有する必要があると思いました。GitHubのプロジェクトのサイトには、より多くのドキュメントがあります。


1

特定のUnity用語ではお答えできませんが、依存関係注入について学習しているようです。その場合は、Ninjectの簡潔で明確な情報が満載のユーザーガイドを読むことをお勧めします。

ここでは、DIを使用するときに使用できるさまざまなオプションと、その過程で直面する特定の問題を説明する方法について説明します。あなたのケースでは、DIコンテナーを使用してオブジェクトをインスタンス化し、そのオブジェクトにコンストラクターを介して各依存関係への参照を取得させることをお勧めします。

このチュートリアルでは、メソッド、プロパティ、さらには実行時にそれらを区別するために属性を使用してパラメーターに注釈を付ける方法についても詳しく説明します。

Ninjectを使用しない場合でも、このチュートリアルでは目的に合った機能の概念と用語を説明し、その知識をUnityまたは他のDIフレームワークにマッピングできるはずです(またはNinjectを試してみるように説得できます)。 。


それをありがとう。私は実際にDIフレームワークを評価しており、NInjectは私の次のフレームワークになります。
Igor Zevaka


1

私はそれを解決したと思います、そしてそれはかなり健全だと思うので、それは半分正しいでしょう:))

IMyIntf「ゲッター」インターフェースと「セッター」インターフェースに分かれています。そう:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

次に実装:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()それでも複数回呼び出すことができますが、Service Locatorパラダイムのビットを使用して、それを非常にうまくまとめることができるため、IMyIntfSetterとはほぼ内部インターフェイスが異なりIMyIntfます。


13
これは、Leaky AbstractionであるInitializeメソッドに依存しているため、特に良い解決策ではありません。ところで、これはService Locatorのようには見えませんが、Interface Injectionのように見えます。いずれにせよ、より良い解決策については私の答えを参照してください。
Mark Seemann、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.