貧血,充血模型的解釋以及一些經驗

Coding-lover發表於2015-10-15

為了補大家的遺憾,在此總結下ROBBIN的領域模型的一些觀點和大家的補充,在網站和演講中,robbin將領域模型初步分為4大類:
  

  1. 失血模型
  2. 貧血模型
  3. 充血模型
  4. 脹血模型

那麼讓我們看看究竟有這些領域模型的具體內容,以及他們的優缺點:
  

一、失血模型

失血模型簡單來說,就是domain object只有屬性的getter/setter方法的純資料類,所有的業務邏輯完全由business object來完成(又稱Transaction Script),這種模型下的domain object被Martin Fowler稱之為“貧血的domain object”。下面用舉一個具體的程式碼來說明,程式碼來自Hibernate的caveatemptor,但經過我的改寫:

  • 一個實體類叫做Item,指的是一個拍賣專案
  • 一個DAO介面類叫做ItemDao
  • 一個DAO介面實現類叫做ItemDaoHibernateImpl
  • 一個業務邏輯類叫做ItemManager(或者叫做ItemService)
public class Item implements Serializable { 
     private Long id = null; 
     private int version; 
     private String name; 
     private User seller; 
     private String description; 
     private MonetaryAmount initialPrice; 
     private MonetaryAmount reservePrice; 
     private Date startDate; 
     private Date endDate; 
     private Set categorizedItems = new HashSet(); 
     private Collection bids = new ArrayList(); 
     private Bid successfulBid; 
     private ItemState state; 
     private User approvedBy; 
     private Date approvalDatetime; 
     private Date created = new Date(); 
     //getter/setter方法省略不寫,避免篇幅太長 
}
public interface ItemDao { 
     public Item getItemById(Long id); 
     public Collection findAll(); 
     public void updateItem(Item item); 
}

ItemDao定義持久化操作的介面,用於隔離持久化程式碼。

public class ItemDaoHibernateImpl implements ItemDao extends HibernateDaoSupport { 
     public Item getItemById(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); 
     } 
}

ItemDaoHibernateImpl完成具體的持久化工作,請注意,資料庫資源的獲取和釋放是在ItemDaoHibernateImpl裡面處理的,每個DAO方法呼叫之前開啟Session,DAO方法呼叫之後,關閉Session。(Session放在ThreadLocal中,保證一次呼叫只開啟關閉一次)

public class ItemManager { 
     private ItemDao itemDao; 
     public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;} 
     public Bid loadItemById(Long id) { 
         itemDao.loadItemById(id); 
     } 
     public Collection listAllItems() { 
         return   itemDao.findAll(); 
     } 
     public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, 
                             Bid currentMaxBid, Bid currentMinBid) throws BusinessException { 
             if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) { 
             throw new BusinessException("Bid too low."); 
     } 
     // Auction is active 
     if ( !state.equals(ItemState.ACTIVE) ) 
             throw new BusinessException("Auction is not active yet."); 
     // Auction still valid 
     if ( item.getEndDate().before( new Date() ) ) 
             throw new BusinessException("Can't place new bid, auction already ended."); 
     // Create new Bid 
     Bid newBid = new Bid(bidAmount, item, bidder); 
     // Place bid for this Item 
     item.getBids().add(newBid); 
     itemDao.update(item);      //   呼叫DAO完成持久化操作 
     return newBid; 
     } 
}

事務的管理是在ItemManger這一層完成的,ItemManager實現具體的業務邏輯。除了常見的和CRUD有關的簡單邏輯之外,這裡還有一個placeBid的邏輯,即專案的競標。
以上是一個完整的第一種模型的示例程式碼。在這個示例中,placeBid,loadItemById,findAll等等業務邏輯統統放在ItemManager中實現,而Item只有getter/setter方法。

二、貧血模型

簡單來說,就是domain ojbect包含了不依賴於持久化的領域邏輯,而那些依賴持久化的領域邏輯被分離到Service層。
Service(業務邏輯,事務封裝) –> DAO —> domain object
這也就是Martin Fowler指的rich domain object。
一個帶有業務邏輯的實體類,即domain object是Item。
一個DAO介面ItemDao。
一個DAO實現ItemDaoHibernateImpl。
一個業務邏輯物件ItemManager。

public class Item implements Serializable { 
     //   所有的屬性和getter/setter方法同上,省略 
     public Bid placeBid(User bidder, MonetaryAmount bidAmount, 
                         Bid currentMaxBid, Bid currentMinBid) 
             throws BusinessException { 
             // Check highest bid (can also be a different Strategy (pattern)) 
             if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) { 
                     throw new BusinessException("Bid too low."); 
             } 
             // Auction is active 
             if ( !state.equals(ItemState.ACTIVE) ) 
                     throw new BusinessException("Auction is not active yet."); 
             // Auction still valid 
             if ( this.getEndDate().before( new Date() ) ) 
                     throw new BusinessException("Can't place new bid, auction already ended."); 
             // Create new Bid 
             Bid newBid = new Bid(bidAmount, this, bidder); 
             // Place bid for this Item 
             this.getBids.add(newBid);   // 請注意這一句,透明的進行了持久化,但是不能在這裡呼叫ItemDao,Item不能對ItemDao產生依賴!return newBid; 
     } 
}

競標這個業務邏輯被放入到Item中來。請注意this.getBids.add(newBid); 如果沒有Hibernate或者JDO這種O/R Mapping的支援,我們是無法實現這種透明的持久化行為的。但是請注意,Item裡面不能去呼叫ItemDAO,對ItemDAO產生依賴!

ItemDao和ItemDaoHibernateImpl的程式碼同上,省略。

public class ItemManager { 
     private ItemDao itemDao; 
     public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;} 
     public Bid loadItemById(Long id) { 
         itemDao.loadItemById(id); 
     } 
     public Collection listAllItems() { 
         return   itemDao.findAll(); 
     } 
     public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, 
                             Bid currentMaxBid, Bid currentMinBid) throws BusinessException { 
         item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid); 
         itemDao.update(item);     // 必須顯式的呼叫DAO,保持持久化 
     } 
}

在第二種模型中,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框架之外獨立存在。

這種模型的優點:

  1. 各層單向依賴,結構清楚,易於實現和維護。
  2. 設計簡單易行,底層模型非常穩定。

這種模型的缺點:
  

  1. domain object的部分比較緊密依賴的持久化domain logic被分離到Service層,顯得不夠OO。
  2. Service層過於厚重。

三、充血模型

充血模型和第二種模型差不多,所不同的就是如何劃分業務邏輯,即認為,絕大多業務邏輯都應該被放在domain object裡面(包括持久化邏輯),而Service層應該是很薄的一層,僅僅封裝事務和少量邏輯,不和DAO層打交道。

  Service(事務封裝) —> domain object <—> DAO
  
這種模型就是把第二種模型的domain object和business object合二為一了。所以ItemManager就不需要了,在這種模型下面,只有三個類,他們分別是:

  Item:包含了實體類資訊,也包含了所有的業務邏輯
  ItemDao:持久化DAO介面類
  ItemDaoHibernateImpl:DAO介面的實現類
  由於ItemDao和ItemDaoHibernateImpl和上面完全相同,就省略了。

public class Item implements Serializable { 
    //所有的屬性和getter/setter方法都省略 
    private static ItemDao itemDao; 
     public void setItemDao(ItemDao itemDao) {this.itemDao = itemDao;} 
     public static Item loadItemById(Long id) { 
         return (Item) itemDao.loadItemById(id); 
     } 
     public static Collection findAll() { 
         return (List) itemDao.findAll(); 
     } 
     public Bid placeBid(User bidder, MonetaryAmount bidAmount, 
                     Bid currentMaxBid, Bid currentMinBid) 
     throws BusinessException { 
         // Check highest bid (can also be a different Strategy (pattern)) 
         if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) { 
                 throw new BusinessException("Bid too low."); 
         } 
         // Auction is active 
         if ( !state.equals(ItemState.ACTIVE) ) 
                 throw new BusinessException("Auction is not active yet."); 
         // Auction still valid 
         if ( this.getEndDate().before( new Date() ) ) 
                 throw new BusinessException("Can't place new bid, auction already ended."); 
         // Create new Bid 
         Bid newBid = new Bid(bidAmount, this, bidder); 
         // Place bid for this Item 
         this.addBid(newBid); 
         itemDao.update(this);       //   呼叫DAO進行顯式持久化 
         return newBid; 
     } 
}

在這種模型中,所有的業務邏輯全部都在Item中,事務管理也在Item中實現。

這種模型的優點:

  1. 更加符合OO的原則
  2. Service層很薄,只充當Facade的角色,不和DAO打交道。

這種模型的缺點:
 

  1. DAO和domain object形成了雙向依賴,複雜的雙向依賴會導致很多潛在的問題。
  2. 如何劃分Service層邏輯和domain層邏輯是非常含混的,在實際專案中,由於設計和開發人員的水平差異,可能導致整個結構的混亂無序。
  3. 考慮到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種模型來做。
      

四、脹血模型

基於充血模型的第三個缺點,有同學提出,乾脆取消Service層,只剩下domain object和DAO兩層,在domain object的domain logic上面封裝事務。
  domain object(事務封裝,業務邏輯) <—> DAO
  似乎ruby on rails就是這種模型,他甚至把domain object和DAO都合併了。

該模型優點:

  1. 簡化了分層
  2. 也算符合OO

該模型缺點:

  1. 很多不是domain logic的service邏輯也被強行放入domain object ,引起了domain ojbect模型的不穩定。
  2. domain object暴露給web層過多的資訊,可能引起意想不到的副作用。

評價:
在這四種模型當中,失血模型和脹血模型應該是不被提倡的。而貧血模型和充血模型從技術上來說,都已經是可行的了。但是我個人仍然主張使用貧血模型。其理由:

  1. 參考充血模型第三個缺點,由於暴露給web層程式拿到的還是Service Transaction Script,對於web層程式設計師來說,底層OO意義喪失了。
  2. 參考充血模型第三個缺點,為了事務封裝,Service層要給每個domain logic提供一個過程化封裝,這對於程式設計來說,做了多餘的工作,非常煩瑣。
  3. domain object和DAO的雙向依賴在做大專案中,考慮到團隊成員的水平差異,很容易引入不可預知的潛在bug。
  4. 如何劃分domain logic和service logic的標準是不確定的,往往要根據個人經驗,有些人就是覺得某個業務他更加貼近domain,也有人認為這個業務是貼近service的。由於劃分標準的不確定性,帶來的後果就是實際專案中會產生很多這樣的爭議和糾紛,不同的人會有不同的劃分方法,最後就會造成整個專案的邏輯分層混亂。這不像貧血模型中我提出的按照是否依賴持久化進行劃分,這種標準是非常確定的,不會引起爭議,因此團隊開發中,不會產生此類問題。
  5. 貧血模型的domain object確實不夠rich,但是我們是做專案,不是做研究,好用就行了,管它是不是那麼純的OO呢?其實我不同意firebody認為的貧血模型在設計模型和實現程式碼中有很大跨越的說法。一個設計模型到實現的時候,你直接得到兩個類:一個實體類,一個控制類就行了,沒有什麼跨越。

簡單評價一下:

第一種模型絕大多數人都反對,因此反對理由我也不多講了。但遺憾的是,我觀察到的實際情形是,很多使用Hibernate的公司最後都是這種模型,這裡面有很大的原因是很多公司的技術水平沒有達到這種層次,所以導致了這種失血模型的出現。從這一點來說,Martin Fowler的批評聲音不是太響了,而是太弱了,還需要再繼續吶喊。

第二種模型就是Martin Fowler一直主張的模型,實際上也是我一直在實際專案中採用這種模型。我沒有看過Martin的POEAA,之所以能夠自己摸索到這種模型,也是因為從2002年我已經開始思考這個問題並且尋求解決方案了,但是當時沒有看到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個類分為三層:

  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,業務邏輯層並不需要提供封裝方法,它可以適用於任何場合。

概括說:action做為控制器 ,service面向use case,domain object是中間穩定的一層,dao是作為下層的服務,提供持久化服務,可以被domain object所使用。

針對上面帖子中分析的業務邏輯物件的方法有三類的情況,我們在實際的專案中會遇到一些困擾。主要的困擾就是業務邏輯物件的方法會變動的相當頻繁,並且業務邏輯物件的方法數量會非常龐大。針對這個問題,我所知道的有兩種解決方案,我姑且稱之為第二種模型的兩類變種:

第一類變種就是partech的那種模型,簡單的來說,就是把業務邏輯物件層和DAO層合二為一;第二類變種就是乾脆取消業務邏輯層,把事務控制前推至Web層的Action層來處理,下面分別分析一下兩類變種的優缺點:

第一類變種是合併業務邏輯物件和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提供封裝方法,未能做到徹底的簡化。

總體而言,個人認為這種變種各方面權衡下來,是目前相對最為合理方案,這也是我目前專案中採用的架構

第二種模型的第二類變種就是乾脆取消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的話,就有問題了。合併也意味中不能很好的重用,說到底就是粒度的問題,分得細重用好,但類多、結構複雜、繁瑣。分得粗(乾脆用一個類幹所有的事)重用差,但類少、結構簡單。

轉載自:貧血,充血模型的解釋以及一些經驗

相關文章