151

我试图让 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. 不过,我真正想要的是知道这个接口需要某些一次性初始化参数。有没有办法以某种方式用这些信息注释(属性?)接口,并在创建对象时将这些信息传递给框架?

编辑:更多地描述了界面。

4

5 回答 5

283

任何需要运行时值来构造特定依赖项的地方,Abstract Factory都是解决方案。

在接口上有 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不需要臭的初始化方法。

一个IMyIntfFactory实现可能像这样简单:

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

在您需要实例的所有消费者中,您只需通过构造函数注入IMyIntf请求它即可获得依赖。IMyIntfFactory

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

于 2009-12-22T08:50:09.043 回答
16

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

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

拥有 setter 或 Initialize() 方法通常被认为是糟糕的设计,因为您需要“记住”调用它们并确保它们不会打开太多的实现状态(即阻止某人重新-调用初始化或设置器?)。

于 2009-12-22T03:03:57.933 回答
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 上的项目站点上有更多文档。

于 2013-07-11T19:10:44.107 回答
1

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

这将引导您了解使用 DI 时的各种选项,以及如何解决您在此过程中将面临的具体问题。在您的情况下,您很可能希望使用 DI 容器来实例化您的对象,并让该对象通过构造函数获取对其每个依赖项的引用。

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

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

于 2009-12-22T00:56:35.897 回答
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.

于 2009-12-22T02:08:31.867 回答