通过 DI 容器创建的对象初始化是否有模式?

153

我正在尝试让Unity管理我的对象创建,并且我想有一些初始化参数,这些参数在运行时是未知的:

目前,我能想到的唯一方法是在接口上拥有一个Init方法。

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

那么在Unity中使用它,我会这样做:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");
在这种情况下,runTimeParam 参数是基于用户输入在运行时确定的。简单情况下,这里只需返回runTimeParam的值,但实际上该参数将是文件名之类的内容,并且初始化方法将对文件执行某些操作。
这会产生许多问题,即 Initialize方法在接口上是可用的,可以被多次调用。在实现中设置一个标志并在重复调用 Initialize时抛出异常似乎有点笨拙。
在解析接口时,我不想知道关于 IMyIntf 的实现任何细节。但我所想要的是,该接口需要某些一次性初始化参数的知识。是否有一种方式可以通过注释(属性?)将此信息添加到接口中,并在创建对象时将其传递给框架?

9
你没有理解使用 DI 容器的重点。依赖项应该由容器来解决。 - Pierreten
你从哪里获取所需的参数?(配置文件、数据库、??) - Jaime
runTimeParam是一个依赖项,它根据用户输入在运行时确定。这种情况的替代方案应该是将其拆分为两个接口 - 一个用于初始化,另一个用于存储值吗? - Igor Zevaka
在IoC中,依赖通常指依赖于其他引用类型的类或对象,这些类或对象可以在IoC初始化阶段确定。如果您的类只需要一些值来工作,那么您的类中的Initialize()方法就非常方便了。 - The Light
我的意思是,想象一下你的应用程序中有100个类可以应用这种方法;那么你就需要为这100个类创建额外的100个工厂类+100个类接口,如果你只是使用Initialize()方法,就可以轻松解决这个问题。 - The Light
5个回答

286
任何需要在运行时构建特定依赖项的场合,抽象工厂模式是解决方案。
在接口上拥有 Initialize 方法会让人感到漏洞百出
在您的情况下,我建议您将 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` 关键字来保护类的不变量。不需要使用令人讨厌的 Initialize 方法。
一个 `IMyIntfFactory` 实现可能如下所示:
public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}
在所有需要一个 IMyIntf 实例的消费者中,只需通过构造函数注入请求IMyIntfFactory,即可解决问题。
如果您正确注册,任何值得其盐的 DI 容器都能够自动为您连接 IMyIntfFactory 实例。

13
问题在于,类似Initialize这样的方法是您的API的一部分,而构造函数则不是。 - Mark Seemann
13
此外,一个初始化方法表明了时间耦合(Temporal Coupling):http://blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx - Mark Seemann
2
@Darlene 你或许可以使用惰性初始化的装饰器,详见我的书8.3.6章节。我在Big Object Graphs Up Front演讲中也提供了一个类似的示例。 - Mark Seemann
2
@Mark 如果工厂创建 MyIntf 实现需要更多的 runTimeParam(即:其他服务,希望通过 IoC 解决),那么您仍然需要在工厂中解决这些依赖关系。我喜欢 @PhilSandler 的答案,将这些依赖项传递到 工厂 构造函数中来解决这个问题 - 这也是您的看法吗? - Jeff
2
你的回答对我来说非常好,尤其是这个问题的答案 - Jeff
显示剩余16条评论

16
通常情况下,当你遇到这种情况时,你需要重新审视你的设计,并确定是否混淆了有状态/数据对象和纯服务。在大多数情况下(并非全部),你会希望将这两种类型的对象分开。
如果确实需要在构造函数中传递上下文特定的参数,一种选择是创建一个工厂,通过构造函数解析你的服务依赖项,并将你的运行时参数作为Create()方法(或Generate()、Build()或你命名的其他工厂方法)的参数。
通常认为具有setter或Initialize()方法是不良设计,因为你需要“记住”调用它们,并确保它们不会打开太多你的实现状态(例如防止别人重新调用Initialize或setter)。

5
我曾在一些动态创建ViewModel对象的环境中遇到过这种情况(这个Stackoverflow post描述得非常好)。我喜欢Ninject extension的做法,它允许你基于接口动态创建工厂。

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

我认为我解决了这个问题,感觉非常完整,所以应该是对的一半:))

我将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() 可以被多次调用,但是使用 服务定位器模式 的一些技巧,我们可以将其封装得非常好,使得 IMyIntfSetter 成为几乎与 IMyIntf 不同的内部接口。


14
这不是一个特别好的解决方案,因为它依赖于一个Initialize方法,这是一个有缺陷的抽象。顺便说一句,这看起来不像是服务定位器(Service Locator),而更像是接口注入(Interface Injection)。无论如何,请查看我的答案以获得更好的解决方案。 - Mark Seemann

1

我无法用具体的Unity术语回答,但听起来你正在学习依赖注入。如果是这样,我建议你阅读简短、清晰、信息丰富的Ninject用户指南

这将引导您了解使用DI时的各种选项,以及如何解决您在此过程中遇到的特定问题。在您的情况下,您最有可能想要使用DI容器来实例化您的对象,并使该对象通过构造函数获取其每个依赖项的引用。

本教程还详细介绍了如何使用属性对方法、属性甚至参数进行注释,以在运行时区分它们。

即使您不使用Ninject,本教程也会为您提供适合您目的的功能的概念和术语,您应该能够将该知识映射到Unity或其他DI框架(或说服您尝试Ninject)。


谢谢。我正在评估 DI 框架,NInject 将是我的下一个选择。 - Igor Zevaka
@johann: 提供者是什么? https://github.com/ninject/ninject/wiki/Providers%2C-Factory-Methods-and-the-Activation-Context - anthony

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接