Dependency Injection For Rich Domain Model 1

来源:百度文库 编辑:神马文学网 时间:2024/04/29 09:08:43
怎样给马丁的充血域对象注射依赖
本文中,我们将简要介绍一下在应用Martin Fowler所推荐的rich domain model模型中遇到的依赖注射的问题。然后在本文后半部分我们给出一个相对比较简单的利用Yan容器来管理这些域对象的依赖的方法。这个方法不要求程序对容器有任何直接或者间接的依赖,如果愿意,我们完全可以用手工注射作为一个实现策略。(当然,它要求容器有足够的灵活性来处理这种依赖管理。)
然后,在下一章Transparent Dependency Injection for Rich Domain Model中,我们介绍Yan提供的一种结合动态代理的更加方便的解决方案。
hyperaemia domain model
在讨论domain model的时候,我惊讶地发现,一些同志在应用马丁同学的充血模型(hyperaemia)的时候,为如何给这些从hibernate创建而来的domain对象怎么注射依赖而烦恼.
hibernate或者Dao,不可能知道你的充血对象都需要什么外部的依赖。对hibernate来说,不管你对象是否充血,它都是当作一个简单的结构来往里填入数据成员。这其实很尴尬,尤其是对Dao,它将返回一个半成品对象,这个对象只有一些数据成员被设置,而其它的对这个对象的功能至关重要的依赖却还没有被建立。这是什么样的一个Dao阿。返回一个 "小心!它看起来是BankAccount,但是千万别当作BankAccount用呢先,你得先设置各种依赖关系。" 。
我发现我怎么就无论如何喜欢不起来这种充血模型呢?一个好的设计,应该尽量用静态类型来表示设计意图。尽量让符合类型要求的操作合法化,而不符合类型要求的操作非法。在这个充血模型里面,事情却不是这么简单。从Dao返回出来的BankAccount不是真正的BankAccount.
更优雅的设计,在我看来,应该是Dao只返回纯数据的结构,比如BankAccountData,这个对象里除了getter/setter就没有业务逻辑。然后在service层面把BankAccountData转换为BankAccount。这样,就不存在一个半成品的状态,不存在假象。类型告诉我们什么可用,什么就必然可用。
当然,这也许比较麻烦,毕竟多了一个转换的步骤,多了BankAccountData这个类型。
AOP?
好了,不唧唧歪歪了。让我们看看同学们头疼的注射依赖问题。
目前,包括xiecc同学在内的许多朋友都不约而同地选择用aop来完成这个依赖注射,我们不用这种手工注射:
class BankAccountService{private final SmtpService smtp;private final BankAccountDao dao;BankAccount getBankAccountById(String id){BankAccount acc = dao.getById(id);acc.setSmtpService(smtp);return acc;}}
而是:
class BankAccountService{private final SmtpService smtp;private final BankAccountDao dao;BankAccount getBankAccountById(String id){BankAccount acc = dao.getById(id);//acc.setSmtpService(smtp); return acc;}}
简单地把acc.setSmtpService给注释掉。这段代码虽然简短,看起来很费解,因为明明smtpService没有被设置,这个BankAccount仍然还是个半成品嘛。
然后,弄一个aspect,让它偷偷把smtpService注射给BankAccount。
什么?aspect怎么知道smtpService?当然是service locator了。通过主动在容器里查询,找到smtpService不就好了?
我来给这个方法挑挑毛病:
上面说过了。代码不容易理解。明明setSmtpService没有被调用,不知道有个aop会在预定地点冒出来得人抓破头皮也看不懂这程序怎么工作的。 不容易unit test。这个代码能够工作,完全依赖于aop的存在。即使在单体测试,aop,以及容器也必须包含在内。 aspect的代码内部仍然是service locator,而不能dependency injection。而service locator的缺点就跟jndi一样的。 aop呀?aop一般不是为了所谓cross-cut concern设计的吗?我们看看这里的setSmtpService是cross-cut concern吗?它不明显是整个逻辑的不可分割的一部分吗?这不是横切,而是乱搅和。 aop。相当重量级的东西呀。就为了这么个dependency injection就引入这么大代价?
回归ioc。
那么,我们推荐什么样的方法呢?
首先,这个方法要足够简单。不引入额外的复杂性。 其次,它应该对单体测试友好。 最后,代码应该更易懂。
怎么办?还是ioc。任何时候,ioc都是我们坚持的原则。
仔细分析需求,我们这里不是要寻找容器,也不是要个aop,真正最根本的需要是“依赖注射”。
根据我们面向接口编程的原则,任何时候,如果需要某个外界提供的功能,用接口把它描述出来,然后让外界注射进来。当然,我们这里需要从外界注射的是一个“依赖注射逻辑”。有点绕是么?呵呵,看看代码把:
public interface Injector{void inject(Object obj);}class BankAccountService{private final Injector injector;private final BankAccountDao dao;BankAccountService(Injector injector, BankAccountDao dao){this.injector = injector;this.dao = dao;}BankAccount findAccountById(String id){BankAccount acc = dao.getById(id);injector.inject(acc);return acc;}}
好了,domain object和service的设计到此为止。剩下的就是衣来伸手,饭来张口,等着外面给我注射这个我自家用的注射器了。
这个代码完全是单体测试友好的。因为我甚至可以简单地用手工注射来实现Injector。比如:
Injector manual_injector = new Injector(){public Object inject(Object obj){BankAccount acc = (BankAccount)obj;acc.setSmtpService(new SmtpService());}};
阅读起来,这个代码也是自我解释的。injector.inject(acc)负责注射依赖,于是然后BankAccount就是一个完整的rich domain object了。逻辑清晰直白。
它也不依赖容器,aop等任何不该依赖的东西,就是一个传统的严谨的ioc设计。
从容器注射
下面探讨怎么从Yan的容器给这个Service注射Injector。
首先,因为这些注射都是基于某个不是容器创建的对象进行的,我们用Binder接口来描述这些注射逻辑。在这个例子中,我们只关心smtpService这个property:
Binder injection = new Binder(){public Creator bind(Object obj){return Components.value(obj).bean(new String[]{"smtpService"});}};
这个bind()函数返回的Component对象就负责对这个参数??obj??的smtpService进行注射。
接下来,需要把这个Binder对象转换成一个生成Injector实例的Component,InjectorHelper 这个帮助类可以被用来进行这个转换:
void registerBankAccountService(Container yan){Component some_dao_component = ...;Binder injection = ...;InjectorHelper helper = new InjectorHelper();Component injector = helper.getInjectorComponent(Injector.class, injection);yan.registerComponent("bankaccount_service",Components.ctor(BankAccountService.class).withArgument(0, some_dao_component).withArgument(1, injector);}
new InjectorHelper() 创建一个InjectorHelper对象。 helper.getInjectorComponent(Injector.class, injection) 把这个Binder对象转换为一个可以创建Injector接口实例的Component。 最后, 这个 injector 组件被制定作为BankAccountService这个组件的第二个参数. 这样,这个把注射器注射给Service的任务就算完成了。