Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3336752
  • 博文数量: 530
  • 博客积分: 13360
  • 博客等级: 上将
  • 技术积分: 5473
  • 用 户 组: 普通用户
  • 注册时间: 2006-07-13 13:32
文章分类

全部博文(530)

文章存档

2017年(1)

2015年(2)

2013年(24)

2012年(20)

2011年(97)

2010年(240)

2009年(117)

2008年(12)

2007年(8)

2006年(9)

分类:

2010-08-02 16:02:29

在上一章 中, 我们给出了一种通过显式调用"injector.inject(domain_obj)"的方法来解决对这些域对象的注射的方法。这个方法基本上屏蔽了依赖注射的复杂性,也避免了对aop或者容器的依赖,保证了单体测试的方便。

然而,程序员的美德是懒惰的。被迫调用一些inject()函数还是让人不是太爽。看到aop的那种完全不需要关心依赖的简单,我们能不能做点什么呢?

完全透明的注射而不用aop,可能吗?

答案是肯定的。

我们的理想是,让DAO对象在返回BankAccount的时候就自动把依赖设置好。可是,我们前面也分析了,DAO层的代码是不可能也不应该知道业务层的那些依赖的。让BankAccountDaoImpl非要知道SmtpService也显得很滑稽。看来似乎除了aop一家,别无分店了。

山重水尽疑无路,柳暗花明又一村。我们还是在重重困难中看到了一线光明。proxy,对,我们不用full-blown的aop,但是我们可以用OO的经典模式:proxy来把BankAccountDaoImpl封装成一个知道这些依赖的对象。如此:

class BankAccountService{
  private final BankAccountDao dao;
BankAccount getAccountById(String id){
return dao.getById(id);
}
public BankAccountService(BankAccountDao dao){
this.dao = dao;
}
...
}

我们可以假设这个Dao没有返回半成品,而是一个完全有效的完整的BankAccount。

同时,我们不希望改动BankAccountDaoImpl类,它仍然保持它对业务层的一无所知。只不过,在组装BankAccontService的时候,我们做点手脚:

new BankAccountService(new DaoProxy(new BankAccountDaoImpl(), new SmtpService()));

通过把BankAccountDaoImpl传递给一个proxy,我们可以得到一个满足我们需要的BankAccountDao对象。

这个proxy怎么写呢?一个naive的实现如下:

        class BankAccountDaoProxy implements BankAccountDao{ 
private final SmtpService smtp;
private final BankAccountDao dao;
public BankAccount getById(String id){
BankAccount acc = dao.getById(id);
acc.setSmtpService(smtp);
 return acc;
}
...
}

不过,这个解决方案虽然简单,却比较笨拙:

  1. 我们需要手工注射所有的依赖。调用这些setSmtpService(), setWebService()等等等等setter。
  2. 我们需要给每个BankAccount类型的返回值注射依赖。很累人。

对于第一点,我们还可以用 中提到的Injector接口来抽象它。对于第二点,可以考虑使用java的动态代理来实现。最终,我们希望容器在注射BankAccountDao给BankAccountService的时候能够注射这个代理。这样,所有的依赖仍然被容器管理。在程序代码中,我们得到了使用aop同样的好处,但是却没有引入对aop的依赖。

Yan对充血domain object的依赖注射的完整解决方案

InjectorHelper类对这个问题提供了完整的解决方案。使用这个类,我们可以把组装代码变成这样:

    void registerBankAccountService(Container yan){ 
Binder injection = new Binder(){
public Creator bind(Object obj){
 return Components.value(obj) .bean(new String[]{"smtpService"}); } }; Component dao_impl = Components.ctor(BankAccountDaoImpl.class); InjectorHelper helper = new InjectorHelper(); Component dao = helper.getProxyComponentReturningInjected( BankAccountDao.class, dao_impl, BankAccount.class, injection ); yan.registerComponent("bankaccount_service", Components.ctor(BankAccountService.class) .withArgument(0, dao); }
  • 对域对象的注射仍然用一个Binder对象表示。
  • InjectorHelper.getProxyComponentReturningInjected(...) 负责把dao_impl这个生成BankAccountDaoImpl对象的组件转换为一个生成我们需要的proxy的组件。
  • getProxyComponentReturningInjected()的第一个参数是目标接口。这个proxy必须实现这个BankAccountDao接口。
  • 第二个参数是一个生成那个被代理对象的组件。
  • 第三个参数是需要注射依赖的返回值类型。我们只对BankAccount类型的对象注射依赖。
  • 第四个参数是注射的具体逻辑。

如此,通过容器的支持,我们可以完美地给充血对象注射依赖了。

给若干种类型的返回值注射依赖

假设,我们的BankAccountImpl还可能返回Bank这个也需要注射依赖的充血对象,怎么办?其实很简单,我们只要再多调用一次getProxyComponentReturningProxy()函数就可以了。如下:

void registerBankAccountService(Container yan){                        Binder bankaccount_injection = ...;                        Binder bank_injection = ...;                        Component dao_impl = Components.ctor(BankAccountDaoImpl.class);                        InjectorHelper helper = new InjectorHelper();                        Component dao = helper.getProxyComponentReturningInjected(                        BankAccountDao.class, dao_impl, BankAccount.class, bankaccount_injection                        );                        dao = helper.getProxyComponentReturningInjected(                        BankAccountDao.class, dao, Bank.class, bank_injection                        );                        yan.registerComponent("bankaccount_service",                        Components.ctor(BankAccountService.class)                        .withArgument(0, dao);                        }

这里,我们对一个dao组件包装了两层代理,头一层代理负责对BankAccount类型的返回值进行注射。第二个代理负责对Bank类型的返回值进行注射。
如此,两种injection逻辑就都可以被绑在组件上了。

程序中直接创建的域对象怎么办?

上面的方法封装了dao,保证dao返回的都是依赖注射已经完成的可用对象。那么如果我这么做怎么办?

class BankAccountUser{ 
void f(){
  BankAccount account = new BankAccount();
account.sendMail(...);
 }
}

这个代码仍然不能工作因为sendMail所依赖的smtp service没有被设置。

对这种问题,我们建议采用下面这种方法:
不直接调用构造函数,而是定义一个BankAccountFactory的接口。采用上面同样的方法,容器可以注射一个能够处理依赖的BankAccountFactory实现。比如:

public class BankAccountUser{ 
public BankAccountUser(BankAccountFactory factory){...}
private final BankAccountFactory factory;
void f(){
BankAccount account = factory.newBankAccount();
 account.sendMail(...);
}
}
void registerBankAccountUser(Container yan){
 Component ctor = Components.ctor(BankAccount.class);
Component factory = ctor.bean(new String[]{"smtpService"}) .factory(BankAccountFactory.class);
Component user = Components.ctor(BankAccountUser.class) .withArgument(0, factory); yan.registerComponent("bankaccountuser", user); }
  • xxx.factory(BankAccountFactory.class) 创建一个组件,这个组件在被实例化的时候,会返回一个BankAccountFactory类型的对象。这个对象会采用 xxx 所定义的逻辑来生成对象。

结论

本文中,我们阐述了Martine Fowler倡导的rich domain object模型。提出了在这个模型中的依赖注入的尴尬困境。
我们比较了现在一些流行的方法(使用aop拦截)。

最后,我们给出了一个作用类似于aop的拦截,但是本身不依赖aop而仅仅用纯粹的ioc依赖注射的方法。
在这种方法中,domain object的使用者可以假设dao返回的是一个完全可用的rich domain object,所有的依赖问题都被转移到容器中管理。

不同于采用aop的方法,整个解决方案是完全的dependency injection,没有service locator。单元测试能力也没有受到影响。

 ()

看完以后,发现我对一些基本概念不了解了。什么是aop?dynamic proxy跟aop难道没有联系?我总是觉得动态代理为aop的截取提供了物质基础。

另外,我越发的觉得DI container干的事情其实就是factory + 有副效应构造函数。我记得ajoo是坚决反对构造函数中带上副效应的。它明显的导致异常处理难弄。莫非我又错了不成?

Posted by Anonymous at 二月 14, 2006 10:04 | Permalink
阅读(1210) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~