分类:
2010-08-02 16:02:29
在上一章 中, 我们给出了一种通过显式调用"injector.inject(domain_obj)"的方法来解决对这些域对象的注射的方法。这个方法基本上屏蔽了依赖注射的复杂性,也避免了对aop或者容器的依赖,保证了单体测试的方便。
然而,程序员的美德是懒惰的。被迫调用一些inject()函数还是让人不是太爽。看到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;
}
...
}
不过,这个解决方案虽然简单,却比较笨拙:
对于第一点,我们还可以用 中提到的Injector接口来抽象它。对于第二点,可以考虑使用java的动态代理来实现。最终,我们希望容器在注射BankAccountDao给BankAccountService的时候能够注射这个代理。这样,所有的依赖仍然被容器管理。在程序代码中,我们得到了使用aop同样的好处,但是却没有引入对aop的依赖。
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); }
如此,通过容器的支持,我们可以完美地给充血对象注射依赖了。
假设,我们的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); }
本文中,我们阐述了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是坚决反对构造函数中带上副效应的。它明显的导致异常处理难弄。莫非我又错了不成?