论坛首页 Java企业应用论坛

领域模型——想说爱你很难

浏览 20067 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-04-29  
在Eric Evan的Domain Driven Design中对领域模型的好处进行了非常深入的探讨。但是在实践中,我们往往发现要想把业务逻辑散布到领域对象的交互中不是那么容易的。其中最大的阻碍就来自于关系数据库。当然这个问题,没有人对他避而不见。Martin Fowler在PoEAA中有将近一半的篇幅在讲如何处理好两者的关系。DDD也提出了如何用Repository来隔离两者。有了前人这么多的研究,现在已经有很多高级的ORM工具,比如Hibernate,来减少体力活了。但是有了Hiberante之后,我们就达到了持久化策略对应用开发者透明的状态了吗?我觉得不是。至少我还要写HSQL来做查询。HSQL与SQL有什么本质区别吗?我看是没有的。本质上来说HSQL是用字符串的描述代替了本来应该用在对象间游走来表达的业务逻辑。对象之间的关系,特别是按照一定路径来游走,是业务逻辑的种种表现形式中比较重要的一种。把这样的逻辑写在HSQL格式的字符串中,我们首先丧失了静态类型给我们带来的类型安全和IDE的重构支持。更为重要的是,我们失去了面向对象给我们带来的好处(什么好处?用SQL有什么坏处?此处需要例证。)。
不用SQL做查询可能么?DDD中提出了一个很有见解的说法:用Repository来加载一个聚合了其他Entity的根Entity,然后其余的对象用对象间游走来获取。这是一个很美好的愿景。落实到实践中,我们会发现能够用对象游走的场合不是很多。一个很重要的原因是,ORM工具表达一对多的关系时,用的是Collection。Collection与关系数据库中的集合的区别就在于,集合可以进一步用关系查询来缩小范围,而Collection要操作的话必须Lazy Load出来。加载所有的关联对象往往是不能接受的,而且用代码来过滤一个Collection也是很笨拙的。所以到头来,大部分时间查询还是靠SQL。
还有其他的选择么?有的。PoEAA和DDD中都提到了如何用拼装的Criteria来表达复杂查询条件。db4o的NativeQuery也与此类似。Hibernate中有一个对应的实现叫DetchedCriteria。不说这些拼装Criteria的接口是否设计得Fluent,写法繁琐都是其次的。最主要的问题有两个,一个是里面需要用字符串来引用属性名,这样就失去了静态类型和重构安全。另外一个就是它不过是字符串操作拼写SQL的高级版本而已,你能然在上面看不到领域对象之间的游走。
由于受到关系数据库的限制,导致领域对象之间无法自由地互相游走。相反,领域模型逐渐退化成表模型,而业务逻辑出现在夹杂着SQL的方法中,而这些方法又被集中到一个个的Service中。很快,DDD是什么,我们已经忘却了。
在查询方面,我比较欣赏RoR的做法。但是仍然不够,RoR在关联多表的时候仍然需要findBySQL。但是RoR鼓励充血模型,已经让程序员感觉到了爽的感觉。这不光是简洁的写法带来了。更重要的是它鼓励了Domain Driven Design。
   发表时间:2007-04-29  
我们目前在项目中正在重构成以ddd为指导思想的domain.遇到了同样的问题。我们目前采用了一些基本的ddd里的构造块。在查询这块,用repository来封闭了所有的对数据库的访问。repository里用了query object来拼装hsql.hsql比起sql来还是有很多好处的。首先,我可以说我的查询是针对domain的而不是针对数据库的。如果数据库真的要改字段名那么我的hsql是不用改变的。其次,hsql里的字段同样可以是多态的。hibernate可以帮你解决这些问题。当然,说到重构和静态安全,确实是一个问题。不过在我们的实际使用中,目前这个问题还不算大(如果说hsql不能保证,那么rails就更不能保证了).
采用Aggregate模式,对我们来讲还是有很多好处的。我们可以把我们的domain模型区分开来,太多的小的类型有时候会把我们自己搞晕掉。现在我们的domain模型比较清晰。另外,由root隔离了对内部对象的直接访问,而由root对象来维护内部的模型,有效的隔离,而且在系统的演化中也利于添加功能。我们现在把一些跟Customer有关的类做为一个aggregate,跟order有关的做一个aggregate.如果要查询就用repository.如果是一个aggregate内部的就用list或者set之类的来完成。

我觉得采用domain driven design,我们逐步地改进domain模型,使其职责越来越丰富,整个team也在逐渐地从transaction script走出来(我知道好多人其实就喜欢transaction script,不做争论)。
0 请登录后投票
   发表时间:2007-04-29  
嗯,你这样的办法已经是当下最佳了。我在想是不是可能让表达对象间关联的Collection能够更智能点,这样就可以直接在上面做查询了。查询的结果是另外一个Collection。如果能够这样的话,SQL表达的多表联合查询的逻辑就可以用操作Collection来表达。这样就可以写出一串话来:Divisions.Employees.FindWhoHoldsPosition(positionsTitledManager).FindWhoCalled("GuoXiao")
比用Query Object会更好看一些。
0 请登录后投票
   发表时间:2007-04-29  
我们现在完全使用DetchedCriteria,没有使用HSQL,当然这可能是因为我们需要做的查询都不是很复杂。只要熟悉了DetchedCriteria,用DetchedCriteria开发的代码维护起来要比用HSQL开发的代码容易很多,这确实要归功于DetchedCriteria的面向对象的特征。

我没有读过DDD这本书,只读过PoEAA,也没有完全读完。主要原因是因为发现在Spring+Hibernate的架构中实现完美的领域模型相当困难。除了小陶说的来自关系数据库的影响外,还有一个主要的影响来自传统的J2EE开发中分层的设计,这个设计集中体现在《J2EE核心模式》这本书中。在基于Spring+Hibernate的开发中也沿用了这个分层的设计。

在Spring2推出之前,Spring IoC框架只能为位于业务层的业务对象提供各种服务,通常不能为持久对象提供服务,因为持久对象是位于持久层的,是从Hibernate中取出的。如果希望实现一个充血的模型(就是将持久对象的属性和与这个持久对象直接相关的方法都放在一个类中)就相当困难,特别是当一些较为复杂的方法会依赖其他业务对象的时候。在这种架构下解决这个问题,通常是一分为二,将属性放在来自Hibernate的持久对象中,而将方法放在某个业务对象中,根据持久对象的类型来动态实例化相应的业务对象。

例如:有一个User的领域对象,需要将这个User对象分成两部分:UserEntity(仅包括属性)、UserOperation(包括操作这些属性的方法)。一个类需要分成两个类,才能解决这个问题。

这样做当然很不舒服。我曾经问过moxie,他说你们在ThoughtWorks是使用Hibernate的interceptor来为持久对象提供IoC服务的。这种方法我尝试过,开发起来仍然感到很不舒服。IoC服务可以采用这种方式解决掉,但是还有AOP服务、声明式事务服务等服务仍然享受不到。

到了Spring2推出后,Spring可以使用AspectJ来为持久对象提供全面的服务,似乎很好地解决了这个问题。但是我没有尝试过,也不清楚难度如何,也没有看到有谁展示过具体的做法。

前些天Gavin King来到上海,acdc问过Gavin这个问题,就是是否应该将属性与操作这些属性的方法合在一起构造一个充血的模型,我听到Gavin对此抱有否定的态度。
所以在Spring+Hibernate的架构中做开发,最好是基于传统的方式来做开发,而PoEAA和DDD中的内容,参考的价值更大一些。毕竟得到一个稳定可靠的应用的重要性要比得到一个完美的设计的重要性更大些。
0 请登录后投票
   发表时间:2007-04-29  
taowen 写道
嗯,你这样的办法已经是当下最佳了。我在想是不是可能让表达对象间关联的Collection能够更智能点,这样就可以直接在上面做查询了。查询的结果是另外一个Collection。如果能够这样的话,SQL表达的多表联合查询的逻辑就可以用操作Collection来表达。这样就可以写出一串话来:Divisions.Employees.FindWhoHoldsPosition(positionsTitledManager).FindWhoCalled("GuoXiao")
比用Query Object会更好看一些。


rails的ActiveRecord就可以很好的进行这样集合运算的导航,例如你刚才的例子:

class Division < ActiveRecord::Base
  has_and_belongs_to_many :employees do
    def find_who_holds_positions(position_title)
      find_by_position_title(position_title)
    end
  end
end


我的观点是dlee是一样的,在常规的Spring/Hibernate,甚至EJB3架构下,不要搞什么DDD,真的不适合。
0 请登录后投票
   发表时间:2007-04-29  
C# 3里面实现了LINQ framework,语言本身直接支持查询,java估计迟早也会加入,那时会有新的手段来解决这个问题。

目前的技术手段,只能靠AspectJ了,非主流做法,值得研究,也许鼓捣出下个流行框架也说不定。
0 请登录后投票
   发表时间:2007-04-29  
taowen 写道
嗯,你这样的办法已经是当下最佳了。我在想是不是可能让表达对象间关联的Collection能够更智能点,这样就可以直接在上面做查询了。查询的结果是另外一个Collection。如果能够这样的话,SQL表达的多表联合查询的逻辑就可以用操作Collection来表达。这样就可以写出一串话来:Divisions.Employees.FindWhoHoldsPosition(positionsTitledManager).FindWhoCalled("GuoXiao")
比用Query Object会更好看一些。


为什么?好看,简单些?
你这不是要把数据全部读到内存里去吗,数据库本来就是设计出要处理海量数据的,
所以不能回避,如果数据量大
1)内存够吗?
2)从Collection搜索速度有保证(或优化)吗?
3)你只是写了一个简单条件搜索的例子,如果你要between, like之类,不是又要添加这些接口吗?
   这样,简单事情不是又可能慢慢的变复杂了,失去了这样做的初衷吗?
0 请登录后投票
   发表时间:2007-04-29  
dlee 写道

......

在Spring2推出之前,Spring IoC框架只能为位于业务层的业务对象提供各种服务,通常不能为持久对象提供服务,因为持久对象是位于持久层的,是从Hibernate中取出的。如果希望实现一个充血的模型(就是将持久对象的属性和与这个持久对象直接相关的方法都放在一个类中)就相当困难,特别是当一些较为复杂的方法会依赖其他业务对象的时候。在这种架构下解决这个问题,通常是一分为二,将属性放在来自Hibernate的持久对象中,而将方法放在某个业务对象中,根据持久对象的类型来动态实例化相应的业务对象。

例如:有一个User的领域对象,需要将这个User对象分成两部分:UserEntity(仅包括属性)、UserOperation(包括操作这些属性的方法)。一个类需要分成两个类,才能解决这个问题。

这样做当然很不舒服。我曾经问过moxie,他说你们在ThoughtWorks是使用Hibernate的interceptor来为持久对象提供IoC服务的。这种方法我尝试过,开发起来仍然感到很不舒服。IoC服务可以采用这种方式解决掉,但是还有AOP服务、声明式事务服务等服务仍然享受不到。
........

不赞成将持久对象的属性和与这个持久对象直接相关的方法都放在一个类中。
可以用新实现业务逻辑,继承那个PO,然后用那个新类来工作,把PO扔到一边去,不是很好吗?
为什么不舒服?
例如:
UserOperation extends UserEntity {
    getFemaleCount() {

     // write code.

    }
}

这样不是很好吗?有什么不舒服的地方?
0 请登录后投票
   发表时间:2007-04-29  
lihy70 写道
这样不是很好吗?有什么不舒服的地方?

这样还是要配置在两个地方,UserEntity要配置在Hibernate中(通过mapping文件),UserOperation要配置在Spring IoC容器中。

客户端的代码以领域模型的写法为:
User user = createUser();  //通过Spring代理实现的工厂方法,从Spring IoC容器中获取一个prototype对象。
                           //User是UserEntity和UserOperation共同实现的接口。
user.setId(userId);
user.setXXX(XXX);
user.setYYY(YYY)
user.save();

或者:
User user = createUser();
user.setId(userId);
user.get();  //要执行一下这个操作将持久对象从数据库加载进来
user.setXXX(XXX);
user.setYYY(YYY)
user.update();

但是多出来了很多的类,有多少个PO类,就会多出来多少个对应的业务类。另外你还需要在Spring IoC容器中配置这些业务类,并且配置使用这些业务类的其他业务对象(就是这些业务类的“客户端代码”)中的工厂方法。

按照传统的方式的写法为:
User user = userDao.get(userId);
user.setXXX(XXX);
user.setYYY(YYY)
userDao.save(user);

或者:
User user = userDao.get(userId);
user.setXXX(XXX);
user.setYYY(YYY)
userDao.update(user);

不需要另外为每个PO类创建对应的业务类。
传统的写法确实是事务脚本,但是领域模型在这里并没有显示出足够的优势来,带来的麻烦要大于带来的好处。
0 请登录后投票
   发表时间:2007-04-29  
DDD可以说是一种指导思想吧,在系统开发的时候,可以借鉴一些DDD的思想,但真正在做的时候,并不一定非要DDD。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics