在上一章 Dependency Injection For Rich Domain Model 中, 我們給出了一種通過顯式調(diào)用"injector.inject(domain_obj)"的方法來解決對這些域?qū)ο蟮淖⑸涞姆椒?。這個方法基本上屏蔽了依賴注射的復雜性,也避免了對aop或者容器的依賴,保證了單體測試的方便。
然而,程序員的美德是懶惰的。被迫調(diào)用一些inject()函數(shù)還是讓人不是太爽??吹絘op的那種完全不需要關(guān)心依賴的簡單,我們能不能做點什么呢?
答案是肯定的。
我們的理想是,讓DAO對象在返回BankAccount的時候就自動把依賴設(shè)置好??墒?,我們前面也分析了,DAO層的代碼是不可能也不應該知道業(yè)務(wù)層的那些依賴的。讓BankAccountDaoImpl非要知道SmtpService也顯得很滑稽。看來似乎除了aop一家,別無分店了。
山重水盡疑無路,柳暗花明又一村。我們還是在重重困難中看到了一線光明。proxy,對,我們不用full-blown的aop,但是我們可以用OO的經(jīng)典模式:proxy來把BankAccountDaoImpl封裝成一個知道這些依賴的對象。如此:
class BankAccountService{private final BankAccountDao dao;BankAccount getAccountById(String id){return dao.getById(id);}public BankAccountService(BankAccountDao dao){this.dao = dao;}...}
我們可以假設(shè)這個Dao沒有返回半成品,而是一個完全有效的完整的BankAccount。
同時,我們不希望改動BankAccountDaoImpl類,它仍然保持它對業(yè)務(wù)層的一無所知。只不過,在組裝BankAccontService的時候,我們做點手腳:
new BankAccountService(new DaoProxy(new BankAccountDaoImpl(), new SmtpService()));
通過把BankAccountDaoImpl傳遞給一個proxy,我們可以得到一個滿足我們需要的BankAccountDao對象。
這個proxy怎么寫呢?一個naive的實現(xiàn)如下:
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的動態(tài)代理來實現(xiàn)。最終,我們希望容器在注射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);}
如此,通過容器的支持,我們可以完美地給充血對象注射依賴了。
假設(shè),我們的BankAccountImpl還可能返回Bank這個也需要注射依賴的充血對象,怎么辦?其實很簡單,我們只要再多調(diào)用一次getProxyComponentReturningProxy()函數(shù)就可以了。如下:
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返回的都是依賴注射已經(jīng)完成的可用對象。那么如果我這么做怎么辦?
class BankAccountUser{void f(){BankAccount account = new BankAccount();account.sendMail(...);}}
這個代碼仍然不能工作因為sendMail所依賴的smtp service沒有被設(shè)置。
對這種問題,我們建議采用下面這種方法:
不直接調(diào)用構(gòu)造函數(shù),而是定義一個BankAccountFactory的接口。采用上面同樣的方法,容器可以注射一個能夠處理依賴的BankAccountFactory實現(xiàn)。比如:
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模型。提出了在這個模型中的依賴注入的尷尬困境。
我們比較了現(xiàn)在一些流行的方法(使用aop攔截)。
最后,我們給出了一個作用類似于aop的攔截,但是本身不依賴aop而僅僅用純粹的ioc依賴注射的方法。
在這種方法中,domain object的使用者可以假設(shè)dao返回的是一個完全可用的rich domain object,所有的依賴問題都被轉(zhuǎn)移到容器中管理。
不同于采用aop的方法,整個解決方案是完全的dependency injection,沒有service locator。單元測試能力也沒有受到影響。