是否存在用于初始化通过DI容器创建的对象的模式


147

我试图让Unity来管理对象的创建,并且我想拥有一些直到运行时才知道的初始化参数:

目前,我唯一想到的方法是在接口上使用Init方法。

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

然后要使用它(在Unity中),我会这样做:

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

在这种情况下,runTimeParam将在运行时根据用户输入确定参数。此处的普通情况只是返回的值,runTimeParam但实际上,参数将类似于文件名,而initialize方法将对文件进行处理。

这会产生许多问题,即该Initialize方法在界面上可用并且可以多次调用。在实现中设置标志并在重复调用时引发异常Initialize似乎很麻烦。

在我解析界面的那一刻,我不想了解任何有关的实现IMyIntf。不过,我真正想要的是该接口需要某些一次性初始化参数的知识。有没有一种方法可以用此信息注释(属性?)接口,并在创建对象时将其传递给框架?

编辑:描述了更多界面。


9
您错过了使用DI容器的意义。依赖关系应该为您解决。
Pierreten

您从哪里获得所需的参数?(配置文件,数据库,??)
Jaime

runTimeParam是在运行时根据用户输入确定的依赖项。是否应该将其拆分为两个接口-一个用于初始化,另一个用于存储值?
Igor Zevaka 09年

IoC中的依赖关系,通常是指对其他引用类型的类或对象的依赖关系,这些类或对象可以在IoC初始化阶段确定。如果您的类只需要一些值即可工作,那么您的类中的Initialize()方法就变得很方便。
The Light

我的意思是,假设您的应用程序中可以应用100种类;那么您将不得不为您的类创建额外的100个工厂类+ 100个接口,如果您仅使用Initialize()方法,就可以避免使用。
The Light

Answers:


276

在需要运行时值来构建特定依赖项的任何地方,抽象工厂就是解决方案。

泄漏抽象的接口上有初始化方法。

在您的情况下,我会说您应该根据需要的使用方式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通过Constructor Injection来请求依赖即可。

IMyIntfFactory如果正确注册,任何值得盐分的DI容器都可以为您自动连接实例。


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您可能可以使用延迟初始化的Decorator,如我的书的 8.3.6节所述。在我的演讲Big Object Graphs Frontfront中,我还提供了类似的示例。
Mark Seemann

2
@Mark如果工厂创建的MyIntf实现需要的内容不止于此runTimeParam(请阅读:一个IoC希望解决的其他服务),那么您仍然面临着解决工厂中这些依赖项的问题。我喜欢@PhilSandler的答案,即将这些依赖项传递到工厂的构造函数中以解决此问题-您是否也愿意这样做?
杰夫,

2
也是很棒的东西,但是对另一个问题的回答确实很正确。
杰夫,

15

通常,当您遇到这种情况时,您需要重新设计并确定是否将状态/数据对象与纯服务混合在一起。在大多数(并非全部)情况下,您需要将这两种类型的对象分开。

如果确实需要在构造函数中传递特定于上下文的参数,则一个选择是创建一个工厂,该工厂通过构造函数解析服务依赖关系,并将运行时参数作为Create()方法的参数(或Generate( ),Build()或任何您命名的工厂方法)。

通常认为使用setter或Initialize()方法是不好的设计,因为您需要“记住”调用它们,并确保它们不会打开过多的实现状态(例如,阻止某人重新执行该操作的方法) -调用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 09年


1

我想我解决了它,而且感觉很健康,所以它必须是正确的一半:))

我分为IMyIntf“ getter”和“ setter”接口。所以:

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
这不是一个特别好的解决方案,因为它依赖于初始化方法,即泄漏抽象。顺便说一句,这看起来不像服务定位器,而是更像接口注入。无论如何,请参阅我的答案以获得更好的解决方案。
Mark Seemann
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.