论坛首页 Java企业应用论坛

贫血就贫血,咂地?

浏览 53810 次
精华帖 (1) :: 良好帖 (0) :: 新手帖 (1) :: 隐藏帖 (0)
作者 正文
   发表时间:2005-10-29  
被hostler提起了兴趣。翻看了那个吵翻了天的domain model讨论。
然后又看了看马丁同学的反对贫血檄文。
http://www.martinfowler.com/bliki/AnemicDomainModel.html

说实话,虽然我并不对只有getter/setter的pojo多感冒,但是对马丁的论述也没觉得服气。

马丁的最大的论点似乎是:
分离数据和行为的方法不是OO,而是面向过程,所以该打倒!

这种上来就往意识形态上靠,只问姓资还是姓社的论调真让我惊讶。要说马丁怎么也是在大众面前混了这么多年,靠着笔头和舌头混饭吃的主,怎么能这么菜?

哦,item.save()就是OO,manager.save(item)就不是OO?

robbin举的那个placeBid的例子,那么应该item.placeBid(user, amount)是oo呢?还是user.placeBid(item, amount)是oo呢?还是new Bid(user, big, amount).place()算oo呢?

其实这根本就是一个伪问题。OO与否根本不是这么看的。而马丁就拿这么个似是而非的逻辑糊弄人玩儿?

oo的关键在管理职责,在抽象,在封装,该分开的就要分开,本身具备强耦合的就要封装在一起。而不是说数据就必然不能和行为分开!

OO为什么强调数据和行为封装在一起?那是因为这两者之间有很强的耦合。在domain model这里面是不是这种情况呢?我想应该是具体情况具体分析,而不是宁要社会主义草,不要资本主义苗吧?


现在,我们来分析一下一个简单的场景。借用robbin的例子。也是一个Item, 支持getById, update, findAll()和placeBig()。

我们先从业务层下手。

其实,面向对象分析最忌讳的就是吃着碗里的,看着锅里的。既然在业务层,我们就要集中精力光想业务层要完成的任务,暂时忘掉什么dao,什么持久层吧!

那么,先定义接口:
interface Service{
  Item getItemById(int id);;
  Collection findAllItem();;
  ...
}

接下来是决定怎么搞update和placeBid。这两个东西是放在Service里面还是放在Item里面呢?
不要考虑持久层,就让我们以一个根本不知道持久化为何物的BA的眼光看一看吧。

update()似乎可以放在Item中,毕竟item.update()看着挺爽。(不知道马丁对OO的定义是不是就是“我看着爽”啊?)

不过仔细分析,如果Item.update()的话,那么意味着Item对象必须通过某种方法通知Service对象(最简单的,假设我们就把对象存在内存中)。

或者item要直接依赖Service,或者两者都要依赖同一个第三方对象。否则item.update()就无法影响service.getItemById()的结果。

也就是说,一个update(),我们要同时通过至少两个对象才能搞定。这种隐含的依赖关系不是很好,它是一种坏味道的信号, 说明我们把同一件事情分给了两个对象来做了。

而如果把update放在Service中,那么,getById()和updateItem()这两个互相有逻辑耦合关系的方法放在一处了。这样,我们大可以把Item做成一个没有额外依赖的value object。不用担心生命期,不用担心序列化。

另外一个支持把update/save放在service里的理由是,我可以这样做:
Service rdb_service = ...;
Service xml_service = ...;
Service mem_service = ...;
Item item = ...;
rdb_service.update(item);;
xml_service.uupdate(item);;

同样的item我可以往不同的service里面去update。
而如果是item.update(),没戏,它从rdb_service来的,就只能会数据库,从xml_service来的,就只能更新xml。灵活性无谓地丧失了。

placeBid()呢,还是前面的那个问题,是user.placeBid()还是item.placeBid(),还是bid.place()?似乎都有点道理。

不过同样的耦合关系分析表明,placeBid()还是放在service中较好,因为Service里面也许还有findBid()这种动作呢?

分析来,分析去,我们就把大部分的业务逻辑放到了Service。而Item似乎只能是一个get/set了。

其实,也不是。如果有些业务逻辑是完全独立在Item这个概念下面的,而不会要求对Service对象有什么依赖的话,其实是可以放在Item里面的。当然,这个例子里面我们没有看到这样的需求。

先总结一下,不是说Item就必须是一个贫血对象,不能有业务逻辑。而是说具体业务逻辑要具体分析。并不是说item.f()就一定比service.f(item)更OO。


下面,分析持久层。
肯定是有一个ItemDao了:

interface ItemDao{
  Item findById(int id);;
  void update(Item item);;
}


这里面有一个疑问。这个findById()返回的Item是不是上面业务层的那个Item呢?理想情况应该不是。为什么?
1。业务层的实现肯定会依赖持久层,你这里再弄个反方向依赖,坏味道。
2。如果业务层的Item定义了一些业务逻辑,那么,这种逻辑很难说不会有多于一种的实现,选取哪个实现,甚至在动态切换不同实现都是业务层的考虑。那么,在dao.findById()的时候,我们职责所在必须创建一个对象,也就必须选择某个具体实现,不管这个选择是直接的静态依赖那个实现类,还是通过service locator来动态找到那个实现类。这里面的依赖无论如何也无法避免。

基于这个分析,贫血的domain model似乎就不显得那么荒谬了。它的哲学可以这样理解:
1。双向依赖肯定不好。所以我单独弄一个entities层。让业务和持久层都单向依赖它。
2。既然业务层的rich domain object的具体实现我在持久层不知道,我不如不管这个实现,只在持久层返回纯粹数据。

以上两个考虑合二为一就成了贫血模型。代价是,业务层的api可能会不那么自然。
而如果坚持保持业务层的对象模型,就必须要在持久层单独弄出一套纯数据对象。然后让业务层把这写entity对象转换为rich domain model。

后一种考虑更纯粹,业务层和持久层的灵活性都被照顾到了。但是代价也不小,我们要在业务层有Item,在持久层还要有ItemData。持久层变成这样:
interface ItemDao{
  ItemData findById(int id);;
  void update(ItemData item);;
}


具体怎么选择,我想更多的取决于你的rich domain object到底有多rich。在robbin的这个例子里,Item和ItemData几乎一模一样,那么似乎就没有必要再迁就业务层,贫血就贫血,挺好。
   发表时间:2005-10-29  
关于贫血模型的讨论,上周末几个朋友吃饭,也谈到了这个问题。由于徐昊现在进了 ThoughtWorks工作,所以他透露了一点有意思的内幕。

ThoughtWorks现在做项目使用的框架也是Hibernate+Spring+Webwork,徐昊说在ThoughtWorks内部邮件列表里面这个问题也吵翻了天,大家都在问老马,你批评我们用Hibernate的实体类贫血,那么我们现在该怎么做,才能不贫血?老马到现在也没有给出解答,把这个问题放进了自己的TODOList。

所以别管老马怎么说,我们该贫血照贫血!他老人家信口开河了一把,现在自己都收不了这个场。
0 请登录后投票
   发表时间:2005-10-29  
可能那个Item的例子可能有点太简单了,所以分析来分析去,都找不到所谓rich model的好处。

要是换个BankAccount,那么是account.withdraw(amount)还是service.withdraw(account, amount)呢?这就有点费脑筋了。

但是无论如何,我认为持久层dao返回的一般不应该是rich model对象,因为业务逻辑易变,而且这是个不折不扣的双向依赖。
0 请登录后投票
   发表时间:2005-10-29  
ajoo 写道
可能那个Item的例子可能有点太简单了,所以分析来分析去,都找不到所谓rich model的好处。

要是换个BankAccount,那么是account.withdraw(amount)还是service.withdraw(account, amount)呢?这就有点费脑筋了。

但是无论如何,我认为持久层dao返回的一般不应该是rich model对象,因为业务逻辑易变,而且这是个不折不扣的双向依赖。


贫血不贫血这个问题也无所谓,Matin同志酒精考验,火眼精精,哪能看不到双向依赖的问题,此问题被Matin同志用了一招Separated Interface进行化解

Martin 同志立论依据倒也非
引用
分离数据和行为的方法不是OO,而是面向过程,所以该打倒!
这么简单,POEAA就有那个“收入确认”体现贫富差距的例子,ajoo不妨研究研究。

Robbin说道用Hibernate做rich Model有些困难,其实Hibernate有他做的不到位的地方,需另开贴讨论了
0 请登录后投票
   发表时间:2005-10-29  
不管是哪种,在开发时有利于测试的编写就趋向哪种。
搂住的接口:
interface Service{
  Item getItemById(int id);
  Collection findAllItem();
  ...
}

interface ItemDao{
  Item findById(int id);
  void update(Item item);
}

实际上从测试驱动的角度并不实用,比如Service,通常一次只会调用其中一个方法(当然,也可能会都用到,但是毕竟是少数,即使是要使用多个接口也不是问题),如果一个unit只依赖其中getItemById,那么将这个Service暴露给这个unit就会使这个unit依赖不必要的接口,在为目标unit的测试构造Service的mock时就会需要去处理不必要的逻辑(不允许或者说不应该访问某些接口方法),
很多人喜欢mock工具(比如jmock、easymock),用mock工具做mock时根本就会忽略那些使用不到的接口(因为只mock对getItemById的操作),但是实际上mock 工具只是将这个作为乐默认的初始化,就是不允许访问没有定义mock的其它方法,
通常我优先考虑使用Self shunt测试模式,因此过多的接口依赖让人非常不爽,多出乐不必要的测试逻辑。
所以,喜欢上乐command模式及其变种,它让目标测试更简单清晰(也许应该说是单一职责的接口更好)。

(最近经常和同事讨论使用mock工具做mock,由于mock工具导致开发过程忽略多职责的接口,让我更加肯定应该尽量不去使用它,除非实在没办法,比如使用别人开发的api,里面出现乐多职责的接口。)

似乎说乐很多题外话,不过根本意思是,我希望更多是由测试驱动出来的结果,而不是预先设计的结果。最近的项目实践让我体会到,更多的单一职责接口是很自然且有效的结果。
0 请登录后投票
   发表时间:2005-10-29  
引用
比如Service,通常一次只会调用其中一个方法(当然,也可能会都用到,但是毕竟是少数,即使是要使用多个接口也不是问题),如果一个unit只依赖其中getItemById,那么将这个Service暴露给这个unit就会使这个unit依赖不必要的接口,在为目标unit的测试构造Service的mock时就会需要去处理不必要的逻辑(不允许或者说不应该访问某些接口方法),


有点鸡蛋里挑骨头了。我不知道你的所有test case是不是都依赖最小接口的。

就说你把逻辑集中在Item里面,架设item里面有update()和placeBid(),那么你那个测试update()的test case里面不是也依赖了不必要的placeBid()?
这就不是问题?

要说java.sql.Connection有那么多函数,是不是sun写test case的时候每个case都会测到所有的函数,没有任何多余?
0 请登录后投票
   发表时间:2005-10-29  
ajoo 写道
引用
比如Service,通常一次只会调用其中一个方法(当然,也可能会都用到,但是毕竟是少数,即使是要使用多个接口也不是问题),如果一个unit只依赖其中getItemById,那么将这个Service暴露给这个unit就会使这个unit依赖不必要的接口,在为目标unit的测试构造Service的mock时就会需要去处理不必要的逻辑(不允许或者说不应该访问某些接口方法),


有点鸡蛋里挑骨头了。我不知道你的所有test case是不是都依赖最小接口的。

就说你把逻辑集中在Item里面,架设item里面有update()和placeBid(),那么你那个测试update()的test case里面不是也依赖了不必要的placeBid()?
这就不是问题?

要说java.sql.Connection有那么多函数,是不是sun写test case的时候每个case都会测到所有的函数,没有任何多余?


是有点较真,不过,那只是我最近的感想而已,多职责的接口确实让人不爽。需要声明的是,我不是说test case依赖最小接口,而是目标unit依赖最小接口,由于我使用test case扩展目标unit依赖的接口来做self shunt,所以一旦发现这个接口不是目标unit的最小依赖接口时,就会让我做多余的事情,让代码显得复杂,所以不爽。

至于:
引用

就说你把逻辑集中在Item里面,架设item里面有update()和placeBid(),那么你那个测试update()的test case里面不是也依赖了不必要的placeBid()?
这就不是问题?

没看明白什么意思,不过,没什么不可以的,只要测试好就行,具体的代码可以重构,只是不同的设计之间的选择。
0 请登录后投票
   发表时间:2005-10-29  
这个问题太难讨论了,因为即使有相同的理论,但是对于这个理论每一个人都会有不同的理解。

把行为封装到对象上,没有任何人会对此有异议。
然而,怎么封装到对象则是仁者见仁,智者见智,
回到Ajoo所说的正题上来,Service包含业务逻辑,Service本身已经富含业务逻辑的领域模型的一部分,对着已经是“非贫血”的领域模型的东西讨论 是否贫血似乎已经陷入一个讨论1已经是1的一个怪圈。
反过来说,既然行为跟数据可以分离,那么如果行为和数据结合在一起又肯定是错的吗? 把行为甚至是复杂的业务逻辑构建在实体对象上又有何错误呢?
这两者本没有任何错误,其实争论的焦点在于 这两种方式下得出的领域模型,哪一种更加好?哪一种更加合乎易重用的原则. 哪一种更合乎易封装的原则。 哪一种更合乎OO的表现的原则。

可以回到Martin fowler的理论上来,我为他辩护并不是因为他是大师,仅仅根据我实施具体项目的实际经验而来:
从需求分析--〉概念模型建立---〉细化设计阶段的领域模型的设计这么一个过程,我们分析的一般都是概念化的对象,这些概念化对象我们需要根据具体的需求赋予它丰富的行为,大家注意,此时我们得出的是一种很自然的与技术无关的领域模型。
然而,在实际实现阶段的时候,如果硬是要把这些模型分为实体层和Service层,那么设计和实现的领域模型之间将会有很大一个距离要跨越。

正如我一直想表述的一样,硬生生的把领域模型分为:Service layer+Entity larer
这个是很Action script的做法。
然而,在大多数的企业应用中,这样的做法是最容易理解和实施的,因为大家都习惯这种方式,OO反而仅仅口头上说说而已。
然而,自己不去揭开这个面沙,又怎么能够体味真正的OO的快感呢?
更具体深入的讨论请看这里:
http://forum.iteye.com/viewtopic.php?t=15973
0 请登录后投票
   发表时间:2005-10-29  
个人觉得, 关键是没有重复代码.程序结构较好理解.
至于过程式.还是OO,只是达到目的的手段.
贫血与否应该,要看具体的环境.
0 请登录后投票
   发表时间:2005-10-29  
单独分一个entity层,更象一个权宜之计。就像我前面分析的,持久层为了避免反向依赖,同时又懒得在持久层和业务层之间进行ItemData到Item的转换。所以为了方便,就弄了这么个层。

事实上我觉得这个贫血模型无可厚非.马丁在这种细枝末节上纠缠挺没劲的.而且,还容易误导大众,比如xiecc那个帖子我觉得就是生生被马丁误导了。

其实像firebody说得,领域对象确实不一定非要贫血,但是也不一定一个不包含多少业务逻辑的对象就是错的。还是要具体分析,根据OO的各种原则,看看业务逻辑放在哪里最好。
0 请登录后投票
论坛首页 Java企业应用版

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