在上一章 Dependency Injection For Rich Domain Model 中, 我们给出了一种通过显式调用"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;}...}
不过,这个解决方案虽然简单,却比较笨拙:
对于第一点,我们还可以用 Dependency Injection For Rich Domain Model 中提到的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。单元测试能力也没有受到影响。
联系客服