锁定老帖子 主题:贫血就贫血,咂地?
精华帖 (1) :: 良好帖 (0) :: 新手帖 (1) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2005-10-29
然后又看了看马丁同学的反对贫血檄文。 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几乎一模一样,那么似乎就没有必要再迁就业务层,贫血就贫血,挺好。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2005-10-29
关于贫血模型的讨论,上周末几个朋友吃饭,也谈到了这个问题。由于徐昊现在进了 ThoughtWorks工作,所以他透露了一点有意思的内幕。
ThoughtWorks现在做项目使用的框架也是Hibernate+Spring+Webwork,徐昊说在ThoughtWorks内部邮件列表里面这个问题也吵翻了天,大家都在问老马,你批评我们用Hibernate的实体类贫血,那么我们现在该怎么做,才能不贫血?老马到现在也没有给出解答,把这个问题放进了自己的TODOList。 所以别管老马怎么说,我们该贫血照贫血!他老人家信口开河了一把,现在自己都收不了这个场。 |
|
返回顶楼 | |
发表时间:2005-10-29
可能那个Item的例子可能有点太简单了,所以分析来分析去,都找不到所谓rich model的好处。
要是换个BankAccount,那么是account.withdraw(amount)还是service.withdraw(account, amount)呢?这就有点费脑筋了。 但是无论如何,我认为持久层dao返回的一般不应该是rich model对象,因为业务逻辑易变,而且这是个不折不扣的双向依赖。 |
|
返回顶楼 | |
发表时间: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有他做的不到位的地方,需另开贴讨论了 |
|
返回顶楼 | |
发表时间: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,里面出现乐多职责的接口。) 似乎说乐很多题外话,不过根本意思是,我希望更多是由测试驱动出来的结果,而不是预先设计的结果。最近的项目实践让我体会到,更多的单一职责接口是很自然且有效的结果。 |
|
返回顶楼 | |
发表时间: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都会测到所有的函数,没有任何多余? |
|
返回顶楼 | |
发表时间: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()? 这就不是问题? 没看明白什么意思,不过,没什么不可以的,只要测试好就行,具体的代码可以重构,只是不同的设计之间的选择。 |
|
返回顶楼 | |
发表时间: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 |
|
返回顶楼 | |
发表时间:2005-10-29
个人觉得, 关键是没有重复代码.程序结构较好理解.
至于过程式.还是OO,只是达到目的的手段. 贫血与否应该,要看具体的环境. |
|
返回顶楼 | |
发表时间:2005-10-29
单独分一个entity层,更象一个权宜之计。就像我前面分析的,持久层为了避免反向依赖,同时又懒得在持久层和业务层之间进行ItemData到Item的转换。所以为了方便,就弄了这么个层。
事实上我觉得这个贫血模型无可厚非.马丁在这种细枝末节上纠缠挺没劲的.而且,还容易误导大众,比如xiecc那个帖子我觉得就是生生被马丁误导了。 其实像firebody说得,领域对象确实不一定非要贫血,但是也不一定一个不包含多少业务逻辑的对象就是错的。还是要具体分析,根据OO的各种原则,看看业务逻辑放在哪里最好。 |
|
返回顶楼 | |