在最近的圍繞domain object的討論中浮現出來了三種模型,(還有一些其他的旁枝,不一一分析了),經過一番討論,各種問題逐漸清晰起來,在這里我試圖做一個總結,便于大家了解和掌握。
第一種模型:只有getter/setter方法的純數據類,所有的業務邏輯完全由business object來完成(又稱TransactionScript),這種模型下的domain object被Martin Fowler稱之為“貧血的domain object”。
下面用舉一個具體的代碼來說明,代碼來自Hibernate的caveatemptor,但經過我的改寫:
一個實體類叫做Item,指的是一個拍賣項目
一個DAO接口類叫做ItemDao
一個DAO接口實現類叫做ItemDaoHibernateImpl
一個業務邏輯類叫做ItemManager(或者叫做ItemService)
java代碼: |
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方法省略不寫,避免篇幅太長 }
|
java代碼: |
public interface ItemDao { public Item getItemById(Long id); public Collection findAll(); public void updateItem(Item item); }
|
ItemDao定義持久化操作的接口,用于隔離持久化代碼。
java代碼: |
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中,保證一次調用只打開關閉一次)
java代碼: |
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方法。
第二種模型,也就是Martin Fowler指的rich domain object是下面這樣子的:
一個帶有業務邏輯的實體類,即domain object是Item
一個DAO接口ItemDao
一個DAO實現ItemDaoHibernateImpl
一個業務邏輯對象ItemManager
java代碼: |
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里面不能去調用
java代碼: |
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中。
我提出的原則是:看業務方法是否顯式的依賴持久化。
Item的placeBid這個業務邏輯方法沒有顯式的對持久化ItemDao接口產生依賴,所以要放在Item中。請注意,如果脫離了Hibernate這個持久化框架,Item這個domain object是可以進行單元測試的,他不依賴于Hibernate的持久化機制。它是一個獨立的,可移植的,完整的,自包含的域對象。
而loadItemById和findAll這兩個業務邏輯方法是必須顯式的對持久化ItemDao接口產生依賴,否則這個業務邏輯就無法完成。如果你要把這兩個方法放在Item中,那么Item就無法脫離Hibernate框架,無法在Hibernate框架之外獨立存在。
第三種模型印象中好像是firebody或者是Archie提出的(也有可能不是,記不清楚了),簡單的來說,這種模型就是把第二種模型的domain object和business object合二為一了。所以ItemManager就不需要了,在這種模型下面,只有三個類,他們分別是:
Item:包含了實體類信息,也包含了所有的業務邏輯
ItemDao:持久化DAO接口類
ItemDaoHibernateImpl:DAO接口的實現類
由于ItemDao和ItemDaoHibernateImpl和上面完全相同,就省略了。
java代碼: |
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中實現。
在上面三種模型之外,還有很多這三種模型的變種,例如partech的模型就是把第二種模型中的DAO和Manager三個類合并為一個類后形成的模型;例如frain....(id很長記不住)的模型就是把第三種模型的三個類完全合并為一個單類后形成的模型;例如Archie是把第三種模型的Item又分出來一些純數據類(可能
是,不確定)形成的一個模型。
但是不管怎么變,基本模型歸納起來就是上面的三種模型,下面分別簡單評價一下:
第一種模型絕大多數人都反對,因此反對理由我也不多講了。但遺憾的是,我觀察到的實際情形是,很多使用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的流行使得第二種模型成為了可能(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方法中的這一句:
java代碼: |
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代碼: |
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,業務邏輯層并不需要提供封裝方法,它可以適用于任何場合。
針對上面帖子中分析的業務邏輯對象的方法有三類的情況,我們在實際的項目中會遇到一些困擾。主要的困擾就是業務邏輯對象的方法會變動的相當頻繁,并且業務邏輯對象的方法數量會非常龐大。針對這個問題,我所知道的有兩種解決方案,我姑且稱之為第二種模型的兩類變種:
第一類變種就是partech的那種模型,簡單的來說,就是把業務邏輯對象層和DAO層合二為一;第二類變種就是干脆取消業務邏輯層,把事務控制前推至Web層的Action層來處理,下面分別分析一下兩類變種的優缺點。
第一類變種是合并業務邏輯對象和DAO層,這種設計代碼簡化為3個類,如下所示:
一個domain object:Item(同第二種模型的代碼,省略)
一個業務層接口:ItemManager(合并原來的ItemManager方法簽名和ItemDao接口而來)
一個業務層實現類:ItemManagerHibernateImpl(合并原來的ItemManager方法實現和ItemDaoHibernateImpl)
java代碼: |
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; }
|
java代碼: |
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模型的三種解決方案中(第二模型,第二模型第一變種,第二模型第二變種),我認為權衡下來,第二模型的第一變種是相對最好的解決方案,不過它仍然有一定的不足,在這里我也希望大家能夠提出更好的解決方案。
posted on 2005-10-29 20:28
老妖 閱讀(394)
評論(0) 編輯 收藏