出于本次讨论的目的,对象构造函数可能采用两种参数:状态依赖或服务依赖。使用 IOC 容器提供服务依赖项很容易:DI 接管。但相比之下,状态依赖通常只有客户端知道。即对象请求者。
事实证明,让客户端通过 IOC 容器提供状态参数是非常痛苦的。我将展示几种不同的方法来做到这一点,所有这些方法都有很大的问题,并询问社区是否还有我遗漏的其他选择。让我们开始:
在我将 IOC 容器添加到我的项目代码之前,我从一个这样的类开始:
class Foobar {
//parameters are state dependencies, not service dependencies
public Foobar(string alpha, int omega){...};
//...other stuff
}
我决定向 Foobar 类添加一个 Logger 服务依赖关系,也许我会通过 DI 提供:
class Foobar {
public Foobar(string alpha, int omega, ILogger log){...};
//...other stuff
}
但后来我也被告知我需要让类 Foobar 本身“可交换”。也就是说,我需要对 Foobar 实例进行服务定位。我在组合中添加了一个新界面:
class Foobar : IFoobar {
public Foobar(string alpha, int omega, ILogger log){...};
//...other stuff
}
当我进行服务定位器调用时,它将为我提供 ILogger 服务依赖项。不幸的是,状态依赖项 Alpha 和 Omega 并非如此。一些容器提供了一种语法来解决这个问题:
//Unity 2.0 pseudo-ish code:
myContainer.Resolve<IFoobar>(
new parameterOverride[] { {"alpha", "one"}, {"omega",2} }
);
我喜欢这个功能,但我不喜欢它是无类型的,并且对开发人员来说必须传递哪些参数(通过智能感知等)并不明显。所以我看另一个解决方案:
//This is a "boiler plate" heavy approach!
class Foobar : IFoobar {
public Foobar (string alpha, int omega){...};
//...stuff
}
class FoobarFactory : IFoobarFactory {
public IFoobar IFoobarFactory.Create(string alpha, int omega){
return new Foobar(alpha, omega);
}
}
//fetch it...
myContainer.Resolve<IFoobarFactory>().Create("one", 2);
以上解决了类型安全和智能感知问题,但它(1)强制 Foobar 类通过服务定位器而不是 DI 获取 ILogger 并且(2)它需要我制作一堆样板(XXXFactory,IXXXFactory)对于我可能使用的各种 Foobar 实现。如果我决定采用纯粹的服务定位器方法,它可能不是问题。但我仍然无法忍受完成这项工作所需的所有样板。
所以我尝试了另一种容器提供的支持:
//overall, this is a pseudo-memento pattern.
class Foobar : IFoobar {
public Foobar (FoobarParams parms){
this.alpha = parms.alpha;
this.omega = parms.omega;
};
//...stuff
}
class FoobarParams{
public FoobarParams(string alpha, int omega){...};
}
//fetch an instance:
FoobarParams parms = new FoobarParams("one",2);
//Unity 2.0 pseudo-code...
myContainer.resolve<IFoobar>(new parameterOverride(parms) );
通过这种方法,我已经恢复了一半的智能。但是我必须等到运行时才能检测到我可能忘记提供“FoobarParams”参数的错误。
所以让我们尝试一种新方法:
//code named "concrete creator"
class Foobar : IFoobar {
public Foobar(string alpha, int omega, ILogger log){...};
static IFoobar Create(string alpha, int omega){
//unity 2.0 pseudo-ish code. Assume a common
//service locator, or singleton holds the container...
return Container.Resolve<IFoobar>(
new parameterOverride[] {{"alpha", alpha},{"omega", omega} }
);
}
//Get my instance:
Foobar.Create("alpha",2);
我实际上不介意我使用具体的“Foobar”类来创建 IFoobar。它代表了一个我不希望在我的代码中改变的基本概念。我也不介意静态“创建”中缺乏类型安全,因为它现在被封装了。我的智能感知也在工作!如果它们不适用(Unity 2.0 行为),以这种方式创建的任何具体实例都将忽略提供的状态参数。也许一个不同的具体实现“FooFoobar”可能有一个正式的 arg 名称不匹配,但我仍然很满意。
但这种方法的一个大问题是它只能在 Unity 2.0 中有效(结构图中的不匹配参数会引发异常)。所以只有我留在 Unity 才好。问题是,我开始越来越喜欢 Structure Map。所以现在我进入另一个选择:
class Foobar : IFoobar, IFoobarInit {
public Foobar(ILogger log){...};
public IFoobar IFoobarInit.Initialize(string alpha, int omega){
this.alpha = alpha;
this.omega = omega;
return this;
}
}
//now create it...
IFoobar foo = myContainer.resolve<IFoobarInit>().Initialize("one", 2)
现在有了这个,我对其他方法有了一个很好的折衷:(1)我的论点是类型安全/智能感知的(2)我可以选择通过 DI(如上所示)或服务定位器获取 ILogger,( 3)不需要制作一个或多个单独的具体 FoobarFactory 类(与前面冗长的“样板”示例代码对比),并且(4)它合理地维护了“使接口易于正确使用,难以正确使用”的原则使用不当。” 至少可以说它并不比前面讨论的替代方案差。
一个接受障碍仍然存在:我还想申请“按合同设计”。
我提供的每个示例都有意支持构造函数注入(用于状态依赖项),因为我想保留最常用的“不变”支持。即,不变量在构造函数完成时建立。
在上面的示例中,当对象构造完成时,不变量没有建立。只要我在做本土的“合同设计”,我就可以告诉开发人员在调用 Initialize(...) 方法之前不要测试不变量。
但更重要的是,当 .net 4.0 出来时,我想使用它的“代码契约”支持按契约进行设计。根据我的阅读,它与最后一种方法不兼容。
诅咒!
当然,我也想到我的整个哲学都是错误的。也许有人告诉我,通过服务定位器召唤一个 Foobar : IFoobar 意味着它是一个服务 - 服务只有其他服务依赖关系,它们没有状态依赖关系(例如这些示例中的 Alpha 和 Omega)。我也乐于聆听此类哲学问题,但我也想知道阅读哪些半权威性参考资料会引导我走上这条思想道路。
所以现在我把它转向社区。我应该考虑什么方法我还没有?我必须真的相信我已经用尽了我的选择吗?
ps 这种问题,连同其他问题,让我相信整个 IOC Container 的想法,至少在 .net 中,还为时过早。它让我想起了人们会站在他们的头上让“C”语言感觉面向对象的日子(添加奇怪的宏等)。我们应该寻找的是 IOC Containers 的 CLR 和语言支持。例如,想象一种称为“initiate”的新型接口。启动类似于接口,但也需要特定的构造函数签名。剩下的我留给学生做练习......