領域驅動設計系列(轉)

  曾經參與過系統維護或是在現有系統中進行迭代開發的軟件工程師們,大家是否有過這樣的痛苦經歷:當須要修改一個Bug的時候,面對一個類中成百上千行的代碼,沒有註釋,千奇百怪的方法和變量名字,層層嵌套的方法調用,混亂不堪的結構,不要說準確找到Bug所在的位置,就是要清晰知道一段代碼到底是作了什麼也很是困難。最終,改對了一個Bug,卻多冒出N個新Bug。一樣的狀況,當你拿到一份新的需求,須要在現有系統中添加功能的時候,面對一行行徹底過程式的代碼,須要使用一個功能時,不知道是應該本身編寫,仍是應該尋找是否已經存在的方法,編寫一個很是簡單的新、刪、改功能,卻要費盡九牛二虎之力。最終發現,系統存在着太多的重複邏輯,閱讀、測試、修改很是困難。在經歷了這些痛苦以後,大家是否會不約而同的發出一個感慨:與其進行系統維護和迭代開發,還不如從新設計開發一個新的系統來得痛快?html

  面對這一系列讓軟件陷入無底泥潭的問題,基於面向對象思想的領域驅動設計方法是一個很好的解決方法。從事過系統設計的富有經驗的設計師們,對職責單一原則、信息專家、充血/貧血模型、模型驅動設計這些名詞或概念應該不會感到陌生。面向對象的設計大師Martin Fowler不止一次的在他的Blog和著做《企業應用架構模式》中倡導過上述概念在設計中的巨大威力,而另一位領域模型的出色專家Eric Evans的著做《領域驅動設計》也爲咱們提供了很多寶貴的經驗和方法。java

  筆者從事系統設計多年,將會在本系列文章中把本人對領域驅動設計的理解,結合工做過程當中積累的實際項目經驗進行淺析,但願與你們交流學習。程序員

  在本系列博文的開篇中,我將會拿出一個例子,先用傳統的面向過程方式,使用貧血模型進行設計,而後再逐步加入需求變動。讓讀者發現,隨着系統的不斷變動,基於貧血模型的設計將會讓系統慢慢陷入泥潭,愈來愈難於維護。而後再用基於面向對象的領域驅動設計從新上述過程,經過對比展現領域驅動設計對於複雜的業務系統的威力。web

  假設如今有一個銀行支付系統項目,其中的一個重要的業務用例是帳戶轉帳業務。系統使用迭代的方式進行開發,在1.0版本中,該用例的功能需求很是簡單,事件流描述以下:
主事件流:數據庫

  1)用戶登陸銀行的在線支付系統
  2)選擇用戶在該銀行註冊的網上銀行帳戶
  3)選擇須要轉帳的目標帳戶,輸入轉帳金額,申請轉帳
  4)銀行系統檢查轉出帳戶的金額是否足夠
  5)從轉出帳戶中扣除轉出金額(debit),更新轉出帳戶的餘額
  6)把轉出金額加入到轉入帳戶中(credit),更新轉入帳戶的餘額設計模式

  備選事件流:網絡

  4a)若是轉出帳戶中的餘額不足,轉帳失敗,返回錯誤信息session

  面向過程的設計方式(貧血模型)數據結構

  設計方案以下(忽略展現層部分):架構

  1)設計一個帳戶交易服務接口AccountingService,設計一個服務方法transfer(),並提供一個具體實現類AccountingServiceImpl,全部帳戶交易業務的業務邏輯都置於該服務類中。

  2)提供一個AccountInfo和一個Account,前者是一個用於與展現層交換帳戶數據的帳戶數據傳輸對象,後者是一個帳戶實體(至關於一個EntityBean),這兩個對象都是普通的JavaBean,具備相關屬性和簡單的get/set方法。

  下面是AccountingServiceImpl.transfer()方法的實現邏輯(僞代碼):

public class AccountingServiceImpl implements AccountingService { public void transfer(Long srcAccountId, Long destAccountId, BigDecimal amount) throws AccountingServiceException { Account srcAccount = accountRepository.getAccount(srcAccountId); Account destAccount = accountRepository.getAccount(destAccountId); if(srcAccount.getBalance().compareTo(amount)<0){ throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH); } srcAccount.setBalance(srcAccount.getBalance().sbustract(amount)); destAccount.setBalance(destAccount.getBalance().add(amount)); } } public class Account implements DomainObject { private Long id; private Bigdecimal balance; /** * getter/setter */ }

  能夠看到,因爲1.0版本的功能需求很是簡單,按面向過程的設計方式,把全部業務代碼置於AccountingServiceImpl中徹底沒有問題。

  這時候,新需求來了,在1.0.1版本中,須要爲帳戶轉帳業務增長以下功能,在轉帳時,首先須要判斷帳戶是否可用,而後,帳戶的餘額還要分紅兩部分:凍結部分和活躍部分,處於凍結部分的金額不能用於任何交易業務,咱們來看看變動後的代碼:

public class AccountingServiceImpl implements AccountingService { public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException { Account srcAccount = accountRepository.getAccount(srcAccountId); Account destAccount = accountRepository.getAccount(destAccountId); if(!srcAccount.isActive() || !destAccount.isActive()) throw new AccountingServiceException(AccountingService.ACCOUNT_IS_NOT_AVAILABLE); BigDecimal availableAmount = srcAccount.getBalance().substract(srcAccount.getFrozenAmount()); if(availableAmount.compareTo(amount)<0) throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH); srcAccount.setBalance(srcAccount.getBalance().sbustract(amount)); destAccount.setBalance(destAccount.getBalance().add(amount)); } } public class Account implements DomainObject { private Long id; private BigDecimal balance; private BigDecimal frozenAmount; /** * getter/setter */ }

  能夠看到,狀況變得稍微複雜了,這時候,1.0.2的需求又來了,須要在每次交易成功後,建立一個交易明細帳,因而,咱們又必須在transfer()方面裏面增長建立並持久化交易明細帳的業務邏輯:

AccountTransactionDetails details= new AccountTransactionDetails(…); accountRepository.save(details);

  業務需求不斷複雜化:帳戶每筆轉帳的最大額度須要由其信用指數肯定、須要根據銀行的手續費策略計算並扣除必定的手續費用……,隨着業務的複雜化,transfer()方法的邏輯變得愈來愈複雜,逐漸造成了上文所述的成百上千行代碼。有經驗的程序員可能會作出類此「方法抽取」的重構,把轉帳業務按邏輯劃分紅若干塊:判斷餘額是否足夠、判斷帳戶的信用指數以肯定每筆最大轉帳金額、根據銀行的手續費策略計算手續費、記錄交易明細帳……,從而使代碼更加結構化。這是一個好的開始,但仍是顯然不足。

  假設某一天,系統需求增長一個新的模塊,爲系統增長一個網上商城,讓銀行用戶能夠進行在線購物,而在線購物也存在着不少與帳戶貸記借記業務相同或類似的業務邏輯:判斷餘額是否足夠、對帳戶進行借貸操做(credit/debit)以改變餘額、收取手續費用、產生交易明細帳……

  面對這種狀況,有兩種解決辦法:

  1) 把AccountingServiceImpl中的相同邏輯拷貝到OnlineShoppingServiceImplementation中
  2) 讓OnlineShoppingServiceImpl調用AccountingServiceImpl的相同服務

  顯然,第二種方法比第一種方法更好,結構更清晰,維護更容易。但問題在於,這樣就會造成網上商城服務模塊與帳戶收支服務模塊的沒必要要的依賴關係,系統的耦合度高了,若是系統爲了更靈活的伸縮性,讓每一個大業務模塊獨立進行部署,還須要由於二者的依賴關係創建分佈式調用,這無疑增長了設計、開發和運維的成本。

  有經驗的設計人員可能會發現第三種解決辦法:把相同的業務邏輯抽取成一個新的服務,做爲公共服務同時供上述兩個業務模塊使用。這就是筆者將會立刻討論的方案——使用領域驅動設計。

  面向對象的領域驅動設計方式(充血模型)

  爲了節省篇幅,這裏就直接以最複雜的業務需求來進行設計。

  領域驅動設計的一個重要的概念是領域模型,首先,咱們根據業務領域抽象出如下核心業務對象模型:

  Account:帳戶,是整個系統的最核心的業務對象,它包括如下屬性:對象標識、帳戶號、是否有效標識、餘額、凍結金額、帳戶交易明細集合、帳戶信用等級。

  AccountTransactionDetails:帳戶交易明細,它從屬於帳戶,每一個帳戶有多個交易明細,它包括如下屬性:對象標識、所屬帳戶、交易類型、交易發生金額、交易發生時間。

  AccountCreditDegree:帳戶信用等級,它用於限制帳戶的每筆交易發生金額,包含如下屬性:對象標識、對應帳戶、信用指數。

  BankTransactionFeeCalculator:銀行交易手續費用計算器,它包含一個常量:每筆交易的手續費上限。

  咱們知道,領域對象除了具備自身的屬性和狀態以外,它的一個很重要的標誌是,它具備屬於本身職責範圍以內的行爲,這些行爲封裝了其領域內的領域業務邏輯。因而,咱們進行進一步的建模,根據業務需求爲領域對象設計業務方法:

  根據職責單一的原則,咱們把功能需求中描述的功能合理的分配到不一樣的領域對象中:

  Account:

  • credit:向銀行帳戶存入金額,貸記
  • debit:從銀行帳戶劃出金額,借記
  • transferTo:把固定金額轉入指定帳戶
  • createTransactionDetails:建立交易明細帳
  • updateCreditIndex:更新帳戶的信用指數

  (咱們能夠看到,後兩個業務方法被聲明爲protected,具體緣由見後述)

  AccountCreditDegree:

  • getMaxTransactionAmount:獲取所屬帳戶的每筆交易最大金額 

  BankTransactionFeeCalculator:

  • calculateTransactionFee:根據交易信息計算該筆交易的手續費

  通過這樣的設計,前例中全部放置在服務對象的業務邏輯被分別劃入不一樣的負責相關職責的領域對象當中,下面的時序圖描述了AccountingServiceImpl的轉帳業務的實現邏輯(爲了簡化邏輯,咱們忽略掉事物、持久化等邏輯):

  再看看AccountingServiceImpl.transfer()的實現邏輯:

public class AccountingServiceImpl implements AccountingService { public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountDomainException { Account srcAccount = accountRepository.getAccount(srcAccountId); Account destAccount = accountRepository.getAccount(destAccountId); srcAccount.transferTo(destAccount,amount); } }

  咱們能夠看到,上例那些複雜的業務邏輯:判斷餘額是否足夠、判斷帳戶是否可用、改變帳戶餘額、計算手續費、判斷交易額度、產生交易明細帳……,都再也不存在於AccountingServiceImplementation的transfer方法中,它們被委派給負責這些業務的領域對象的業務方法中去,如今應該猜到爲何Account中有兩個方法被聲明爲protected了吧,由於他們是在debit和credit方法被調用時,由這兩個方法調用的,對於AccountingServiceImpl來講,因爲產生交易明細(createTransactionDetails)和更新帳戶信用指數(updateCreditIndex)都不屬於其職責範圍,它不須要也無權使用這些邏輯。

  咱們能夠看到,使用領域驅動設計至少會帶來下述優勢:

  • 業務邏輯被合理的分散到不一樣的領域對象中,代碼結構更加清晰,可讀性,可維護性更高。
  • 對象職責更加單一,內聚度更高。
  • 複雜的業務模型能夠經過領域建模(UML是一種主要方式)清晰的表達,開發人員甚至能夠在不讀源碼的狀況下就能瞭解業務和系統結構,這有利於對現存的系統進行維護和迭代開發。

  再看看若是這時須要加入網上商城的一個新的模塊,開發人員須要怎麼去作,還記得上面提過的第三種方案嗎?就是把帳戶貸記和借記的相關業務抽取到成一個公共服務,同時供銀行在線支付系統和網上商城系統服務,其實這個公共的服務,本質上就是這些具備領域邏輯的領域對象:Account、AccountCreditDegree……,由此咱們又能夠發現領域驅動設計的一大優勢:

  • 系統高度模塊化,代碼重用度高,不會出現太多的重複邏輯。

  筆者經驗尚淺,並且文筆拙劣,但願經過這樣的一個場景的分析比較,能讓讀者初步認識到基於面向對象的領域驅動設計的威力,並在實際項目中嘗試應用。本篇是領取驅動設計系列博文的第一篇,在系列文章的第二篇博文中,筆者將會淺析VO、DTO、DO、PO的概念、用處和區別,敬請各位對本系列博文感興趣的讀者關注並給予指導修正。

http://kb.cnblogs.com/page/522125/

  上一篇文章做爲一個引子,說明了領域驅動設計的優點,從本篇文章開始,筆者將會結合本身的實際經驗,談及領域驅動設計的應用。本篇文章主要討論一下咱們常常會用到的一些對象:VO、DTO、DO和PO。

  因爲不一樣的項目和開發人員有不一樣的命名習慣,這裏我首先對上述的概念進行一個簡單描述,名字只是個標識,咱們重點關注其概念:

  概念:

  VO(View Object):視圖對象,用於展現層,它的做用是把某個指定頁面(或組件)的全部數據封裝起來。

  DTO(Data Transfer Object):數據傳輸對象,這個概念來源於J2EE的設計模式,原來的目的是爲了EJB的分佈式應用提供粗粒度的數據實體,以減小分佈式調用的次數,從而提升分佈式調用的性能和下降網絡負載,但在這裏,我泛指用於展現層與服務層之間的數據傳輸對象。

  DO(Domain Object):領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。

  PO(Persistent Object):持久化對象,它跟持久層(一般是關係型數據庫)的數據結構造成一一對應的映射關係,若是持久層是關係型數據庫,那麼,數據表中的每一個字段(或若干個)就對應PO的一個(或若干個)屬性。

  模型:

  下面以一個時序圖創建簡單模型來描述上述對象在三層架構應用中的位置:

  • 用戶發出請求(多是填寫表單),表單的數據在展現層被匹配爲VO。
  • 展現層把VO轉換爲服務層對應方法所要求的DTO,傳送給服務層。
  • 服務層首先根據DTO的數據構造(或重建)一個DO,調用DO的業務方法完成具體業務。
  • 服務層把DO轉換爲持久層對應的PO(可使用ORM工具,也能夠不用),調用持久層的持久化方法,把PO傳遞給它,完成持久化操做。
  • 對於一個逆向操做,如讀取數據,也是用相似的方式轉換和傳遞,略。

  VO與DTO的區別

  你們可能會有個疑問(在筆者參與的項目中,不少程序員也有相同的疑惑):既然DTO是展現層與服務層之間傳遞數據的對象,爲何還須要一個VO呢?對!對於絕大部分的應用場景來講,DTO和VO的屬性值基本是一致的,並且他們一般都是POJO,所以不必畫蛇添足。但不要忘記這是實現層面的思惟,對於設計層面來講,概念上仍是應該存在VO和DTO,由於二者有着本質的區別,DTO表明服務層須要接收的數據和返回的數據,而VO表明展現層須要顯示的數據。

  用一個例子來講明可能會比較容易理解:例如服務層有一個getUser的方法返回一個系統用戶,其中有一個屬性是gender(性別),對於服務層來講,它只從語義上定義:1-男性,2-女性,0-未指定,而對於展現層來講,它可能須要用「帥哥」表明男性,用「美女」表明女性,用「祕密」表明未指定。說到這裏,可能你還會反駁,在服務層直接就返回「帥哥美女」不就好了嗎?對於大部分應用來講,這不是問題,但設想一下,若是需求容許客戶能夠定製風格,而不一樣風格對於「性別」的表現方式不同,又或者這個服務同時供多個客戶端使用(不一樣門戶),而不一樣的客戶端對於表現層的要求有所不一樣,那麼,問題就來了。再者,回到設計層面上分析,從職責單一原則來看,服務層只負責業務,與具體的表現形式無關,所以,它返回的DTO,不該該出現與表現形式的耦合。

  理論歸理論,這到底仍是分析設計層面的思惟,是否在實現層面必須這樣作呢?一刀切的作法每每會得不償失,下面我立刻會分析應用中如何作出正確的選擇。

  VO與DTO的應用

  上面只是用了一個簡單的例子來講明VO與DTO在概念上的區別,本節將會告訴你如何在應用中作出正確的選擇。

  在如下才場景中,咱們能夠考慮把VO與DTO二合爲一(注意:是實現層面):

  • 當需求很是清晰穩定,並且客戶端很明確只有一個的時候,沒有必要把VO和DTO區分開來,這時候VO能夠退隱,用一個DTO便可,爲何是VO退隱而不是DTO?回到設計層面,服務層的職責依然不該該與展現層耦合,因此,對於前面的例子,你很容易理解,DTO對於「性別」來講,依然不能用「帥哥美女」,這個轉換應該依賴於頁面的腳本(如JavaScript)或其餘機制(JSTL、EL、CSS)。
  • 即便客戶端能夠進行定製,或者存在多個不一樣的客戶端,若是客戶端可以用某種技術(腳本或其餘機制)實現轉換,一樣可讓VO退隱。

  如下場景須要優先考慮VO、DTO並存:

  • 上述場景的反面場景
  • 由於某種技術緣由,好比某個框架(如Flex)提供自動把POJO轉換爲UI中某些Field時,能夠考慮在實現層面定義出VO,這個權衡徹底取決於使用框架的自動轉換能力帶來的開發和維護效率提高與設計多一個VO所多作的事情帶來的開發和維護效率的降低之間的比對。
  • 若是頁面出現一個「大視圖」,而組成這個大視圖的全部數據須要調用多個服務,返回多個DTO來組裝(固然,這一樣能夠經過服務層提供一次性返回一個大視圖的DTO來取代,但在服務層提供一個這樣的方法是否合適,須要在設計層面進行權衡)。

  DTO與DO的區別

  首先是概念上的區別,DTO是展現層和服務層之間的數據傳輸對象(能夠認爲是二者之間的協議),而DO是對現實世界各類業務角色的抽象,這就引出了二者在數據上的區別,例如UserInfo和User(對於DTO和DO的命名規則,請參見筆者前面的一篇博文),對於一個getUser方法來講,本質上它永遠不該該返回用戶的密碼,所以UserInfo至少比User少一個password的數據。而在領域驅動設計中,正如第一篇系列文章所說,DO不是簡單的POJO,它具備領域業務邏輯。

  DTO與DO的應用

  從上一節的例子中,細心的讀者可能會發現問題:既然getUser方法返回的UserInfo不該該包含password,那麼就不該該存在password這個屬性定義,但若是同時有一個createUser的方法,傳入的UserInfo須要包含用戶的password,怎麼辦?在設計層面,展現層向服務層傳遞的DTO與服務層返回給展現層的DTO在概念上是不一樣的,但在實現層面,咱們一般不多會這樣作(定義兩個UserInfo,甚至更多),由於這樣作並不見得很明智,咱們徹底能夠設計一個徹底兼容的DTO,在服務層接收數據的時候,不應由展現層設置的屬性(如訂單的總價應該由其單價、數量、折扣等決定),不管展現層是否設置,服務層都一律忽略,而在服務層返回數據時,不應返回的數據(如用戶密碼),就不設置對應的屬性。

  對於DO來講,還有一點須要說明:爲何不在服務層中直接返回DO呢?這樣能夠省去DTO的編碼和轉換工做,緣由以下:

  • 二者在本質上的區別可能致使彼此並不一一對應,一個DTO可能對應多個DO,反之亦然,甚至二者存在多對多的關係。
  • DO具備一些不該該讓展現層知道的數據
  • DO具備業務方法,若是直接把DO傳遞給展現層,展現層的代碼就能夠繞過服務層直接調用它不該該訪問的操做,對於基於AOP攔截服務層來進行訪問控制的機制來講,這問題尤其突出,而在展現層調用DO的業務方法也會由於事務的問題,讓事務難以控制。
  • 對於某些ORM框架(如Hibernate)來講,一般會使用「延遲加載」技術,若是直接把DO暴露給展現層,對於大部分狀況,展現層不在事務範圍以內(Open session in view在大部分狀況下不是一種值得推崇的設計),若是其嘗試在Session關閉的狀況下獲取一個未加載的關聯對象,會出現運行時異常(對於Hibernate來講,就是LazyInitiliaztionException)。
  • 從設計層面來講,展現層依賴於服務層,服務層依賴於領域層,若是把DO暴露出去,就會致使展現層直接依賴於領域層,這雖然依然是單向依賴,但這種跨層依賴會致使沒必要要的耦合。

  對於DTO來講,也有一點必須進行說明,就是DTO應該是一個「扁平的二維對象」,舉個例子來講明:若是User會關聯若干個其餘實體(例如Address、Account、Region等),那麼getUser()返回的UserInfo,是否就須要把其關聯的對象的DTO都一併返回呢?若是這樣的話,必然致使數據傳輸量的大增,對於分佈式應用來講,因爲涉及數據在網絡上的傳輸、序列化和反序列化,這種設計更不可接受。若是getUser除了要返回User的基本信息外,還須要返回一個AccountId、AccountName、RegionId、RegionName,那麼,請把這些屬性定義到UserInfo中,把一個「立體」的對象樹「壓扁」成一個「扁平的二維對象」。筆者目前參與的項目是一個分佈式系統,該系統無論三七二十一,把一個對象的全部關聯對象都轉換爲相同結構的DTO對象樹並返回,致使性能很是的慢。

  DO與PO的區別

  DO和PO在絕大部分狀況下是一一對應的,PO是隻含有get/set方法的POJO,但某些場景仍是能反映出二者在概念上存在本質的區別:

  • DO在某些場景下不須要進行顯式的持久化,例如利用策略模式設計的商品折扣策略,會衍生出折扣策略的接口和不一樣折扣策略實現類,這些折扣策略實現類能夠算是DO,但它們只駐留在靜態內存,不須要持久化到持久層,所以,這類DO是不存在對應的PO的。
  • 一樣的道理,某些場景下,PO也沒有對應的DO,例如老師Teacher和學生Student存在多對多的關係,在關係數據庫中,這種關係須要表現爲一箇中間表,也就對應有一個TeacherAndStudentPO的PO,但這個PO在業務領域沒有任何現實的意義,它徹底不能與任何DO對應上。這裏要特別聲明,並非全部多對多關係都沒有業務含義,這跟具體業務場景有關,例如:兩個PO之間的關係會影響具體業務,而且這種關係存在多種類型,那麼這種多對多關係也應該表現爲一個DO,又如:「角色」與「資源」之間存在多對多關係,而這種關係很明顯會表現爲一個DO——「權限」。
  • 某些狀況下,爲了某種持久化策略或者性能的考慮,一個PO可能對應多個DO,反之亦然。例如客戶Customer有其聯繫信息Contacts,這裏是兩個一對一關係的DO,但可能出於性能的考慮(極端狀況,權做舉例),爲了減小數據庫的鏈接查詢操做,把Customer和Contacts兩個DO數據合併到一張數據表中。反過來,若是一本圖書Book,有一個屬性是封面cover,但該屬性是一副圖片的二進制數據,而某些查詢操做不但願把cover一併加載,從而減輕磁盤IO開銷,同時假設ORM框架不支持屬性級別的延遲加載,那麼就須要考慮把cover獨立到一張數據表中去,這樣就造成一個DO對應多個PO的狀況。
  • PO的某些屬性值對於DO沒有任何意義,這些屬性值多是爲了解決某些持久化策略而存在的數據,例如爲了實現「樂觀鎖」,PO存在一個version的屬性,這個version對於DO來講是沒有任何業務意義的,它不該該在DO中存在。同理,DO中也可能存在不須要持久化的屬性。

  DO與PO的應用

  因爲ORM框架的功能很是強大而大行其道,並且JavaEE也推出了JPA規範,如今的業務應用開發,基本上不須要區分DO與PO,PO徹底能夠經過JPA,Hibernate Annotations/hbm隱藏在DO之中。雖然如此,但有些問題咱們還必須注意:

  • 對於DO中不須要持久化的屬性,須要經過ORM顯式的聲明,如:在JPA中,能夠利用@Transient聲明。
  • 對於PO中爲了某種持久化策略而存在的屬性,例如version,因爲DO、PO合併了,必須在DO中聲明,但因爲這個屬性對DO是沒有任何業務意義的,須要讓該屬性對外隱藏起來,最多見的作法是把該屬性的get/set方法私有化,甚至不提供get/set方法。但對於Hibernate來講,這須要特別注意,因爲Hibernate從數據庫讀取數據轉換爲DO時,是利用反射機制先調用DO的空參數構造函數構造DO實例,而後再利用JavaBean的規範反射出set方法來爲每一個屬性設值,若是不顯式聲明set方法,或把set方法設置爲private,都會致使Hibernate沒法初始化DO,從而出現運行時異常,可行的作法是把屬性的set方法設置爲protected。
  • 對於一個DO對應多個PO,或者一個PO對應多個DO的場景,以及屬性級別的延遲加載,Hibernate都提供了很好的支持,請參考Hibnate的相關資料。

  到目前爲止,相信你們都已經比較清晰的瞭解VO、DTO、DO、PO的概念、區別和實際應用了。經過上面的詳細分析,咱們還能夠總結出一個原則:分析設計層面和實現層面徹底是兩個獨立的層面,即便實現層面經過某種技術手段能夠把兩個徹底獨立的概念合二爲一,在分析設計層面,咱們仍然(至少在頭腦中)須要把概念上獨立的東西清晰的區分開來,這個原則對於作好分析設計很是重要(工具越先進,每每會讓咱們越麻木)。第一篇系列博文拋磚引玉,大唱領域驅動設計的優點,但其實領域驅動設計在現實環境中仍是有種種的限制,須要選擇性的使用,正如我在《田七的智慧》博文中提到,咱們不能永遠的理想化的去選擇所謂「最好的設計」,在必要的狀況下,咱們仍是要勇於放棄,由於最合適的設計纔是最好的設計。原本,系列中的第二篇博文應該是討論領取驅動設計的限制和如何選擇性的使用,但請原諒個人疏忽,下一篇系列博文會把這個主題補上,敬請關注。

http://kb.cnblogs.com/page/522348/

  本系列的第一篇博文拋磚引玉,大談領域驅動設計的優點,這裏筆者仍是但願以客觀的態度,談談領域驅動設計的缺點及其不適合使用的場景,以讓讀者能夠有選擇性的使用領域驅動設計。

  咱們知道,沒有最好,只有最合適,設計也是同樣。所以,所謂設計,就是以你和你的團隊的知識、經驗和智慧,全面充分的考慮各類內外因素後,在大家的設計方案中做出合理的選擇的過程。而這些影響大家選擇的因素主要有: 

  •  技術框架的特徵和約束(若是你的項目決定使用C語言進行開發,那麼首先在設計方法上,就須要使用面向過程而非面向對象的設計方法)。 
  •  時間的壓力和約束(你永遠不可能告訴你的老闆,給我10年時間,我和個人團隊將爲你設計出世界上最優秀的軟件)。 
  • 你的團隊的能力、經驗、性格、價值觀等因素的約束(你不能指望一個長期從事遺留系統維護項目或大部分紅員是缺少經驗的高校畢業生的團隊能很好的按照你的設計意圖去實現你的高度抽象的優秀設計,同時你也別期望一幫家裏經濟條件不錯,本着過來熬時間的傢伙會樂意與你一塊兒刻苦鑽研、精益求精)。 
  •  你的系統的特徵(若是你想把一個足夠簡單並且在能夠預計的未來都不存在很大規模的需求變動的系統設計得很複雜,很精妙,具備很好的擴展性,但要爲此付出巨大的時間、人力成本,這顯然是一種不理智的過分設計(Over design))。 
  • 其餘外在因素的約束(你的項目須要參與投標,你必須壓縮人力、時間等以讓你的項目成本成爲巨大的競爭資本)。

  固然,上述的考慮因素站在比較高的角度,一般是項目經理、架構師須要考慮的問題,但這當中你應該會獲得一些啓發。回到咱們的主題,咱們首先看看,領域驅動設計相對於傳統的面向過程式的設計,有什麼缺點: 

  • 複雜化:面向過程思惟之因此一直很受歡迎,是由於它很直觀,很是符合大部分人的思惟習慣。大部分人拿到一個問題,一般都是會很直觀的想第一步作什麼、第二步作什麼,若是怎樣,應該怎樣,不然怎樣……,能夠說,任何水平的程序員,都能很好的使用面向過程的方法進行設計和開發。同時,因爲咱們教育水平的落後和總體IT環境的制約,能夠這樣說,真正掌握面向對象思惟和設計方法的程序員的比例很是低(雖然絕大部分都使用面向對象的語言和工具),而自己面向對象思惟要求人有很好的抽象思惟能力,由於你須要把一個複雜的系統一層層的抽象爲簡單的部分,須要把現實世界的事物(有些是可見的,但有些是不可見的)合理的抽象爲計算機世界的不一樣元素。這些都不是一些很容易作的事情,要作得好,就更難。 
  • 團隊的抗拒:若是你的團隊(很大可能)大部分人都習慣於用很直觀的面向過程的方式進行設計和開發,你須要推進你的團隊轉換思惟來採用面向對象的方式進行領域驅動設計,一般會遭到多數人的抗拒。由於人都是有惰性的,他們習慣安於現狀,而改變是痛苦的,他們要付出額外的努力,須要進行學習,但以筆者的經驗,至關一部分程序員,特別是有必定工做年限的程序員,他們從事IT工做都只是爲了得到一份不錯的報酬,所以他們的學習動力很是有限,並且,他們都至關自負,被說服的難度比較大。 
  • 管理、開發和維護的成本高:複雜度更高,意味着你須要花更多的時間進行設計,同時須要花出額外的時間進行必要的培訓,並且須要有更完善的文檔(設計文檔,API文檔,培訓文檔等)。領域驅動設計的抽象程度比較高,所以必需有良好的文檔,不然,隨着項目的不斷迭代、升級和維護,它很容易由於後來者的誤解而慢慢迴歸面向過程的設計,甚至會變得「四不象」,領域驅動設計的成本優點是隨着時間的推移慢慢體現的(見下圖),若是出現這種狀況,全部前面付出的努力都會付諸東流。


系統的初始階段,領域驅動設計須要付出更大的成本,但隨着時間的推移,領域驅動設計的成本效益優點會逐步顯現

  • 性能比較低:使用純面向對象的方式進行領域驅動設計的程序,其系統開銷一般要比面向過程設計的程序高,從而性能相對較低(關於具體的例子,後續的博文會說起)。

  那麼,假設咱們在時間、團隊能力及各類資源都容許的狀況下,是否就能夠麻木的全盤使用領域驅動設計呢?正如本博文的標題同樣,答案是否認的,咱們須要有選擇性的使用。讓咱們來看看一些指導性原則: 

  • 使用領域驅動設計,並不表明整個系統的方方面面都必須聽從領域驅動設計的原則,須要根據實際狀況,讓適合的部分使用領域驅動設計,讓不適合的部分使用面向過程的設計。 
  • 對於那些業務規則很是簡單,一般只有增、刪、改、查的簡單操做,並且也不大可能發生大規模需求變動的模塊,可讓業務實體成爲一個「貧血模型」,例如一些基礎數據:系統參數、商品類型、國家、地址信息(注:對於這點,本人持保留態度,由於這些業務雖然很是簡單,但既然選擇了領取驅動設計,即便把這些業務實體設計爲「充血模型」,即把極其簡單的業務邏輯也封裝在業務實體中,也並不比使用「貧血模型」成本高,而它卻帶來了統一設計風格的好處)。 
  • 對於查詢操做,特別是複雜的查詢操做,出於性能的考慮,能夠用結構化查詢邏輯一次性完成,並把這些邏輯封裝在Repository(即技術上的DAO)中(這方面的具體例子,後面關於「查詢通道」和「領域對象倉庫」的博文會更具體的闡述)。咱們能夠看到,對於一些業務視圖,以及報表模塊,很明顯不適合使用面向對象的方式設計,由於這些「視圖」和「報表」,本質上就不是業務實體。 
  • 一樣出於性能的考慮,在業務實體的實現邏輯中,某些操做不適合過分偏執的使用面向對象方式。例如,在「訂單」的「新增訂單明細」(order.addOrderItem(orderItem))中,若是業務邏輯規定一張訂單中包含優惠商品的明細數目不能超過20條,使用面向對象的方式,就須要把訂單中的全部訂單明細所有加載,而後逐個明細判斷其對應的商品是不是優惠商品,再統計出優惠商品的數目,這樣很明顯是低效率和高開銷的,這裏只須要使用Repository提供的一個統計方法,用一個結構化查詢邏輯返回統計結果便可,而這就是非面向對象的方式。

  本博文給有志於領域驅動設計的讀者潑了一下冷水,提出一些「反模式」(Bitter),是爲了讓讀者冷靜一下,在領域驅動設計過程當中做出更靈活和更合理的選擇。關於這方面的論述,筆者在這裏淺嘗則止,限於水平、經驗和表達能力,不敢胡亂賣弄,建議讀者能夠參考閱讀Martin Fowler的《Patterns of Enterprise Application Architecture》一書的相關觀點。http://kb.cnblogs.com/page/521969/

相關文章
相關標籤/搜索