摆脱子组件对象的"新"运算符

pht*_*ier 5 language-agnostic unit-testing dependency-injection

我一直在阅读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
   }
}
Run Code Online (Sandbox Code Playgroud)

来电者可以这样做:



// Assuming a UIContext comes from somewhere...
ui = // Wherever you get UI Context from ?
v = new CustomView(ui);
v.addValueEnteredListener(function (text) {
 // whatever...
});

Run Code Online (Sandbox Code Playgroud)

现在,这段代码有三个"新"运算符,我不确定Misko(以及其他DI支持者)提倡哪一个摆脱,或者如何.

摆脱新的Field()和新的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 ... 
Run Code Online (Sandbox Code Playgroud)

这使得组件的代码更轻,当然,它实际上隐藏了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...
});

Run Code Online (Sandbox Code Playgroud)

这对我来说听起来真的很麻烦,特别是因为客户知道这个课程的所有内容,这听起来很愚蠢.

妈妈知道,请注意她

文章似乎提倡的是注入工厂来创建组件元素.


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 ... 
Run Code Online (Sandbox Code Playgroud)

然后一切对调用者来说都很好,因为它只需要从某个地方获取工厂(而且这个工厂将是唯一知道UI上下文的人,所以请求解耦.)



// Assuming a UIContext comes from somewhere...

factory = // wherever factory gets injected from 

v = new CustomView(factory);
v.addValueEnteredListener(function (text) {
 // whatever...
});

Run Code Online (Sandbox Code Playgroud)

一个可能的缺点是,在测试MetaForm时,您通常必须使用"Mock"工厂......创建Field&Button类的Mocks版本.但显然还有另一个缺点......

哟'工厂太胖!

工厂有多大?如果您严格遵循该模式,那么您希望在运行时在应用程序中创建的每个frigging组件(通常是UI的情况,对吧)必须在至少一个工厂中获得自己的createXXXXX方法.

因为现在你需要:

  • Factory.createField创建字段
  • Factory.createButton创建按钮
  • Factory.createMetaForm在客户端创建字段,按钮和MetaForm(比如MetaEditPage想要使用一个)
  • 显然是客户端的Factory.createMetaEditPage ..
  • ......和它的乌龟一路走来.

我可以看到一些策略来简化这个:

  • 尽可能地将在"启动"时创建的图形部分与在运行时创建的部分分开(使用像前者这样的DI框架和后者的工厂).
  • 对工厂进行分层,或者在相同的工厂中并置相关对象(对于Field和Button,UIWidgetFactory是有意义的,但是在哪里放置其他的?在与应用程序链接的工厂中?到其他逻辑层?)

我几乎可以听到来自C家伙的所有笑话,没有Java应用程序可以做任何事情而不需要调用ApplicationProcessFactoryCreator.createFactory().createProcess().startApplication()无意义链...

所以我的问题是:

  • 我在这里完全错过了一点?
  • 如果没有,你会建议哪种策略可以让事情变得可以忍受?

附录:为什么我不确定依赖注入会有所帮助

假设我决定使用依赖注入,使用类似guice的框架.我最终会编写这样的代码:


class CustomView

    @Inject
    private Field fiedl;

    @Inject
    private Button button;

    public void start() {
      this.button.addEventListener(....

// etc...
Run Code Online (Sandbox Code Playgroud)

然后什么 ?

我的"组合根"将如何利用它?我可以通常不为Field和Button配置一个"单例"(带有小写的's',如'一个类的单个实例)(因为我想创建它们的实例作为MetaForm的实例?

使用Provider是没有意义的,因为我的问题不是我想要创建哪个按钮实例,而是我想最近创建它,使用一些只对这个表单有意义的配置(例如它的文本) .

对我来说,DI是不会帮助,因为我是新-ING 部分我的组件,而不是依赖.我想我可以将任何子组件转换为依赖项,并让框架注入它们.只是注入子组件对我来说看起来非常人为和直接,在这种情况下......所以,我一定要错过一些东西;)

编辑

特别是,我的问题是我似乎无法理解你将如何测试以下场景:

"当我点击按钮时,如果Field为空,则应该出现错误".

如果我注入按钮,这是可行的,这样我可以手动调用它的"fireClicked"事件 - 但感觉有点傻.

另一种方法是只执行view.getButton().fireClicked(),但这看起来有点难看......

ale*_*ail 1

好吧,你可以使用一些 DI 框架(Spring 或 Guice)并完全摆脱工厂方法。只需在字段/构造函数/设置方法上添加一些注释,DI 框架就会完成工作。在单元测试中使用模拟框架。