我一直在阅读 Misko Hevery 关于依赖注入的经典 文章,基本上是“将对象图创建代码与代码逻辑分离”。
主要思想似乎是“摆脱‘新’操作符”,将它们放入专用对象(“工厂”)并注入你所依赖的一切。”
现在,我似乎无法理解如何使这与由其他几个组件组成的对象一起工作,并且他们的工作是将这些组件与外部世界隔离开来。
蹩脚的例子
一个 View 类,表示几个字段和一个按钮的组合。所有组件都依赖于图形化的 ui 上下文,但您希望将其隐藏在每个子组件的界面后面。
所以像(在伪代码中,我猜语言并不重要):
class CustomView() {
public CustomView(UIContext ui) {
this.ui = ui
}
public void start() {
this.field = new Field(this.ui);
this.button = new Button(this.ui, "ClickMe");
this.button.addEventListener(function () {
if (field.getText().isEmtpy()) {
alert("Field should not be empty");
} else {
this.fireValueEntered(this.field.getText());
}
});
}
// The interface of this component is that callers
// subscribe to "addValueEnteredListener"..)
public void addValueEnteredListener(Callback ...) {
}
public void fireValueEnteredListener(text) {
// Would call each listeners in turn
}
}
来电者会做类似的事情:
// Assuming a UIContext comes from somewhere...
ui = // Wherever you get UI Context from ?
v = new CustomView(ui);
v.addValueEnteredListener(function (text) {
// whatever...
});
现在,这段代码有三个“新”运算符,我不确定 Misko(和其他 DI 支持者)提倡删除哪一个,或者如何删除。
摆脱 new Field() 和 new Button()
只需注入它
我不认为这里的想法是实际注入Field 和 Button 的实例,可以这样做:
class CustomView() {
public CustomView(Field field, Button button) {
this.field = field;
this.button = button;
}
public void start() {
this.button.addEventListener(function () {
if (field.getText().isEmtpy()) {
alert("Field should not be empty");
} else {
this.fireValueEntered(this.field.getText());
}
});
}
// ... etc ...
这无疑使组件的代码更轻巧,而且它实际上隐藏了 UI 的概念,因此 MetaForm 组件在可读性和可测试性方面明显得到了改进。
但是,创建这些东西的负担现在落在了客户端上:
// Assuming a UIContext comes from somewhere...
ui = // wherever ui gets injected from
form = new Form(ui);
button = new Button(ui);
v = new CustomView(form, button);
v.addValueEnteredListener(function (text) {
// whatever...
});
这对我来说听起来真的很麻烦,尤其是因为客户知道班级的所有内部人员,这听起来很傻。
妈妈知道,给她注射
这些文章似乎提倡的是注入一个工厂来创建组件元素。
class CustomView() {
public CustomView(Factory factory) {
this.factory = factory;
}
public void start() {
this.field = factory.createField();
this.button = factory.createButton();
this.button.addEventListener(function () {
if (field.getText().isEmtpy()) {
alert("Field should not be empty");
} else {
this.fireValueEntered(this.field.getText());
}
});
}
// ... etc ...
然后对调用者来说一切都变得很好,因为它只需要从某个地方获取工厂(并且这个工厂将是唯一知道 UI 上下文的,所以为解耦欢呼吧。)
// Assuming a UIContext comes from somewhere...
factory = // wherever factory gets injected from
v = new CustomView(factory);
v.addValueEnteredListener(function (text) {
// whatever...
});
一个可能的缺点是,在测试 MetaForm 时,您通常必须使用“模拟”工厂......创建字段和按钮类的模拟版本。但显然还有另一个缺点......
哟工厂好胖!!
工厂将有多大?如果您严格遵循该模式,那么您想要在运行时在应用程序中创建的每个单独的组件(通常是 UI 的情况,对)都必须在至少一个工厂中获得自己的 createXXXXX 方法。
因为现在你需要:
- Factory.createField 创建字段
- Factory.createButton 创建按钮
- Factory.createMetaForm 用于在客户端创建字段、按钮和 MetaForm(比如说 MetaEditPage 要使用一个)
- 显然是一个 Factory.createMetaEditPage 为客户端..
- ......和它的乌龟一路走来。
我可以看到一些简化这一点的策略:
- 尽可能将在“启动”时创建的图形部分与在运行时创建的部分分开(前者使用像 Spring 这样的 DI 框架,后者使用工厂)
- 将工厂分层,或在同一个工厂中并置相关对象(UIWidgetFactory 对 Field 和 Button 有意义,但您会将其他的放在哪里?在链接到应用程序的工厂中?到其他逻辑级别?)
我几乎可以听到所有来自 C 人的笑话,即没有 Java 应用程序可以在不调用 ApplicationProcessFactoryCreator.createFactory().createProcess().startApplication() 的废话链的情况下做任何事情......
所以我的问题是:
- 我在这里完全错过了一点?
- 如果不是,您会建议采用哪种策略来让事情变得可以忍受?
附录:为什么我不确定依赖注入会有所帮助
假设我决定使用依赖注入,以及类似 guice 的框架。我最终会编写这样的代码:
class CustomView
@Inject
private Field fiedl;
@Inject
private Button button;
public void start() {
this.button.addEventListener(....
// etc...
然后什么 ?
我的“作曲根”将如何利用它?我当然不能为 Field 和 Button 配置“单例”(带有小写的“s”,如“类的单个实例”)(因为我想创建与 MetaForm 实例一样多的实例?
使用提供者没有意义,因为我的问题不是我想创建哪个按钮实例,而只是我最近想创建它,并带有一些只对这种形式有意义的配置(例如它的文本) .
对我来说,DI 不会有帮助,因为我是组件的新部分而不是依赖项。而且我想我可以将任何子组件转换为依赖项,并让框架注入它们。只是在这种情况下,注入子组件对我来说看起来真的很不自然,而且很直观……所以再一次,我一定错过了一些东西;)
编辑
特别是,我的问题是我似乎无法理解您将如何测试以下场景:
“当我单击按钮时,如果字段为空,则应该有错误”。
如果我注入按钮,这是可行的,这样我就可以手动调用它的“fireClicked”事件——但感觉有点傻。
另一种方法是只做 view.getButton().fireClicked() ,但这看起来有点难看......