1. 程式人生 > 其它 >領域驅動設計 貧血模型 VO、DTO、DO、PO

領域驅動設計 貧血模型 VO、DTO、DO、PO

領域驅動設計系列(1)通過現例項子顯示領域驅動設計的威力_知識庫_部落格園 https://kb.cnblogs.com/page/522125/

領域驅動設計系列(2)淺析VO、DTO、DO、PO的概念、區別和用處_知識庫_部落格園 https://kb.cnblogs.com/page/522348/

領域驅動設計系列(3)有選擇性的使用領域驅動設計_知識庫_部落格園 https://kb.cnblogs.com/page/521969/

領域驅動設計系列(1)通過現例項子顯示領域驅動設計的威力

作者:Johnny.Liang釋出時間: 2015-05-29 22:41閱讀: 16057 次推薦: 28原文連結[收藏]

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

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

  筆者從事系統設計多年,將會在本系列文章中把本人對領域驅動設計的理解,結合工作過程中積累的實際專案經驗進行淺析,希望與大家交流學習。

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

  假設現在有一個銀行支付系統專案,其中的一個重要的業務用例是賬戶轉賬業務。系統使用迭代的方式進行開發,在1.0版本中,該用例的功能需求非常簡單,事件流描述如下:
主事件流:

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

  備選事件流:

  4a)如果轉出賬戶中的餘額不足,轉賬失敗,返回錯誤資訊

  面向過程的設計方式(貧血模型)

  設計方案如下(忽略展示層部分):

  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的概念、用處和區別,敬請各位對本系列博文感興趣的讀者關注並給予指導修正。

領域驅動設計系列(2)淺析VO、DTO、DO、PO的概念、區別和用處

作者:Johnny.Liang釋出時間: 2015-06-02 18:47閱讀: 24058 次推薦: 31原文連結[收藏]

  上一篇文章作為一個引子,說明了領域驅動設計的優勢,從本篇文章開始,筆者將會結合自己的實際經驗,談及領域驅動設計的應用。本篇文章主要討論一下我們經常會用到的一些物件: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的概念、區別和實際應用了。通過上面的詳細分析,我們還可以總結出一個原則:分析設計層面和實現層面完全是兩個獨立的層面,即使實現層面通過某種技術手段可以把兩個完全獨立的概念合二為一,在分析設計層面,我們仍然(至少在頭腦中)需要把概念上獨立的東西清晰的區分開來,這個原則對於做好分析設計非常重要(工具越先進,往往會讓我們越麻木)。第一篇系列博文拋磚引玉,大唱領域驅動設計的優勢,但其實領域驅動設計在現實環境中還是有種種的限制,需要選擇性的使用,正如我在《田七的智慧》博文中提到,我們不能永遠的理想化的去選擇所謂“最好的設計”,在必要的情況下,我們還是要敢於放棄,因為最合適的設計才是最好的設計。本來,系列中的第二篇博文應該是討論領取驅動設計的限制和如何選擇性的使用,但請原諒我的疏忽,下一篇系列博文會把這個主題補上,敬請關注。

領域驅動設計系列(3)有選擇性的使用領域驅動設計

作者:Johnny.Liang釋出時間: 2015-05-27 22:04閱讀: 4273 次推薦: 8[收藏]

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

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

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

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

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


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

  • 效能比較低:使用純面向物件的方式進行領域驅動設計的程式,其系統開銷通常要比面向過程設計的程式高,從而效能相對較低(關於具體的例子,後續的博文會提及)。

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

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

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