该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2005-03-24
还好没有去看那几百帖:wink:
robbin的总结非常清楚,我基本上同意第二种,但是不同意robbin对于第二种的划分标准:“看业务方法是否显式的依赖持久化。”这个标准有问题,因为这里面有一种:“业务设计依赖特定技术”的味道。 我认为划分标准应该依据具体语境,也就是说:“从业务方法的含义上来判断,是否能够属于那个Item”也就是“Case By Case”。但是,“Case By Case”的原则也不应该是“可重用度高”,而应该是“含义”! 有空我再写一个具体的例子。 |
|
返回顶楼 | |
发表时间:2005-03-24
感谢Robbin,对这三种模型的阐述是详尽而准确的。基本上,也统一了大家的观点。相信大家在将来的讨论中,也都有一个共同的基础了。
不过对于模型2,在具体实践中,我还是有些疑问的,主要有两点: 1.“DomainModel的持久化与封装”; 2.“大数据量操作”,这个问题是与 引用 需要一个业务逻辑层来封装所有的domain logic 相关的,而且隐约能感觉到也是与 Partech 所说道的第四种模式相关的。
详细的关于这两点的疑问,我会稍后时间,另开贴和大家讨论的。 |
|
返回顶楼 | |
发表时间:2005-03-24
十分感谢 七彩狼 partech Robbin 等人的争论给我的启发.
我基本同意七彩狼 partech的论点! 胡思乱想终于有了结果,嘿嘿! |
|
返回顶楼 | |
发表时间:2005-03-24
Very nice。
Say no more. All my doubt has been answered. |
|
返回顶楼 | |
发表时间:2005-03-24
既然大家都统一了观点,那么就有了一个很好的讨论问题的基础了。Martin Fowler的Domain Model,或者说我们的第二种模型难道是完美无缺的吗?当然不是,接下来我就要分析一下它的不足,以及可能的解决办法,而这些都来源于我个人的实践探索。
在第二种模型中,我们可以清楚的把这4个类分为三层: 1、实体类层,即Item,带有domain logic的domain object 2、DAO层,即ItemDao和ItemDaoHibernateImpl,抽象持久化操作的接口和实现类 3、业务逻辑层,即ItemManager,接受容器事务控制,向Web层提供统一的服务调用 在这三层中我们大家可以看到,domain object和DAO都是非常稳定的层,其实原因也很简单,因为domain object是映射数据库字段的,数据库字段不会频繁变动,所以domain object也相对稳定,而面向数据库持久化编程的DAO层也不过就是CRUD而已,不会有更多的花样,所以也很稳定。 问题就在于这个充当business workflow facade的业务逻辑对象,它的变动是相当频繁的。业务逻辑对象通常都是无状态的、受事务控制的、Singleton类,我们可以考察一下业务逻辑对象都有哪几类业务逻辑方法: 第一类:DAO接口方法的代理,就是上面例子中的loadItemById方法和findAll方法。 ItemManager之所以要代理这种类,目的有两个:向Web层提供统一的服务调用入口点和给持久化方法增加事务控制功能。这两点都很容易理解,你不能既给Web层程序员提供xxxManager,也给他提供xxxDao,所以你需要用xxxManager封装xxxDao,在这里,充当了一个简单代理功能;而事务控制也是持久化方法必须的,事务可能需要跨越多个DAO方法调用,所以必须放在业务逻辑层,而不能放在DAO层。 但是必须看到,对于一个典型的web应用来说,绝大多数的业务逻辑都是简单的CRUD逻辑,所以这种情况下,针对每个DAO方法,xxxManager都需要提供一个对应的封装方法,这不但是非常枯燥的,也是令人感觉非常不好的。 第二类:domain logic的方法代理。就是上面例子中placeBid方法。虽然Item已经有了placeBid方法,但是ItemManager仍然需要封装一下Item的placeBid,然后再提供一个简单封装之后的代理方法。 这和第一种情况类似,其原因也一样,也是为了给Web层提供一个统一的服务调用入口点和给隐式的持久化动作提供事务控制。 同样,和第一种情况一样,针对每个domain logic方法,xxxManager都需要提供一个对应的封装方法,同样是枯燥的,令人不爽的。 第三类:需要多个domain object和DAO参与协作的business workflow。这种情况是业务逻辑对象真正应该完成的职责。 在这个简单的例子中,没有涉及到这种情况,不过大家都可以想像的出来这种应用场景,因此不必举例说明了。 通过上面的分析可以看出,只有第三类业务逻辑方法才是业务逻辑对象真正应该承担的职责,而前两类业务逻辑方法都是“无奈之举”,不得不为之的事情,不但枯燥,而且令人沮丧。 分析完了业务逻辑对象,我们再回头看一下domain object,我们要仔细考察一下domain logic的话,会发现domain logic也分为两类: 第一类:需要持久层框架隐式的实现透明持久化的domain logic,例如Item的placeBid方法中的这一句: this.getBids();.add(newBid);; 上面已经着重提到,虽然这仅仅只是一个Java集合的添加新元素的操作,但是实际上通过事务的控制,会潜在的触发两条SQL:一条是insert一条记录到bid表,一条是更新item表相应的记录。如果我们让Item脱离Hibernate进行单元测试,它就是一个单纯的Java集合操作,如果我们把他加入到Hibernate框架中,他就会潜在的触发两条SQL,这就是隐式的依赖于持久化的domain logic。 特别请注意的一点是:在没有Hibernate/JDO这类可以实现“透明的持久化”工具出现之前,这类domain logic是无法实现的。 对于这一类domain logic,业务逻辑对象必须提供相应的封装方法,以实现事务控制。 第二类:完全不依赖持久化的domain logic,例如readonly例子中的Topic,如下: class Topic { boolean isAllowReply() { Calendar dueDate = Calendar.getInstance(); dueDate.setTime(lastUpdatedTime); dueDate.add(Calendar.DATE, forum.timeToLive); Date now = new Date(); return now.after(dueDate.getTime()); } } 注意这个isAllowReply方法,他和持久化完全不发生一丁点关系。在实际的开发中,我们同样会遇到很多这种不需要持久化的业务逻辑(主要发生在日期运算、数值运算和枚举运算方面),这种domain logic不管脱离不脱离所在的框架,它的行为都是一致的。对于这种domain logic,业务逻辑层并不需要提供封装方法,它可以适用于任何场合。 |
|
返回顶楼 | |
发表时间:2005-03-24
我也同意第二种模型,结合经验谈谈Manger和domain object的职责划分。
附与domain object业务后,带来的问题要保证domain object接口的稳定性。 相应的manager成为domain object上的一层: 1。它的接口会随着use case的变化而变化 2。作为事务边界 3。和与其它服务打交道的。 Domain Object的接口形如: Order { addItem(Item item); } 对应Manager的接口形如: OrderManager { addItem(String orderId,String productId,int amount) } 为什么这样做?假设在action调用Domian object的接口,在需要 保证参数是处于持久状态的对象的情况下,这个假设由action来保证是不妥当的,第二,为什么不在domain object定义接口addItem(String productId,int amount),希望domain object之间都是通过object来打交道,这也是保证domain object接口稳定性的要求。另外domain object绝对不能调用manager,因为manager是在domain object的上层。 2,假设addItem后还要发个邮件,或者写个日志什么的,这显然不属于domain object的职责。放到manager. 概括说: action做为控制器 ,service面向use case,domain object是中间稳定的一层,dao是作为下层的服务,提供持久化服务,可以被domain object所使用。 |
|
返回顶楼 | |
发表时间:2005-03-24
针对上面帖子中分析的业务逻辑对象的方法有三类的情况,我们在实际的项目中会遇到一些困扰。主要的困扰就是业务逻辑对象的方法会变动的相当频繁,并且业务逻辑对象的方法数量会非常庞大。针对这个问题,我所知道的有两种解决方案,我姑且称之为第二种模型的两类变种:
第一类变种就是partech的那种模型,简单的来说,就是把业务逻辑对象层和DAO层合二为一;第二类变种就是干脆取消业务逻辑层,把事务控制前推至Web层的Action层来处理,下面分别分析一下两类变种的优缺点。 |
|
返回顶楼 | |
发表时间:2005-03-24
第一类变种是合并业务逻辑对象和DAO层,这种设计代码简化为3个类,如下所示:
一个domain object:Item(同第二种模型的代码,省略) 一个业务层接口:ItemManager(合并原来的ItemManager方法签名和ItemDao接口而来) 一个业务层实现类:ItemManagerHibernateImpl(合并原来的ItemManager方法实现和ItemDaoHibernateImpl) public interface ItemManager { public Item loadItemById(Long id); public Collection findAll(); public void updateItem(Item item); public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, Bid currentMaxBid, Bid currentMinBid) throws BusinessException; } public class ItemManagerHibernateImpl implements ItemManager extends HibernateDaoSupport { public Item loadItemById(Long id) { return (Item) getHibernateTemplate().load(Item.class, id); } public Collection findAll() { return (List) getHibernateTemplate().find("from Item"); } public void updateItem(Item item) { getHibernateTemplate().update(item); } public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, Bid currentMaxBid, Bid currentMinBid) throws BusinessException { item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid); updateItem(item); // 确保持久化item } } 第二种模型的第一类变种把业务逻辑对象和DAO层合并到了一起。 考虑到典型的web应用中,简单的CRUD操作占据了业务逻辑的绝大多数比例,因此第一类变种的优点是:避免了业务逻辑不得不大量封装DAO接口的问题,简化了软件架构设计,节省了大量的业务层代码量。 这种方案的缺点是:把DAO接口方法和业务逻辑方法混合到了一起,显得职责不够单一化,软件分层结构不够清晰;此外这种方案仍然不得不对隐式依赖持久化的domain logic提供封装方法,未能做到彻底的简化。 总体而言,个人认为这种变种各方面权衡下来,是目前相对最为合理方案,这也是我目前项目中采用的架构。 |
|
返回顶楼 | |
发表时间:2005-03-25
第二种模型的第二类变种就是干脆取消ItemManager,保留原来的Item,ItemDao,ItemDaoHibernateImpl这3个类。在这种情况下把事务控制前推至Web层的Action去控制,具体来说,就是直接对Action的execute()方法进行容器事务声明。
这种方式的优点是:极大的简化了业务逻辑层,避免了业务逻辑对象不得不大量封装DAO接口方法和大量封装domain logic的问题。对于业务逻辑非常简单的项目,采用这种方案是一个非常合适的选择。 这种方式的缺点主要有3个: 1) 由于彻底取消了业务逻辑对象层,对于那些有重用需要的、多个domain object和多个DAO参与的、复杂业务逻辑流程来说,你不得不在Action中一遍又一遍的重复实现这部分代码,效率既低,也不利于软件重用。 2) Web层程序员需要对持久层机制有相当高程度的了解和掌握,必须知道什么时候应该调用什么DAO方法进行必要的持久化。 3) 事务的范围被扩大了。假设你在一个Action中,首先需要插入一条记录,然后再需要查询数据库,显示一个记录列表,对于这种情况,事务的作用范围应该是在插入记录的前后,但是现在扩大到了整个execute执行期间。如果插入动作完毕,查询动作过程中出现通往数据库服务器的网络异常,那么前面的插入动作将回滚,但是实际上我们期望的是插入应该被提交。 总体而言,这种变种的缺陷比较大,只适合在业务逻辑非常简单的小型项目中,值得一提的是Hibernate的caveatemptor就是采用这种变种的架构,大家可以参考一下。 综上所述,在采用Rich Domain Object模型的三种解决方案中(第二模型,第二模型第一变种,第二模型第二变种),我认为权衡下来,第二模型的第一变种是相对最好的解决方案,不过它仍然有一定的不足,在这里我也希望大家能够提出更好的解决方案。 |
|
返回顶楼 | |
发表时间:2005-03-25
robbin 写道 第二种模型的第一类变种把业务逻辑对象和DAO层合并到了一起。 考虑到典型的web应用中,简单的CRUD操作占据了业务逻辑的绝大多数比例,因此第一类变种的优点是:避免了业务逻辑不得不大量封装DAO接口的问题,简化了软件架构设计,节省了大量的业务层代码量。 这种方案的缺点是:把DAO接口方法和业务逻辑方法混合到了一起,显得职责不够单一化,软件分层结构不够清晰;此外这种方案仍然不得不对隐式依赖持久化的domain logic提供封装方法,未能做到彻底的简化。 总体而言,个人认为这种变种各方面权衡下来,是目前相对最为合理方案,这也是我目前项目中采用的架构。 偶 觉得不大好!感觉Robbin的***Manager接口既有DAO方法又有placeBid这样的逻辑,混合太多职责。 偶会这样做: public interface ItemDAO { public Item loadItemById(Long id);; public Collection findAll();; public void updateItem(Item item);; } public interface itemManager{ public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, Bid currentMaxBid, Bid currentMinBid); throws BusinessException; ....// more biz Logic } public class itemManagerImpl implements itemManager{ itemDAO itemDAO; public void setItemDAO(ItemDAO dao);{ itemDAO=dao; } public public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, Bid currentMaxBid, Bid currentMinBid); throws BusinessException;{ item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid);; itemDAO.update(item);; } itemMnagerImpl是暴露给应用层的Service 接口,我们可以通过AOP或者MIXIN等等技术使得这个接口层的实现更为简单,代码甚至可以这样写: public public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, Bid currentMaxBid, Bid currentMinBid); throws BusinessException;{ item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid);; // itemDAO.update(item);; } 其中itemDAO.update(item)可以通过AOP来实现。 |
|
返回顶楼 | |