说说领域驱动设计和贫血、失血、充血模型

此次想讨论的话题是有关领域驱动设计,和领域驱动设计中使用贫血、失血or充血模型的。在这以前我想讨论下当前不少应用的问题,想起这个话题的原由是由于我在InfoQ上面看到这样一篇文章《Spring Web应用的最大瑕疵》,不得不说,这样的标题至关吸引人(′·ω·`)。内容和主要观点大概是这样的,如今大部分应用Spring框架的Java Web应用都至关关注单一职责原则关注分离原则,可是在此之上却诞生了一些不太好的反模式和设计原则,好比:html

  • 领域模型对象只是用来存储应用的数据。(领域模型使用了贫血模型这种反模式)
  • 业务逻辑位于服务层中,管理域对象的数据。
  • 在服务层中,应用的每一个实体对应一个服务类。

这类设计原则的应用很是普遍,我如今所在的Java Web项目就是使用这样的设计原则进行架构设计的,基本都是常见的三层或多层架构,他们大概是什么样的呢?java

  1. Web层(俗称展示层吧,Presentation Layer):接收用户输入,将数据传至服务层;
  2. 服务层(Service Layer,能够叫Business Logic Layer):事务边界,处理业务逻辑、权限管理与受权,并与存储层通讯;
  3. 存储层(Data access layer):与数据库进行通讯,对数据进行持久化。

可是发现什么没有?问题出在了服务层,他承受了太多的职责,像事务管理、业务逻辑、权限检查等等,这违反了单一职责原则和关注分离原则,而且产生了大量的依赖和循环依赖。当业务复杂度上升时,服务层所包含的代码将会很是庞大和复杂,直接致使了测试成本的上升。 
我这里正好有个例子,在如今的项目中,负责处理保险业务单的核心类中,包含了4000多行代码,它与数据库中某一关键表相关联,引用(注入)了十几个DAO。在数十个各种方法中,能够处理保单、再保、理赔等等各类不一样的业务,同时它还深度依赖于hibernate,不但使用了ORM方法处理数据,甚至还直接用了HQL来获取数据。由于有众多其余服务类与他进行循环引用,项目后期这个庞然大物已经没有人敢轻易改动了,由于谁也不知道他到底都能作什么,重构更是不可能的事。程序员

说了些服务层的坏话,那应该怎么改进呢?web

  • 首先,咱们须要将业务逻辑从服务层移动到领域模型中,这样的好处是,服务层能够只负责应用逻辑(如数据有效性验证、受权检查、开始结束事务等),领域模型能够专门负责其相关的业务逻辑。仍是以以前的保单系统来距离,架构设计时彻底能够针对保单、再保、理赔等多个领域模型进行建模,相关的业务能够分别放到不一样的领域模型中,一些颇有可能重复的业务代码都会被集中到一处,从而下降了复制-粘贴的可能性。
  • 其次,将服务类变得更小,使之只负责单一的职责。文章中有个例子,例如用户帐户的CRUD和其余操做,就能够将其放到两个不一样的服务类中,一个负责帐户的CRUD操做,另一个负责与用户帐户相关的其余操做。

这样就能使服务类变得小巧、松散、可测试了,同时还能下降其余人理解与重用的成本。spring

接下来的问题就是,在实际的项目中,怎样实践这些设计原则呢? 
这里有一篇《领域驱动设计和开发实践》很是值得一看,他所推崇的分层结构和上文所述相似,甚至提出了一些更细节的规则:数据库

  • 服务层须要包含应用逻辑、用户会话的管理,但不能包含领域逻辑、业务逻辑和数据访问逻辑;
  • 领域层(领域对象)应该包含业务逻辑,能够处理与业务相关的会话状态.但做为商业应用的核心,应该具备良好的可移植性,不能对特定框架(如Struts、Hibernate、EJB等)产生依赖

说到这里,终于到了讨论的正题——贫血、失血和充血模型。什么是贫血失血充血模型呢?简单来讲编程

  • 失血模型:模型仅仅包含数据的定义和getter/setter方法,业务逻辑和应用逻辑都放到服务层中。这种类在java中叫POJO,在.NET中叫POCO。
  • 贫血模型:贫血模型中包含了一些业务逻辑,但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。能够看出,贫血模型中的领域对象是不依赖于持久层的。
  • 充血模型:充血模型中包含了全部的业务逻辑,包括依赖于持久层的业务逻辑。因此,使用充血模型的领域层是依赖于持久层,简单表示就是UI层->服务层->领域层<->持久层
  • 胀血模型:胀血模型就是把和业务逻辑不想关的其余应用逻辑(如受权、事务等)都放到领域模型中。我感受胀血模型反而是另一种的失血模型,由于服务层消失了,领域层干了服务层的事,到头来仍是什么都没变。

能够看出来,失血模型和胀血模型都是不可取的,如今的问题是,贫血模型和充血模型哪一个更加好一些。好久好久之前,人们针对这个问题进行了旷日持久的争论,最后仍然没有什么结果。这里有一些帖子可供回味: 
贫血,充血模型的解释以及一些经验 
总结一下最近关于domain object以及相关的讨论设计模式

双方争论的焦点主要在我上面加粗的两句话上,就是领域模型是否要依赖持久层,由于依赖持久层就意味着单元测试的展开要更加困难(没法脱离框架进行测试,原文的讨论中这里专指Hibernate),领域层就更难独立,未来也更难从应用程序中剥离出来,固然好处是业务逻辑没必要混放在不一样的层中,使得单一职责性体现的更好。而支持者(充血模型)认为,只要将持久层抽象出来,便可减小测试的困难性,同时适用充血模型毕竟带来了很多开发上的便利性,除了依赖持久层这一点,拥有更多好处的充血模型仍然值得选择。最后,谁也没能说服谁,关于贫血模型和充血模型的选择,更多的要靠具体的业务场景来决定,并不能说哪种更比哪种好。设计模式这种东西不是向来都没有什么定论么。ruby

我我的则倾向使用充血模型,由于充血模型更加像一个设计完善的系统架构,好在计算机世界里有不少的IOC和DI框架,惟一的缺陷依赖持久层能够经过各类变通的方法绕过,随着技术的进步,一些缺陷也会被慢慢解决。个人思路是这样的:先将持久层抽象为接口,而后经过服务层将持久层注入到领域模型中,这样领域模型仅仅会依赖于持久层的接口。而这个接口,能够利用现有框架的技术进行抽象。举例来讲,Java版Hibernate我了解很少,就以.NET的Entity Framework来讲吧:服务器

如今有这么一个DbContext,你们都懂得,DbContext和DbSet是很是很差Mock的两个类(我就是嫌麻烦而已,高手请无视),里面有两个表,一个叫Animes另外一个叫Users

怎样设计接口才能使它既容易使用又能够方便测试呢?直接提取一个接口?DbSet不容易Mock的问题仍是没有解决吧。

好在咱们有LINQ和IQueryable<T>,随便改造一下,接口就变成了这样:

请注意Query<T>()方法,这个方法返回一个IQueryable<T>的对象,而实现了IQueryable的对象是支持LINQ操做的,也就是说,咱们能够仍然能够将搜索的Expression交给真正的DbContext来作,而这个DbContext只须要简单一句话:

查询时从 from a in db.Anime.AsQueryable() 改为 from a in db.Query<Anime>(),一切都解决了。当你在单元测试中想要返回一个假的数据源的时候,直接让FakeDb.Query<T>()方法返回一个拥有假数据的List<T>.AsQueryable()就能够了。这样就实现了领域层和持久层的解耦,毕竟IQueryable是通用的嘛。

转载自:说说领域驱动设计和贫血、失血、充血模型 

-------------------------------------

https://www.evernote.com/shard/s315/sh/a389cb28-9910-443b-8e30-4d1796dafe9d/2838ff81c0a65b966acda644ebb2300d

贫血,充血模型的解释以及一些经验

为了补你们的遗憾,在此总结下ROBBIN的领域模型的一些观点和你们的补充,在网站和演讲中,robbin将领域模型初步分为4大类:
 1,失血模型
 2,贫血模型
 3,充血模型
 4,胀血模型
 那么让咱们看看究竟有这些领域模型的具体内容,以及他们的优缺点:

1、失血模型

失血模型简单来讲,就是domain object只有属性的getter/setter方法的纯数据类,全部的业务逻辑彻底由business object来完成(又称TransactionScript),这种模型下的domain object被Martin Fowler称之为“贫血的domain object”。下面用举一个具体的代码来讲明,代码来自Hibernate的caveatemptor,但通过个人改写:
一个实体类叫作Item,指的是一个拍卖项目 
一个DAO接口类叫作ItemDao 
一个DAO接口实现类叫作ItemDaoHibernateImpl 
一个业务逻辑类叫作ItemManager(或者叫作ItemService)

java代码:  
 
 

 
  1. public class Item implements Serializable {   
  2.      private Long id = null;   
  3.      private int version;   
  4.      private String name;   
  5.      private User seller;   
  6.      private String description;   
  7.      private MonetaryAmount initialPrice;   
  8.      private MonetaryAmount reservePrice;   
  9.      private Date startDate;   
  10.      private Date endDate;   
  11.      private Set categorizedItems = new HashSet();   
  12.      private Collection bids = new ArrayList();   
  13.      private Bid successfulBid;   
  14.      private ItemState state;   
  15.      private User approvedBy;   
  16.      private Date approvalDatetime;   
  17.      private Date created = new Date();   
  18.      //   getter/setter方法省略不写,避免篇幅太长   
  19. }  

 java代码:  
 
 

 
  1. public interface ItemDao {   
  2.      public Item getItemById(Long id);   
  3.      public Collection findAll();   
  4.      public void updateItem(Item item);   

 ItemDao定义持久化操做的接口,用于隔离持久化代码。

java代码:  
 
 

 
  1. public class ItemDaoHibernateImpl implements ItemDao extends HibernateDaoSupport {   
  2.      public Item getItemById(Long id) {   
  3.          return (Item) getHibernateTemplate().load(Item.class, id);   
  4.      }   
  5.      public Collection findAll() {   
  6.          return (List) getHibernateTemplate().find("from Item");   
  7.      }   
  8.      public void updateItem(Item item) {   
  9.          getHibernateTemplate().update(item);   
  10.      }   

 ItemDaoHibernateImpl完成具体的持久化工做,请注意,数据库资源的获取和释放是在ItemDaoHibernateImpl里面处理的,每一个DAO方法调用以前打开Session,DAO方法调用以后,关闭Session。(Session放在ThreadLocal中,保证一次调用只打开关闭一次)

java代码:
 

 
  1. public class ItemManager {   
  2.      private ItemDao itemDao;   
  3.      public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;}   
  4.      public Bid loadItemById(Long id) {   
  5.          itemDao.loadItemById(id);   
  6.      }   
  7.      public Collection listAllItems() {   
  8.          return   itemDao.findAll();   
  9.      }   
  10.      public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount,   
  11.                              Bid currentMaxBid, Bid currentMinBid) throws BusinessException {   
  12.              if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {   
  13.              throw new BusinessException("Bid too low.");   
  14.      }   
  15.       
  16.       // Auction is active   
  17.      if ( !state.equals(ItemState.ACTIVE) )   
  18.              throw new BusinessException("Auction is not active yet.");   
  19.       
  20.       // Auction still valid   
  21.      if ( item.getEndDate().before( new Date() ) )   
  22.              throw new BusinessException("Can't place new bid, auction already ended.");   
  23.       
  24.       // Create new Bid   
  25.      Bid newBid = new Bid(bidAmount, item, bidder);   
  26.       
  27.       // Place bid for this Item   
  28.      item.getBids().add(newBid);   
  29.      itemDao.update(item);      //   调用DAO完成持久化操做   
  30.      return newBid;   
  31.      }   

 


事务的管理是在ItemManger这一层完成的,ItemManager实现具体的业务逻辑。除了常见的和CRUD有关的简单逻辑以外,这里还有一个placeBid的逻辑,即项目的竞标。

以上是一个完整的第一种模型的示例代码。在这个示例中,placeBid,loadItemById,findAll等等业务逻辑通通放在ItemManager中实现,而Item只有getter/setter方法。
 
2、贫血模型

简单来讲,就是domain ojbect包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。 
Service(业务逻辑,事务封装) --> DAO ---> domain object 
这也就是Martin Fowler指的rich domain object :

一个带有业务逻辑的实体类,即domain object是Item 
一个DAO接口ItemDao 
一个DAO实现ItemDaoHibernateImpl 
一个业务逻辑对象ItemManager

java代码:  
 
 

 
  1. public class Item implements Serializable {   
  2.      //   全部的属性和getter/setter方法同上,省略   
  3.      public Bid placeBid(User bidder, MonetaryAmount bidAmount,   
  4.                          Bid currentMaxBid, Bid currentMinBid)   
  5.              throws BusinessException {   
  6.       
  7.               // Check highest bid (can also be a different Strategy (pattern))   
  8.              if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {   
  9.                      throw new BusinessException("Bid too low.");   
  10.              }   
  11.       
  12.               // Auction is active   
  13.              if ( !state.equals(ItemState.ACTIVE) )   
  14.                      throw new BusinessException("Auction is not active yet.");   
  15.       
  16.               // Auction still valid   
  17.              if ( this.getEndDate().before( new Date() ) )   
  18.                      throw new BusinessException("Can't place new bid, auction already ended.");   
  19.       
  20.               // Create new Bid   
  21.              Bid newBid = new Bid(bidAmount, this, bidder);   
  22.       
  23.               // Place bid for this Item   
  24.              this.getBids.add(newBid);   // 请注意这一句,透明的进行了持久化,可是不能在这里调用ItemDao,Item不能对ItemDao产生  
  25.    
  26. 依赖!   
  27.       
  28.               return newBid;   
  29.      }   

 竞标这个业务逻辑被放入到Item中来。请注意this.getBids.add(newBid); 若是没有Hibernate或者JDO这种O/R Mapping的支持,咱们是没法实现这种透明的持久化行为的。可是请注意,Item里面不能去调用ItemDAO,对ItemDAO产生依赖!

ItemDao和ItemDaoHibernateImpl的代码同上,省略。

java代码: 
 

 
  1. public class ItemManager {   
  2.      private ItemDao itemDao;   
  3.      public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;}   
  4.      public Bid loadItemById(Long id) {   
  5.          itemDao.loadItemById(id);   
  6.      }   
  7.      public Collection listAllItems() {   
  8.          return   itemDao.findAll();   
  9.      }   
  10.      public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount,   
  11.                              Bid currentMaxBid, Bid currentMinBid) throws BusinessException {   
  12.          item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid);   
  13.          itemDao.update(item);     // 必须显式的调用DAO,保持持久化   
  14.      }   

 在第二种模型中,placeBid业务逻辑是放在Item中实现的,而loadItemById和findAll业务逻辑是放在ItemManager中实现的。不过值得注意的是,即便placeBid业务逻辑放在Item中,你仍然须要在ItemManager中简单的封装一层,以保证对placeBid业务逻辑进行事务的管理和持久化的触发。
        这种模型是Martin Fowler所指的真正的domain model。在这种模型中,有三个业务逻辑方法:placeBid,loadItemById和findAll,如今的问题是哪一个逻辑应该放在Item中,哪一个逻辑应该放在ItemManager中。在咱们这个例子中,placeBid放在Item中(可是ItemManager也须要对它进行简单的封装),loadItemById和findAll是放在ItemManager中的。
        切分的原则是什么呢? Rod Johnson提出原则是“case by case”,可重用度高的,和domain object状态密切关联的放在Item中,可重用度低的,和domain object状态没有密切关联的放在ItemManager中。
        通过上面的讨论,如何区分domain logic和business logic,我想提出一个改进的区分原则:domain logic只应该和这一个domain object的实例状态有关,而不该该和一批domain object的状态有关;当你把一个logic放到domain object中之后,这个domain object应该仍然独立于持久层框架以外(Hibernate,JDO),这个domain object仍然能够脱离持久层框架进行单元测试,这个domain object仍然是一个完备的,自包含的,不依赖于外部环境的领域对象,这种状况下,这个logic才是domain logic。 
这里有一个很肯定的原则:logic是否只和这个object的状态有关,若是只和这个object有关,就是domain logic;若是logic是和一批domain object的状态有关,就不是domain logic,而是business logic。
        Item的placeBid这个业务逻辑方法没有显式的对持久化ItemDao接口产生依赖,因此要放在Item中。请注意,若是脱离了Hibernate这个持久化框架,Item这个domain object是能够进行单元测试的,他不依赖于Hibernate的持久化机制。它是一个独立的,可移植的,完整的,自包含的域对象。而loadItemById和findAll这两个业务逻辑方法是必须显式的对持久化ItemDao接口产生依赖,不然这个业务逻辑就没法完成。若是你要把这两个方法放在Item中,那么Item就没法脱离Hibernate框架,没法在Hibernate框架以外独立存在。 
这种模型的优势: 
一、各层单向依赖,结构清楚,易于实现和维护 
二、设计简单易行,底层模型很是稳定 
这种模型的缺点: 
一、domain object的部分比较紧密依赖的持久化domain logic被分离到Service层,显得不够OO 
二、Service层过于厚重

3、充血模型 
        充血模型和第二种模型差很少,所不一样的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在domain object里面(包括持久化逻辑),而Service层应该是很薄的一层,仅仅封装事务和少许逻辑,不和DAO层打交道。 
Service(事务封装) ---> domain object <---> DAO 
这种模型就是把第二种模型的domain object和business object合二为一了。因此ItemManager就不须要了,在这种模型下面,只有三个类,他们分别是:
Item:包含了实体类信息,也包含了全部的业务逻辑 
ItemDao:持久化DAO接口类 
ItemDaoHibernateImpl:DAO接口的实现类

因为ItemDao和ItemDaoHibernateImpl和上面彻底相同,就省略了。

java代码:  
 
 

 
  1. public class Item implements Serializable {   
  2.      //   全部的属性和getter/setter方法都省略   
  3.     private static ItemDao itemDao;   
  4.      public void setItemDao(ItemDao itemDao) {this.itemDao = itemDao;}   
  5.       
  6.       public static Item loadItemById(Long id) {   
  7.          return (Item) itemDao.loadItemById(id);   
  8.      }   
  9.      public static Collection findAll() {   
  10.          return (List) itemDao.findAll();   
  11.      }   
  12.  
  13.      public Bid placeBid(User bidder, MonetaryAmount bidAmount,   
  14.                      Bid currentMaxBid, Bid currentMinBid)   
  15.      throws BusinessException {   
  16.       
  17.           // Check highest bid (can also be a different Strategy (pattern))   
  18.          if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {   
  19.                  throw new BusinessException("Bid too low.");   
  20.          }   
  21.           
  22.           // Auction is active   
  23.          if ( !state.equals(ItemState.ACTIVE) )   
  24.                  throw new BusinessException("Auction is not active yet.");   
  25.           
  26.           // Auction still valid   
  27.          if ( this.getEndDate().before( new Date() ) )   
  28.                  throw new BusinessException("Can't place new bid, auction already ended.");   
  29.           
  30.           // Create new Bid   
  31.          Bid newBid = new Bid(bidAmount, this, bidder);   
  32.           
  33.           // Place bid for this Item   
  34.          this.addBid(newBid);   
  35.          itemDao.update(this);       //   调用DAO进行显式持久化   
  36.          return newBid;   
  37.      }   

 在这种模型中,全部的业务逻辑所有都在Item中,事务管理也在Item中实现。
 这种模型的优势: 
一、更加符合OO的原则 
二、Service层很薄,只充当Facade的角色,不和DAO打交道。 
这种模型的缺点: 
一、DAO和domain object造成了双向依赖,复杂的双向依赖会致使不少潜在的问题。 
二、如何划分Service层逻辑和domain层逻辑是很是含混的,在实际项目中,因为设计和开发人员的水平差别,可能致使整个结构的混乱无序。 
三、考虑到Service层的事务封装特性,Service层必须对全部的domain object的逻辑提供相应的事务封装方法,其结果就是Service彻底重定义一遍全部的domain logic,很是烦琐,并且Service的事务化封装其意义就等于把OO的domain logic转换为过程的Service TransactionScript。该充血模型辛辛苦苦在domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了。
1.事务我是不但愿由Item管理的,而是由容器或更高一层的业务类来管理。
2.若是Item不脱离持久层的管理,如JDO的pm,那么itemDao.update(this); 是不须要的,也就是说Item是在事务过程当中从数据库拿出来的,而且声明周期不超出当前事务的范围。
3.若是Item是脱离持久层,也就是在Item的生命周期超出了事务的范围,那就要必须显示调用update或attach之类的持久化方法的,这种时候就应该是按robbin所说的第2种模型来作。
 
4、胀血模型 
        基于充血模型的第三个缺点,有同窗提出,干脆取消Service层,只剩下domain object和DAO两层,在domain object的domain logic上面封装事务。 
domain object(事务封装,业务逻辑) <---> DAO 
彷佛ruby on rails就是这种模型,他甚至把domain object和DAO都合并了。 
该模型优势: 
一、简化了分层 
二、也算符合OO 
该模型缺点: 
一、不少不是domain logic的service逻辑也被强行放入domain object ,引发了domain ojbect模型的不稳定 
二、domain object暴露给web层过多的信息,可能引发意想不到的反作用。


评价:

        在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来讲,都已是可行的了。可是我我的仍然主张使用贫血模型。其理由:

一、参考充血模型第三个缺点,因为暴露给web层程序拿到的仍是Service Transaction Script,对于web层程序员来讲,底层OO意义丧失了。

二、参考充血模型第三个缺点,为了事务封装,Service层要给每一个domain logic提供一个过程化封装,这对于编程来讲,作了多余的工做,非
 
常烦琐。

三、domain object和DAO的双向依赖在作大项目中,考虑到团队成员的水平差别,很容易引入不可预知的潜在bug。

四、如何划分domain logic和service logic的标准是不肯定的,每每要根据我的经验,有些人就是以为某个业务他更加贴近domain,也有人认
 
为这个业务是贴近service的。因为划分标准的不肯定性,带来的后果就是实际项目中会产生不少这样的争议和纠纷,不一样的人会有不一样的划分
 
方法,最后就会形成整个项目的逻辑分层混乱。这不像贫血模型中我提出的按照是否依赖持久化进行划分,这种标准是很是肯定的,不会引发争议,所以团队开发中,不会产生此类问题。

五、贫血模型的domain object确实不够rich,可是咱们是作项目,不是作研究,好用就好了,管它是否是那么纯的OO呢?其实我不一样意firebody认为的贫血模型在设计模型和实现代码中有很大跨越的说法。一个设计模型到实现的时候,你直接获得两个类:一个实体类,一个控制类就好了,没有什么跨越。

简单评价一下:

第一种模型绝大多数人都反对,所以反对理由我也很少讲了。但遗憾的是,我观察到的实际情形是,不少使用Hibernate的公司最后都是这种模型,这里面有很大的缘由是不少公司的技术水平没有达到这种层次,因此致使了这种贫血模型的出现。从这一点来讲,Martin Fowler的批评声音不是太响了,而是太弱了,还须要再继续呐喊。

第二种模型就是Martin Fowler一直主张的模型,实际上也是我一直在实际项目中采用这种模型。我没有看过Martin的POEAA,之因此可以本身摸索到这种模型,也是由于从02年我已经开始思考这个问题而且寻求解决方案了,可是当时没有看到Hibernate,那时候作的一个小型项目我已经按照这种模型来作了,可是因为没有O/R Mapping的支持,写到后来又不得不所有改为贫血的domain object,项目作完之后再继续找,随后就发现了Hibernate。固然,如今不少人一开始就是用Hibernate作项目,没有经历过我经历的那个阶段。
        不过我以为这种模型仍然不够完美,由于你仍是须要一个业务逻辑层来封装全部的domain logic,这显得很是罗嗦,而且业务逻辑对象的接口也不够稳定。若是不考虑业务逻辑对象的重用性的话(业务逻辑对象的可重用性也不可能好),不少人干脆就去掉了xxxManager这一层,在Web层的Action代码直接调用xxxDao,同时容器事务管理配置到Action这一层上来。Hibernate的caveatemptor就是这样架构的一个典型应用。

第三种模型是我很反对的一种模型,这种模型下面,Domain Object和DAO造成了双向依赖关系,没法脱离框架测试,而且业务逻辑层的服务也和持久层对象的状态耦合到了一块儿,会形成程序的高度的复杂性,不好的灵活性和糟糕的可维护性。也许未来技术进步致使的O/R Mapping管理下的domain object发展到足够的动态持久透明化的话,这种模型才会成为一个理想的选择。就像O/R Mapping的流行使得第二种模型成为了可能Martin Fowler的Domain Model,或者说咱们的第二种模型难道是天衣无缝的吗?固然不是,接下来我就要分析一下它的不足,以及可能的解决办法,而这些都来源于我我的的实践探索。

在第二种模型中,咱们能够清楚的把这4个类分为三层:

一、实体类层,即Item,带有domain logic的domain object 
二、DAO层,即ItemDao和ItemDaoHibernateImpl,抽象持久化操做的接口和实现类 
三、业务逻辑层,即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方法中的这一句: 
java代码:  
 
 

 
  1. 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,以下:

java代码:  
 

 
  1.  class Topic {   
  2.      boolean isAllowReply() {   
  3.          Calendar dueDate = Calendar.getInstance();   
  4.          dueDate.setTime(lastUpdatedTime);   
  5.          dueDate.add(Calendar.DATE, forum.timeToLive);   
  6.       
  7.           Date now = new Date();   
  8.          return now.after(dueDate.getTime());   
  9.      }   

 注意这个isAllowReply方法,他和持久化彻底不发生一丁点关系。在实际的开发中,咱们一样会遇到不少这种不须要持久化的业务逻辑(主要发生在日期运算、数值运算和枚举运算方面),这种domain logic无论脱离不脱离所在的框架,它的行为都是一致的。对于这种domain logic,业务逻辑层并不须要提供封装方法,它能够适用于任何场合。归纳说:action作为控制器 ,service面向use case,domain object是中间稳定的一层,dao是做为下层的服务,提供持久化服务,能够被domain object所使用。

        针对上面帖子中分析的业务逻辑对象的方法有三类的状况,咱们在实际的项目中会遇到一些困扰。主要的困扰就是业务逻辑对象的方法会变更的至关频繁,而且业务逻辑对象的方法数量会很是庞大。针对这个问题,我所知道的有两种解决方案,我姑且称之为第二种模型的两类变种:

第一类变种就是partech的那种模型,简单的来讲,就是把业务逻辑对象层和DAO层合二为一;第二类变种就是干脆取消业务逻辑层,把事务控制前推至Web层的Action层来处理,下面分别分析一下两类变种的优缺点:
 
第一类变种是合并业务逻辑对象和DAO层,这种设计代码简化为3个类,以下所示:

一个domain object:Item(同第二种模型的代码,省略) 
一个业务层接口:ItemManager(合并原来的ItemManager方法签名和ItemDao接口而来) 
一个业务层实现类:ItemManagerHibernateImpl(合并原来的ItemManager方法实现和ItemDaoHibernateImpl)

java代码:  
 

 
  1. public interface ItemManager {   
  2.      public Item loadItemById(Long id);   
  3.      public Collection findAll();   
  4.      public void updateItem(Item item);   
  5.      public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, Bid currentMaxBid, Bid currentMinBid) throws   
  6.  
  7. BusinessException;   

java代码:  
 

 
  1. public class ItemManagerHibernateImpl implements ItemManager extends HibernateDaoSupport {   
  2.      public Item loadItemById(Long id) {   
  3.          return (Item) getHibernateTemplate().load(Item.class, id);   
  4.      }   
  5.      public   Collection findAll() {   
  6.          return (List) getHibernateTemplate().find("from Item");   
  7.      }   
  8.      public void updateItem(Item item) {   
  9.          getHibernateTemplate().update(item);   
  10.      }   
  11.      public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, Bid currentMaxBid, Bid currentMinBid) throws   
  12.  
  13. BusinessException {   
  14.          item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid);   
  15.          updateItem(item);    //   确保持久化item   
  16.      }   

 第二种模型的第一类变种把业务逻辑对象和DAO层合并到了一块儿。 
考虑到典型的web应用中,简单的CRUD操做占据了业务逻辑的绝大多数比例,所以第一类变种的优势是:避免了业务逻辑不得不大量封装DAO接口的问题,简化了软件架构设计,节省了大量的业务层代码量。 
这种方案的缺点是:把DAO接口方法和业务逻辑方法混合到了一块儿,显得职责不够单一化,软件分层结构不够清晰;此外这种方案仍然不得不对隐式依赖持久化的domain logic提供封装方法,未能作到完全的简化。

整体而言,我的认为这种变种各方面权衡下来,是目前相对最为合理方案,这也是我目前项目中采用的架构。

 第二种模型的第二类变种就是干脆取消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模型的三种解决方案中(第二模型,第二模型第一变种,第二模型第二变种),我认为权衡下来,第二模型的第一变种是相对最好的解决方案,不过它仍然有必定的不足,在这里我也但愿你们可以提出更好的解决方案。
 ,partech 提出了 实体控制对象 和 实体对象 两种不一样层次的 Domain Object ,因为 Domain Object 能够依赖于 XXXFinderDAO,所以,也就不存在“大数据量问题”,所以,整个 Domain 体系,对于实际业务表述的更为完整,更为一体化。我很是倾向这种方式。
 通常是这样的顺序: 
Client-->Service-->D Object-->DAO-->DB 
至于哪些该放在哪里,基本有这样的原则:(就是robbin的第二种了) 
DO封装内在的业务逻辑 
Service 封装外在于DO的业务逻辑

固然若是业务逻辑简单或者没有的话也能够: 
Client-->D Object-->DAO-->DB

对于第二种的第一个变种当然是个好办法,但如Robbin所说也有缺陷若是有多个Servcie要调用DAO的话,就有问题了。合并也意味中不能很好的重用。

说到底就是粒度的问题,分得细重用好,但类多、结构复杂、繁琐。分得粗(干脆用一个类干全部的事)重用差,但类少、结构简单。

相关文章
相关标签/搜索