我正在尝试让Unity来管理我的对象的创建,并且我想要一些在运行时才知道的初始化参数:
目前我能想到的唯一方法是在界面上使用Init方法。
interface IMyIntf {
void Initialize(string runTimeParam);
string RunTimeParam { get; }
}
然后使用它(在Unity中)我会这样做:
var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");
在这种情况下,runTimeParam
param是在运行时根据用户输入确定的。这里的简单案例只返回runTimeParam
的值,但实际上参数将类似于文件名,初始化方法将对文件执行某些操作。
这会产生许多问题,即Initialize
方法在界面上可用,并且可以多次调用。
Initialize
时抛出异常似乎很笨拙。
在我解析界面时,我不想知道IMyIntf
的实现。但是,我想要的是知道这个接口需要一定的一次初始化参数。有没有办法以某种方式用这个信息注释(属性?)接口,并在创建对象时将它们传递给框架?
编辑:更多地描述了界面。
答案 0 :(得分:267)
任何需要运行时值来构建特定依赖关系的地方,抽象工厂就是解决方案。
在接口上初始化方法有一种漏洞抽象的气味。
在你的情况下,我会说你应该在如何使用它上建模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
个实例。
答案 1 :(得分:14)
通常,当您遇到这种情况时,您需要重新访问您的设计并确定是否将有状态/数据对象与纯服务混合。在大多数(并非所有)情况下,您都希望将这两种类型的对象分开。
如果确实需要在构造函数中传递特定于上下文的参数,则可以选择创建一个通过构造函数解析服务依赖关系的工厂,并将运行时参数作为Create()方法的参数(或Generate(),Build()或您为工厂方法命名的任何内容。)
有了setter或者一个Initialize()方法通常被认为是糟糕的设计,因为你需要“记住”调用它们并确保它们不会打开过多的实现状态(即什么阻止某人重新调用初始化或设置者?)。
答案 2 :(得分:5)
在我基于Model对象动态创建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上有关该项目网站的更多文档。
答案 3 :(得分:1)
我不能用特定的Unity术语回答,但听起来你只是在学习依赖注入。如果是这样,我建议您阅读简要,明确和信息包装user guide for Ninject。
这将引导您完成使用DI时的各种选项,以及如何解决您在此过程中遇到的具体问题。在您的情况下,您很可能希望使用DI容器来实例化对象,并让该对象通过构造函数获取对其每个依赖项的引用。
本演练还详细介绍了如何使用属性注释方法,属性甚至参数,以便在运行时区分它们。
即使你不使用Ninject,演练也会为你提供适合你目的的功能的概念和术语,你应该能够将这些知识映射到Unity或其他DI框架(或说服你给Ninject试一试。
答案 4 :(得分: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 paradigm的位我们可以非常好地包装它,以便IMyIntfSetter
几乎是一个与{{1}不同的内部接口}。