声明:该贴并不讨论Domain Model对于企业应用是否有意义,JE已经讨论过很多了,有很多优秀的帖供参考,这里仅谈实现。

马丁大叔在《企业应用架构模式中》提出Domain Model(领域模型,领域对象,Domain Object)的概念后,我们发现这才是开发企业应用更OO的模式,以前的Transaction Script简直太土了。。。按照领域模型组织起来的面向对象语言代码,简直美极了,而ROR中的ActiveRecord就是一种Domain Model的实现,其威力大家也都见识过了。但是因为Java语言的特性的原因(例如静态类型),一直没有一种好的,优美的,简单的方式去实现,主要的难题就是领域对象和数据库逻辑的依赖关系(例如和Dao的依赖),大家关于这方面的尝试从来就没有断过,目前有以下几种主流的实现方式:

1 放弃IOC,在DomainObject里直接去获得Dao的Instance,例如在Domain Object里拿到Spring上下文,直接get所需要的Bean,这样的优势就是简单,缺点就是DomainObject与Dao的实现以及上下文的耦合,领域逻辑的执行依赖数据库,无法单元测试。有的观点认为利用内存数据库,这样比真正的访问数据库要快,要方便,认为mock dao没有意义,这个观点还持保留意见 :)

2 利用一些框架的特性去实现Dao或者数据库访问能力的注入,例如利用Hibernate Interceptor,在Domain Object持久化的时候注入,这个方式有个明显的缺点就是Domain Object的生命周期依赖特定环境,也就是说这个Domain Object必须是由Hibernate构造的,其才会被注入Dao。这点其实很郁闷,我们以前有个项目就是用这种方式,自己写了Hibernate Interceptor去注入Domain Object,初看很爽很EASY,但是表现层这边,DWR将用户提交的表单数据转换成DomainObject后,那个Dao当然就是NULL了,于是自己又写了个DWR的 Convertor,去实现注入,而这个Domain Object真的是很Rich,随后又要通过web serivce传输,又要在webservice框架里加拦截器实现注入,真的是很郁闷。

3 将Dao通过方法参数传入,优点就是简单,POJOS In Action里就提到了这种方式,缺点就是会影响方法以及接口的设计,经常会出现一个方法会有6个参数之多,而5个都是Dao,真是很丑陋。

4 利用AspectJ等工具实现编译期注入,Spring 2.0刚出的时候,引入了AspectJ,其Reference中就提到了这个方式,我也尝试过,非常的麻烦,而且性能很慢,没记错的话当时大概是06年10月份,用的JDK5,现在不知道有没有改善,我当时还在JE发过新手帖询问过,某大牛说是BEA的虚拟机对其有优化,我也没条件尝试,而且编译期的手脚动太多了,感觉不是很好。

上述几种方式都有明显的缺点,我们追求的好的领域模型的实现就是,领域模型不依赖实现,例如可以注入,易于单元测试,而且其生命周期不依赖特定的环境,还要够简单!难道真是Robbin所说的JAVA不适合Rich Domain Model吗?不见得:

首先,不依赖实现,可以注入,那么要在领域模型中提供接口dao的set方法,允许被注入,这个很明确。

其次,领域模型的生命周期不依赖环境, 不依赖hibernate的构造,不被spring管理等。一般的情况,dao都是无状态的,单例的,那么把领域模型中的依赖的dao设置为static,static是class级别的,和具体的instance没关系,只要在应用初始化的时候给所有的领域模型的class注入一次,整个应用运行的时间周期,领域模型随意怎么new,在哪new,new出来的Domain Object的都可以访问一开始被注入的static的dao,实现了领域模型的生命周期不依赖环境,这样也易于测试,易于MOCK,和目前传统编程方式差别很小,仅仅把dao改为static的。

 

但是spring可以实现对一个class的静态peroperty注入吗?答案是不能(经bottom同学修正,spring是可以静态property注入,但是还是要在所有bean初始化之前注入才行,所以还是要扩展,我在3楼已经回复说明),但是spring是个优美扩展性强的框架,我们只要稍微扩展一下,就可以达到我们的目的。

就拿一个典型的web应用为例,普遍的方式的是在web.xml配置spring的context loader listener和spring的配置文件,这个listener会在应用启动时,创建并初始化spring的上下文,我们需要在所有bean初始化之前注入domain class,那么就在这里找切入点,通过spring的源代码可以发现类AbstractApplicationContext的refresh方法内的finishBeanFactoryInitialization有些重要的东东。

/**
 * Finish the initialization of this context's bean factory,
 * initializing all remaining singleton beans.
*/
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
       // 省略
       // Instantiate all remaining (non-lazy-init) singletons.
        // 这里,会初始化所有的单例
       beanFactory.preInstantiateSingletons();
}


通过观察代码发现,初始化单例其实就是将Spring配置文件中定义的所有不是lazy init的单例初始化了,对一个Bean的初始化就是调用getBean来完成,Spring在调用getBean的时候,会判断如果已经初始化了就直接返回给你Bean的引用,否则就初始化这个Bean再返回给你,很典型的实现LAZY的方式。

我们只需要在这个初始化方法之前,主动去扫描所有的领域模型class,并且知道他们需要注入的Dao,直接getBean获得dao的实例,并且注入进Domain Model的class就完成了,那么我们只需要自己实现一个ConfigurableListableBeanFactory接口,也就是继承DefaultListableBeanFactory(其是ConfigurableListableBeanFactory接口的默认实现), 并且在preInstantiateSingletons方法的时候先注入领域模型,巨简单

private class StaticPropertyInjectSupportListableBeanFactory extends DefaultListableBeanFactory {
       @Override//覆盖父类的方法
       public void preInstantiateSingletons() throws BeansException {
              //先注入所有的领域模型的class
              injectStaticPropertyForBasePackage();
              //再调用父类的该方法去初始化所有的非LAZY单例
              super.preInstantiateSingletons();
       }
}

继续观察代码,finishBeanFactoryInitialization的方法的参数ConfigurableListableBeanFactory,是通过调用的createBeanFactory方法获得的,默认的实现在AbstractRefreshableApplicationContext中有这样一个方法,去创建DefaultListableBeanFactory,而这个方法是protected的,摆明着让我们去复盖扩展其功能,这也是spring易于扩展的体现,我们只要实现一个自己的ApplicaitonContext,覆盖createBeanFactory方法创建我们刚才自己实现的StaticPropertyInjectSupportListableBeanFactory就OK了,代码也巨简单

public class StaticPropertyInjectSupportXmlWebApplicationContext extends XmlWebApplicationContext {
       @Override
       protected DefaultListableBeanFactory createBeanFactory() {
              return new StaticPropertyInjectSupportListableBeanFactory(getInternalParentBeanFactory());
       }
}


实现了自己的ApplicationContext,怎么让Spring启动的时候初始化你的上下文呢?当然可以了,spring提供了扩展的后门,只要在web.xml配置context parameter "contextClass"告诉spring,他就会用你自己的ApplicationContext去初始化了,不配置该参数默认的ApplicationContext实现是XmlWebApplicationContext。

关于自己实现静态property的注入就很简单了,在context parameter里加入个参数configurableBeanBasePackage,获得所有要注入的领域模型的父包,就可以了,如下

<!--加入这两个参数,其余不变 -->
<!--告诉spring用我们自己实现的支持静态bean注入的context -->
<context-param>
        <param-name>contextClass</param-name>
	<param-value>com.norther.sps.StaticPropertyInjectSupportXmlWebApplicationContext</param-value>
</context-param>
<!--注入com.norther包下(包含子包)所有的领域模型的class-->
<context-param>  
	<param-name>configurableBeanBasePackage</param-name>
	<param-value>com.norther</param-value>
</context-param>


这样就OK了,有人会想到这样还是有一点点点点麻烦,用PostProcessorBean或者Spring的容器初始化完毕的事件去实现注入static property不是更爽吗?这也是我最先想到的方式,但是这两种方式的注入的时间太靠后了,那个preInstantiateSingletons方法 已经被调用过了,有些bean已经被初始化了,例如quartz的SchedulerFactoryBean,就有可能你部署的quartz或者其他的 job已经开始跑了,但是领域模型还没有被注入,在那些job里调用领域模型都会空指针,所以一定要在preInstantiateSingletons方法调用前去注入。

 

那么如何辨别哪些是领域模型呢?就利用了spring的Configurable这个annotation去标识这个是需要静态注入的领域模型,然后自己实现了两种自动装配,BY_TYPE和BY_NAME,以及JSR-250的Resource annotation,例子如下

@Configurable
public class Student {
       private Long id;

       @Resource //注入field的名字所代表的bean,
       private static StudentDao studentDao;

       public static void setStudentDao(StudentDao studentDao) {
              Student.studentDao = studentDao;
       }

 

@Configurable
public class Assistant {
       private static StudentDao studentDao;

       @Resource(name = "studentDao") //注入name为studentDao的bean 
       public static void setDao(StudentDao studentDao) {
              Assistant.studentDao = studentDao;
       }
@Configurable(autowire = Autowire.BY_TYPE)//自动装配
public class SchoolMaster {
       private static StudentDao studentDao;

       // by type
       public static void setDao(StudentDao studentDao) {
              SchoolMaster.studentDao = studentDao;
       }

单元测试以及Mock

@Configurable(autowire = Autowire.BY_NAME)//自动装配
public class Teacher {
       private static StudentDao studentDao;

       public static void setStudentDao(StudentDao studentDao) {
              Teacher.studentDao = studentDao;
       }

       public List<Students> getStudents() {
              return studentDao.getByClassId(this.getClassId());
       }

 

 

 StudentDao studentDao = EasyMock.createMock(StudentDao.class);
 EasyMock.expected(studentDao.getByClassId(998877)).andReturn(students);

 Teacher.setStudentDao(studentDao);
 Teacher zhang3 = new Teacher();
 zhang3.setClassId(998877);//设置班级ID
 List<Student> actual = zhang3.getStudents();
 .......


后半部分说了这么多其实都是实现static property的注入,原理很简单,代码也很简单,就4个类,但是我认为这么简单的东西spring之类的框架可以更容易并且实现的更好,也就是说目前的java可以简单的解决领域对象和dao依赖的问题从而实现rich domain model。

 

当然这个代码只是demo性质的,实现的比较简单,只有web方式的实现方法,希望和大家共同讨论。

 

  • spis.zip (12.3 KB)
  • 描述: 源代码,依赖spring 2.5,servlet-api,log4j,测试部分依赖EasyMock,Junit
  • 下载次数: 80
评论
melin 2008-06-04
http://blog.springsource.com/main/2008/01/23/new-improvements-in-domain-object-dependnecy-injection-feature/
与这个篇文章相比有什么不同?其中关键字 transient的使用有点不明白,请指教?
melin 2008-06-04
http://blog.springsource.com/main/2008/01/23/new-improvements-in-domain-object-dependnecy-injection-feature/
与这个篇文章相比有什么不同?其中关键字 transient的使用有点不明白,请指教?
Norther 2008-06-02
nighthawk 写道
恕我眼拙,我在《领域驱动开发》和《pojos in action》当中通篇都找不到domain object 需要依赖dao的证据。


呵呵,拿《pojos in action》来说,第88页,“实现方法”“访问仓库的各种选择”,以及其代码中的注释How to access this?都是说明如何将Repository(也就是Dao)放进到 Domain Object里。
nighthawk 2008-06-02
恕我眼拙,我在《领域驱动开发》和《pojos in action》当中通篇都找不到domain object 需要依赖dao的证据。
pig345 2008-05-23
galaxystar 写道
quaff 写道
galaxystar 写道
如果是分布式数据访问,远程调用?对象是反序列化生成的,那么这个是否只能在服务端进行静态注入?

加上transient


可能没表达清楚,就是说,服务端返回的domain object应该是具有客户端远程调用功能的,这个功能的实现,可以理解为dao。
这个dao需要在服务端将对象返回到客户端后,注入进去(现在是通过AOP这么做的)。
或者服务端就将客户端实现注入进去,但是这种做法,会导致服务端跟客户端的耦合。


这种设计不是这样的,在服务器端的service层就是DomainObject的运行边界(同时也是事务的边界),客户端到服务器端(进程外/远程通信)是通过DTO对象或者一些简单数据(Array,List,Map,String,Integer)的组合,进行交互。
rain2005 2008-05-22
楼主的想法不错,不过domain object不能直接暴露给Action,必须用service加事务脚本,可不可以直接依赖注入service呢?
quaff 2008-05-22
galaxystar 写道
quaff 写道
galaxystar 写道
如果是分布式数据访问,远程调用?对象是反序列化生成的,那么这个是否只能在服务端进行静态注入?

加上transient


可能没表达清楚,就是说,服务端返回的domain object应该是具有客户端远程调用功能的,这个功能的实现,可以理解为dao。
这个dao需要在服务端将对象返回到客户端后,注入进去(现在是通过AOP这么做的)。
或者服务端就将客户端实现注入进去,但是这种做法,会导致服务端跟客户端的耦合。

返回给客户端的Domain Object应该设计成只作为数据载体,dao是没办法也传到客户端的
Norther 2008-05-22
ljl 写道
用一句话概括spring就是:“开发人员需要某一个对象实例的时候可以去取,关于对象的创建和对象之间的依赖关系,就让spring去维护吧”,如果我们再去维护过多的依赖关系,比如说你的领域对象domain object,其中依赖了许多的dao,违背了spring的初衷。
假如你的Teacher,Student...业务逻辑有变化,例如学校要给每一个Teacher,Student建立医保档案,可能又需要在Teacher,Student中加入一个用于处理医保的dao了,不仅违背了OO的思想,也不符合当前的业务需要。


帖子的第一句话就声明了,你这个是质疑domain model,推荐你可以看看以前关于domain model的帖子以及相关的书籍,主要有《企业应用架构模式》,《领域驱动开发》和《pojos in action》,全都有中文版。
ljl 2008-05-22
用一句话概括spring就是:“开发人员需要某一个对象实例的时候可以去取,关于对象的创建和对象之间的依赖关系,就让spring去维护吧”,如果我们再去维护过多的依赖关系,比如说你的领域对象domain object,其中依赖了许多的dao,违背了spring的初衷。
假如你的Teacher,Student...业务逻辑有变化,例如学校要给每一个Teacher,Student建立医保档案,可能又需要在Teacher,Student中加入一个用于处理医保的dao了,不仅违背了OO的思想,也不符合当前的业务需要。
galaxystar 2008-05-21
quaff 写道
galaxystar 写道
如果是分布式数据访问,远程调用?对象是反序列化生成的,那么这个是否只能在服务端进行静态注入?

加上transient


可能没表达清楚,就是说,服务端返回的domain object应该是具有客户端远程调用功能的,这个功能的实现,可以理解为dao。
这个dao需要在服务端将对象返回到客户端后,注入进去(现在是通过AOP这么做的)。
或者服务端就将客户端实现注入进去,但是这种做法,会导致服务端跟客户端的耦合。
quaff 2008-05-21
galaxystar 写道
如果是分布式数据访问,远程调用?对象是反序列化生成的,那么这个是否只能在服务端进行静态注入?

加上transient
galaxystar 2008-05-21
如果是分布式数据访问,远程调用?对象是反序列化生成的,那么这个是否只能在服务端进行静态注入?
jianfeng008cn 2008-05-20
Norther 写道
liuyifan.com 写道
在我看来,这就是静态的hibernate session注入
以前自己防rails的activeRecord就是静态注入一个
hibernatetemplate,操作是挺爽的,但在生产上常
报连接已关闭的问题
后来项目不归我管了,就被人改掉了,还骂我的
程序垃圾。。。


是不一样的,虽然都是静态,但是dao注入和Hibernate Session注入是两码子事,dao是单例的,无状态的,所以才适合static注入,但是hibernate session不是单例的,是有状态的,就不适合static注入。在一般的web应用中,Hibernate Session的生命周期都是open session per request,这个和static的整个应用只有一个是完全不吻合的。

另外你那个Hibernate Templet链接已关闭的问题,肯定是哪个地方没控制好,和Hibernate Templet无关。


但是session虽然不是单例,但是针对每个domain都是只有一个也可以说是domain级别的“单例”了 ,session也可以说是无状态的,因为状态都是外部管理的,在web的环境中都是open session per request,状态也不需要业务代码中进行维护,状态对业务逻辑是没有影响的,如果是跑schedule或者别的调用方式,session就要刻意管理好,才不至于影响代码的正常执行,session的状态还是要带来额外的关注,所以说静态注入的关键是注入的东西是无状态的,单例并不是必要条件(假如我只有一个basedao作为通用的,同样适合静态注入),理解对的吧?
Norther 2008-05-20
liuyifan.com 写道
在我看来,这就是静态的hibernate session注入
以前自己防rails的activeRecord就是静态注入一个
hibernatetemplate,操作是挺爽的,但在生产上常
报连接已关闭的问题
后来项目不归我管了,就被人改掉了,还骂我的
程序垃圾。。。


是不一样的,虽然都是静态,但是dao注入和Hibernate Session注入是两码子事,dao是单例的,无状态的,所以才适合static注入,但是hibernate session不是单例的,是有状态的,就不适合static注入。在一般的web应用中,Hibernate Session的生命周期都是open session per request,这个和static的整个应用只有一个是完全不吻合的。

另外你那个Hibernate Templet链接已关闭的问题,肯定是哪个地方没控制好,和Hibernate Templet无关。
liuyifan.com 2008-05-20
在我看来,这就是静态的hibernate session注入
以前自己防rails的activeRecord就是静态注入一个
hibernatetemplate,操作是挺爽的,但在生产上常
报连接已关闭的问题
后来项目不归我管了,就被人改掉了,还骂我的
程序垃圾。。。
jianfeng008cn 2008-05-20
domain直接归spring管也未尝不可吧? 我现在有一些帮助类就是这么做的


ps:的确想不到好的办法来管理,statefull bean很难这样处理。
Norther 2008-05-19
jianfeng008cn 写道
Norther 写道
jianfeng008cn 写道
Norther 写道
jianfeng008cn 写道
Norther 写道
谢谢楼上同学的纠正,这个当然算静态property注入,但是,在bean的初始化的时候,每个bean初始化的时间是不确定的,仅spring管理的bean有依赖初始化的关系,而我们的domain model是class级别的注入,这个依赖关系,spring无法知道,而只有在这个MethodInvokingFactoryBean被spring初始化的时候才去注入,有可能这个时候,其他调用Domain Object的quartz的job已经再开始跑了,但是他们去执行一些domain object的方法时,他们都还没有被注入,所以必须要要在所有的bean初始化之前去注入,才能保证所有的spring管理的bean去调用domain object时,他们都是被注入过的。


既然你的调用是有依赖的 用spring配置了这些有依赖关系的bean 那么初始化的时候也肯定会根据这个关系生成实例,怎么会和你说的这样呢? 比方说 a 依赖于b 在实例化a的时候 发现b还没有实例化 那肯定会先去实例化b啊,即使lazy了 哪么用的时候肯定要实例化了吧


那肯定了,这是没办法的,用到的那些DAO等于都先初始化了。

在Spring中,你getBean("a"), 他会初始化a,如果a依赖b,spring就会初始化b,很明确。
但是这种情况下,你初始化一个quartz的任务,开始跑,他里面没有依赖a,只是他里面操作的领域模型Student,依赖a了,还是static的,Student不是被spring管理的,spring不会知道这个关系,它不会注入,也不会初始化a(如果他是lazy的)。



“只是他里面操作的领域模型Student,依赖a了,还是static的,Student不是被spring管理的”
你的意思是特殊情况(调用Student而不是student bean)还是普遍情况 ,如果是说Student bean 那也没道理不让spring管理啊, 看你这个方案不就是在应用启动的时候,先初始化所有的dao bean(还是归spring管理的吧?),然后在domain(如Student,不归spring管?)的class级别初始化static的dao 。我理解有问题?



你理解的没任何问题,Student是不被spring管理的,而他依赖的Dao是spring管理的。




我表达的不清楚吧

你说是Student不被spring管理, 意思是你调用了静态方法 而不是调用student bean? 如果是这样能否说明下 为什么不调用student bean去调用Student呢?


你的应用里student能是spring管理的么?所有的Hibernate的entity怎么可能让spring管理,这些对象都是根据业务逻辑控制生命周期的,就是要不被spring管理的domain object要注入被spring管理的bean。
jianfeng008cn 2008-05-19
Norther 写道
jianfeng008cn 写道
Norther 写道
jianfeng008cn 写道
Norther 写道
谢谢楼上同学的纠正,这个当然算静态property注入,但是,在bean的初始化的时候,每个bean初始化的时间是不确定的,仅spring管理的bean有依赖初始化的关系,而我们的domain model是class级别的注入,这个依赖关系,spring无法知道,而只有在这个MethodInvokingFactoryBean被spring初始化的时候才去注入,有可能这个时候,其他调用Domain Object的quartz的job已经再开始跑了,但是他们去执行一些domain object的方法时,他们都还没有被注入,所以必须要要在所有的bean初始化之前去注入,才能保证所有的spring管理的bean去调用domain object时,他们都是被注入过的。


既然你的调用是有依赖的 用spring配置了这些有依赖关系的bean 那么初始化的时候也肯定会根据这个关系生成实例,怎么会和你说的这样呢? 比方说 a 依赖于b 在实例化a的时候 发现b还没有实例化 那肯定会先去实例化b啊,即使lazy了 哪么用的时候肯定要实例化了吧


那肯定了,这是没办法的,用到的那些DAO等于都先初始化了。

在Spring中,你getBean("a"), 他会初始化a,如果a依赖b,spring就会初始化b,很明确。
但是这种情况下,你初始化一个quartz的任务,开始跑,他里面没有依赖a,只是他里面操作的领域模型Student,依赖a了,还是static的,Student不是被spring管理的,spring不会知道这个关系,它不会注入,也不会初始化a(如果他是lazy的)。



“只是他里面操作的领域模型Student,依赖a了,还是static的,Student不是被spring管理的”
你的意思是特殊情况(调用Student而不是student bean)还是普遍情况 ,如果是说Student bean 那也没道理不让spring管理啊, 看你这个方案不就是在应用启动的时候,先初始化所有的dao bean(还是归spring管理的吧?),然后在domain(如Student,不归spring管?)的class级别初始化static的dao 。我理解有问题?



你理解的没任何问题,Student是不被spring管理的,而他依赖的Dao是spring管理的。




我表达的不清楚吧

你说是Student不被spring管理, 意思是你调用了静态方法 而不是调用student bean? 如果是这样能否说明下 为什么不调用student bean去调用Student呢?
Norther 2008-05-17
partech 写道
Feiing 写道
inject dao to domain object, 不也是一种 rich domain object 的实现吗 ?

我前面的Finder就对应DAO啊,因为往数据库里持久的任务我们已经采用另一个Aspect集中处理了,所以这里DAO就退化为往外拿Domain对象的Finder了。



public aspect FinderInitialization {   
    private pointcut getXXXFinder() :    
        get(public static * IDomainObject+.*Finder);   
  
    Object around() : getXXXFinder(){   
        Object finder = proceed();   
        if(finder == null) {   
            //注入   
            ...   
            finder = proceed();   
        }   
        assert finder != null : thisJoinPoint.getSignature().toShortString() + " 返回 null ";   
        return finder;   
    }   
}

public Employee implements IDomainObject{
         public static employeeFinder;      
...
}
    //当如下调用时,employeeFinder将被方面自动注入
     Employee.employeeFinder.findbyCode(employeeCode);
}





我也尝试过Dao退化成finder,但是发现有些批量delete和update的逻辑没处可写-_-! studentFinder.deleteByName("张三"); 这样的代码总觉得很怪,语义有些混乱,就干脆还叫Dao了,其里面也就是find和update的逻辑。
Norther 2008-05-17
pig345 写道
看来相同技术条件(限制)下,路子都有点相似。
07年中的一个小项目,用的也是类似的法子,DomainObject中的EntityManager是static的,配置Spring注入,只不过还是没有DAO,而只有JPA。
研究Spring配置,还真费了不少功夫(工厂套工厂,代理叠代理的,很容易晕,当时搞明白了,现在可是全忘了),不过后来各方面原因(非技术的),改ROR了。



呵呵Dao还是JPA本质上都差不多,Dao再把JPA封装了一层而已,这样和目前传统的编程方式区别不大,更容易被一些保守党接受。
发表评论

提醒: 该博客已发表在公共论坛,博客所有留言会成为论坛回贴,留言请注意遵守论坛发贴规则

您还没有登录,请登录后发表评论

Norther
搜索本博客
博客分类
最近加入圈子
最新评论