說說領域驅動設計和貧血、失血、充血模型

此次想討論的話題是有關領域驅動設計,和領域驅動設計中使用貧血、失血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的話,就有問題了。合併也意味中不能很好的重用。

說到底就是粒度的問題,分得細重用好,但類多、結構複雜、繁瑣。分得粗(乾脆用一個類幹全部的事)重用差,但類少、結構簡單。

相關文章
相關標籤/搜索