使用构造函数注入为可测试性构建的类的正确设计

时间:2011-11-18 00:49:57

标签: java inversion-of-control class-design guice testability

说我有这三层代码:
1.数据库层(ORM)
2. BusinessLogic
3.申请

现在,我按如下方式编写代码:

  1. 数据库层:
    这主要是对数据库进行CURD操作。

    class MyDatabaseLayer {
        public int findValue(int k) {
            // find v
        }
    
        public void insertValue(int k, int v) {
            // Insert v
        }
    }
    
  2. BusinessLogic:
    这保留了调用数据库层并执行操作的实际逻辑。

    class MyBusinessLogic {
        private MyDatabaseLayer dbLayer;
        public MyBusinessLogic(MyDatabaseLayer dbLayer) {
            this.dbLayer  = dbLayer;
        }
    
        public int manipulateValue(int k) {
            dbLayer.findValue(k);
            //do stuff with value
        }
    }
    
  3. 申请层:
    这将调用业务逻辑并显示数据

    MyBusinessLogic logic = new MyBusinessLogic(new MyDatabaseLayer ()); //The problem
    logic.manipulateValue(5);
    
  4. 现在,如果您在应用程序层中看到,它知道数据库层,这是错误的。它知道的太多了。

    Misko Hevery says:构造函数注入很好。但如果我遵循这一点,我将如何实现抽象? Google Guice如何帮助我?

2 个答案:

答案 0 :(得分:1)

控制反转所缺少的部分是应用程序层不直接调用构造函数。它使用工厂(IoC容器)来填充构造函数参数。

无论您使用什么工具,guice / spring / picocontainer / singleton-factories,您的应用程序代码应如下所示:

@Controller
class MyController {
  @Resource // Some container knows about this annotation and wires you in
  MyBusinessLogic myBusinessLogic;

  @RequestMethod("/foo/bar.*")
  public MyWebResponse doService(Response resp, long id, String val) {
     boolean worked = myBusinessLogic.manipulatevalue(id, val);
     return new MyWebResponse(worked);
  }
}

请注意,myBusinessLogic可以通过多种方式注册 - java的@Resource,MyBusinessLogicFactory.getMyBusinessLogic(),guice.get(MyBusinessLogic.class)等。

穷人的解决方案是:

package foo;
class MyBusinessLogicFactory {

   static volatile MyBusinessLogic instance; // package-scoped so unit tests can override
   public static MyBusinessLogic getInstance() {
       if (instance == null) {
           synchronized(MyBusinessLogicFactory.class) {
              instance = new MyBusinessLogic(MyDatabaseLayerFactory.getInstance());
           }
       }
       return instance;
   }
}

// repeat with MyDatabaseLayerFactory

请注意,上述单例模型非常不鼓励,因为它没有范围。你可以在上下文中包含上述内容 - 如

那样
class Context {
   Map<Class,Object> class2Instance = new ConcurrentHashMap<>();
   public <T> T getInstance(Class<T> clazz) {
      Object o = class2Instance.get(clazz);
      if (o == null) { 
        synchronized(this) {
          o = class2Instance.get(clazz);
          if (o != null) return (T)o;
          o = transitivelyLoadInstance(clazz); // details not shown
          for (Class c : loadClassTree(clazz)) { // details not shown
            class2Instance.put(c, o);
          }
        }
      }
      return (T)o;
   } 
   ...
}

但在那时,picocontainer,guice和spring可以更好地解决上述SOOO的复杂性。

此外,像spring这样尊重java 6注释的东西意味着你可以做除了构造函数注入之外的事情,如果你有相同基本数据类型的多个配置项(例如字符串),这非常有用。

答案 1 :(得分:1)

请注意,对于Misko所指的可测试性,理想情况下,您希望为MyDatabaseLayerMyBusinessLogic等创建接口,并让构造函数采用这些接口而不是具体类,以便在测试中您可以轻松传递实际上不使用数据库等的虚假实现。

使用Guice,您可以将接口绑定到ModuleModule中的具体类。然后,您将使用Injector创建Module并从Injector获取一些根对象(例如,您的应用程序对象)。

Injector injector = Guice.createInjector(new AbstractModule() {
  @Override protected void configure() {
    bind(MyDatabaseLayer.class).to(MyDatabaseLayerImplementation.class);
    // etc.
});
MyApplicationLayer applicationLayer = injector.getInstance(MyApplicationLayer.class);

MyApplicationLayer中,您将注入业务逻辑:

@Inject
public MyApplicationLayer(MyBusinessLogic logic) {
  this.logic = logic;
}

这当然是一个非常简单的例子,你可以做更复杂的事情。例如,在Web应用程序中,您可以使用Guice Servlet在servlet上使用构造函数注入,而不是在创建对象后直接从Injector获取对象。