領(lǐng)域驅(qū)動設(shè)計系列文章(1)——通過現(xiàn)實例子顯示領(lǐng)域驅(qū)動設(shè)計的威力
曾經(jīng)參與過系統(tǒng)維護(hù)或是在現(xiàn)有系統(tǒng)中進(jìn)行迭代開發(fā)的軟件工程師們,你們是否有過這樣的痛苦經(jīng)歷:當(dāng)需要修改一個Bug的時候,面對一個類中成百上千行的代碼,沒有注釋,千奇百怪的方法和變量名字,層層嵌套的方法調(diào)用,混亂不堪的結(jié)構(gòu),不要說準(zhǔn)確找到Bug所在的位置,就是要清晰知道一段代碼究竟是做了什么也非常困難,最終,改對了一個Bug,卻多冒出N個新Bug;同樣的情況,當(dāng)你拿到一份新的需求,需要在現(xiàn)有系統(tǒng)中添加功能的時候,面對一行行完全過程式的代碼,需要使用一個功能時,不知道是應(yīng)該自己編寫,還是應(yīng)該尋找是否已經(jīng)存在的方法,編寫一個非常簡單的新、刪、改功能,卻要費盡九牛二虎之力,最終發(fā)現(xiàn),系統(tǒng)存在著太多的重復(fù)邏輯,閱讀、測試、修改非常困難。在經(jīng)歷了這些痛苦之后,你們是否會不約而同的發(fā)出一個感慨:與其進(jìn)行系統(tǒng)維護(hù)和迭代開發(fā),還不如重新設(shè)計開發(fā)一個新的系統(tǒng)來得痛快?
面對這一系列讓軟件嵌入無底泥潭的問題,基于面向?qū)ο笏枷氲念I(lǐng)域驅(qū)動設(shè)計方法是一個很好的解決方法。從事過系統(tǒng)設(shè)計的富有經(jīng)驗的設(shè)計師們,對職責(zé)單一原則、信息專家、充血/貧血模型、模型驅(qū)動設(shè)計這些名詞或概念應(yīng)該不會感到陌生。面向?qū)ο蟮脑O(shè)計大師Martin Fowler不止一次的在他的Blog和著作《企業(yè)應(yīng)用架構(gòu)模式》中倡導(dǎo)過上述概論在設(shè)計中的巨大威力,而另外一位領(lǐng)域模型的出色專家Eric Evans的著作《領(lǐng)域驅(qū)動設(shè)計》也為我們提供了不少寶貴的經(jīng)驗和方法。
筆者從事系統(tǒng)設(shè)計多年,將會在本系列文章中把本人對領(lǐng)域驅(qū)動設(shè)計的理解,結(jié)合工作過程中積累的實際項目經(jīng)驗進(jìn)行淺析,希望與大家交流學(xué)習(xí)。
在本系列博文的開篇中,我將會拿出一個顯示的例子,先用傳統(tǒng)的面向過程方式,使用貧血模型進(jìn)行設(shè)計,然后再逐步加入需求變更,讓讀者發(fā)現(xiàn),隨著系統(tǒng)的不斷變更,基于貧血模型的設(shè)計將會讓系統(tǒng)慢慢陷入泥潭,越來越難于維護(hù),然后再用基于面向?qū)ο蟮念I(lǐng)域驅(qū)動設(shè)計重新上述過程,通過對比展示領(lǐng)域驅(qū)動設(shè)計對于復(fù)雜的業(yè)務(wù)系統(tǒng)的威力。
假設(shè)現(xiàn)在有一個銀行支付系統(tǒng)項目,其中的一個重要的業(yè)務(wù)用例是賬戶轉(zhuǎn)賬業(yè)務(wù)。系統(tǒng)使用迭代的方式進(jìn)行開發(fā),在1.0版本中,該用例的功能需求非常簡單,事件流描述如下:
主事件流:
1)
用戶登錄銀行的在線支付系統(tǒng)
2)
選擇用戶在該銀行注冊的網(wǎng)上銀行賬戶
3)
選擇需要轉(zhuǎn)賬的目標(biāo)賬戶,輸入轉(zhuǎn)賬金額,申請轉(zhuǎn)賬
4)
銀行系統(tǒng)檢查轉(zhuǎn)出賬戶的金額是否足夠
5)
從轉(zhuǎn)出賬戶中扣除轉(zhuǎn)出金額(debit),更新轉(zhuǎn)出賬戶的余額
6)
把轉(zhuǎn)出金額加入到轉(zhuǎn)入賬戶中(credit),更新轉(zhuǎn)入賬戶的余額
備選事件流:
4a)如果轉(zhuǎn)出賬戶中的余額不足,轉(zhuǎn)賬失敗,返回錯誤信息
面向過程的設(shè)計方式(貧血模型)
設(shè)計方案如下(忽略展示層部分):
1)
設(shè)計一個賬戶交易服務(wù)接口AccountingService,設(shè)計一個服務(wù)方法transfer(),并提供一個具體實現(xiàn)類AccountingServiceImpl,所有賬戶交易業(yè)務(wù)的業(yè)務(wù)邏輯都置于該服務(wù)類中。
2)
提供一個AccountInfo和一個Account,前者是一個用于與展示層交換賬戶數(shù)據(jù)的賬戶數(shù)據(jù)傳輸對象,后者是一個賬戶實體(相當(dāng)于一個EntityBean),這兩個對象都是普通的JavaBean,具有相關(guān)屬性和簡單的get/set方法。
下面是AccountingServiceImpl.transfer()方法的實現(xiàn)邏輯(偽代碼):
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版本的功能需求非常簡單,按面向過程的設(shè)計方式,把所有業(yè)務(wù)代碼置于AccountingServiceImpl中完全沒有問題。
這時候,新需求來了,在1.0.1版本中,需要為賬戶轉(zhuǎn)賬業(yè)務(wù)增加如下功能,在轉(zhuǎn)賬時,首先需要判斷賬戶是否可用,然后,賬戶的余額還要分成兩部分:凍結(jié)部分和活躍部分,處于凍結(jié)部分的金額不能用于任何交易業(yè)務(wù),我們來看看變更后的代碼:
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
*/
}
可以看到,情況變得稍微復(fù)雜了,這時候,1.0.2的需求又來了,需要在每次交易成功后,創(chuàng)建一個交易明細(xì)賬,于是,我們又必須在transfer()方面里面增加創(chuàng)建并持久化交易明細(xì)賬的業(yè)務(wù)邏輯:
AccountTransactionDetails details= new AccountTransactionDetails(…);
accountRepository.save(details);
業(yè)務(wù)需求不斷復(fù)雜化:賬戶每筆轉(zhuǎn)賬的最大額度需要由其信用指數(shù)確定、需要根據(jù)銀行的手續(xù)費策略計算并扣除一定的手續(xù)費用……,隨著業(yè)務(wù)的復(fù)雜化,transfer()方法的邏輯變得越來越復(fù)雜,逐漸形成了上文所述的成百上千行代碼。有經(jīng)驗的程序員可能會做出類此“方法抽取”的重構(gòu),把轉(zhuǎn)賬業(yè)務(wù)按邏輯劃分成若干塊:判斷余額是否足夠、判斷賬戶的信用指數(shù)以確定每筆最大轉(zhuǎn)賬金額、根據(jù)銀行的手續(xù)費策略計算手續(xù)費、記錄交易明細(xì)賬……,從而使代碼更加結(jié)構(gòu)化。這是一個好的開始,但還是顯然不足。
假設(shè)某一天,系統(tǒng)需求增加一個新的模塊,為系統(tǒng)增加一個網(wǎng)上商城,讓銀行用戶可以進(jìn)行在線購物,而在線購物也存在著很多與賬戶貸記借記業(yè)務(wù)相同或相似的業(yè)務(wù)邏輯:判斷余額是否足夠、對賬戶進(jìn)行借貸操作(credit/debit)以改變余額、收取手續(xù)費用、產(chǎn)生交易明細(xì)賬……
面對這種情況,有兩種解決辦法:
1)
把AccountingServiceImpl中的相同邏輯拷貝到OnlineShoppingServiceImplementation中
2)
讓OnlineShoppingServiceImpl調(diào)用AccountingServiceImpl的相同服務(wù)
顯然,第二種方法比第一種方法更好,結(jié)構(gòu)更清晰,維護(hù)更容易。但問題在于,這樣就會形成網(wǎng)上商城服務(wù)模塊與賬戶收支服務(wù)模塊的不必要的依賴關(guān)系,系統(tǒng)的耦合度高了,如果系統(tǒng)為了更靈活的伸縮性,讓每個大業(yè)務(wù)模塊獨立進(jìn)行部署,還需要因為兩者的依賴關(guān)系建立分布式調(diào)用,這無疑增加了設(shè)計、開發(fā)和運(yùn)維的成本。
有經(jīng)驗的設(shè)計人員可能會發(fā)現(xiàn)第三種解決辦法:把相同的業(yè)務(wù)邏輯抽取成一個新的服務(wù),作為公共服務(wù)同時供上述兩個業(yè)務(wù)模塊使用。這只是筆者將會馬上討論的方案——使用領(lǐng)域驅(qū)動設(shè)計。
面向過程的領(lǐng)域驅(qū)動設(shè)計方式(充血模型)
為了節(jié)省篇幅,這里就直接以最復(fù)雜的業(yè)務(wù)需求來進(jìn)行設(shè)計。
領(lǐng)域驅(qū)動設(shè)計的一個重要的概念是領(lǐng)域模型,首先,我們根據(jù)業(yè)務(wù)領(lǐng)域抽象出以下核心業(yè)務(wù)對象模型:

Account:賬戶,是整個系統(tǒng)的最核心的業(yè)務(wù)對象,它包括以下屬性:對象標(biāo)識、賬戶號、是否有效標(biāo)識、余額、凍結(jié)金額、賬戶交易明細(xì)集合、賬戶信用等級。
AccountTransactionDetails:賬戶交易明細(xì),它從屬于賬戶,每個賬戶有多個交易明細(xì),它包括以下屬性:對象標(biāo)識、所屬賬戶、交易類型、交易發(fā)生金額、交易發(fā)生時間。
AccountCreditDegree:賬戶信用等級,它用于限制賬戶的每筆交易發(fā)生金額,包含以下屬性:對象標(biāo)識、對應(yīng)賬戶、信用指數(shù)。
BankTransactionFeeCalculator:銀行交易手續(xù)費用計算器,它包含一個常量:每筆交易的手續(xù)費上限。
我們知道,領(lǐng)域?qū)ο蟪司哂凶陨淼膶傩院蜖顟B(tài)之外,它的一個很重要的標(biāo)志是,它具有屬于自己職責(zé)范圍之內(nèi)的行為,這些行為封裝了其領(lǐng)域內(nèi)的領(lǐng)域業(yè)務(wù)邏輯。于是,我們進(jìn)行進(jìn)一步的建模,根據(jù)業(yè)務(wù)需求為領(lǐng)域?qū)ο笤O(shè)計業(yè)務(wù)方法:

根據(jù)職責(zé)單一的原則,我們把功能需求中描述的功能合理的分配到不同的領(lǐng)域?qū)ο笾校?/span>
Account:
- transferTo:把固定金額轉(zhuǎn)入指定賬戶
- createTransactionDetails:創(chuàng)建交易明細(xì)賬
- updateCreditIndex:更新賬戶的信用指數(shù)
(我們可以看到,后兩個業(yè)務(wù)方法被聲明為protected,具體原因見后述)
AccountCreditDegree:
- getMaxTransactionAmount:獲取所屬賬戶的每筆交易最大金額
BankTransactionFeeCalculator:
- calculateTransactionFee:根據(jù)交易信息計算該筆交易的手續(xù)費
經(jīng)過這樣的設(shè)計,前例中所有放置在服務(wù)對象的業(yè)務(wù)邏輯被分別劃入不同的負(fù)責(zé)相關(guān)職責(zé)的領(lǐng)域?qū)ο螽?dāng)中,下面的時序圖描述了AccountingServiceImpl的轉(zhuǎn)賬業(yè)務(wù)的實現(xiàn)邏輯(為了簡化邏輯,我們忽略掉事物、持久化等邏輯):

再看看AccountingServiceImpl.transfer()的實現(xiàn)邏輯:
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);
}
}
我們可以看到,上例那些復(fù)雜的業(yè)務(wù)邏輯:判斷余額是否足夠、判斷賬戶是否可用、改變賬戶余額、計算手續(xù)費、判斷交易額度、產(chǎn)生交易明細(xì)賬……,都不再存在于AccountingServiceImplementation的transfer方法中,它們被委派給負(fù)責(zé)這些業(yè)務(wù)的領(lǐng)域?qū)ο蟮臉I(yè)務(wù)方法中去,現(xiàn)在應(yīng)該猜到為什么Account中有兩個方法被聲明為protected了吧,因為他們是在debit和credit方法被調(diào)用時,由這兩個方法調(diào)用的,對于AccountingServiceImpl來說,由于產(chǎn)生交易明細(xì)(createTransactionDetails)和更新賬戶信用指數(shù)(updateCreditIndex)都不屬于其職責(zé)范圍,它不需要也無權(quán)使用這些邏輯。
我們可以看到,使用領(lǐng)域驅(qū)動設(shè)計至少會帶來下述優(yōu)點:
- 業(yè)務(wù)邏輯被合理的分散到不同的領(lǐng)域?qū)ο笾校a結(jié)構(gòu)更加清晰,可讀性,可維護(hù)性更高。
- 復(fù)雜的業(yè)務(wù)模型可以通過領(lǐng)域建模(UML是一種主要方式)清晰的表達(dá),開發(fā)人員甚至可以在不讀源碼的情況下就能了解業(yè)務(wù)和系統(tǒng)結(jié)構(gòu),這有利于對現(xiàn)存的系統(tǒng)進(jìn)行維護(hù)和迭代開發(fā)。
再看看如果這時需要加入網(wǎng)上商城的一個新的模塊,開發(fā)人員需要怎么去做,還記得上面提過的第三種方案嗎?就是把賬戶貸記和借記的相關(guān)業(yè)務(wù)抽取到成一個公共服務(wù),同時供銀行在線支付系統(tǒng)和網(wǎng)上商城系統(tǒng)服務(wù),其實這個公共的服務(wù),本質(zhì)上就是這些具有領(lǐng)域邏輯的領(lǐng)域?qū)ο螅?/span>Account、AccountCreditDegree……,由此我們又可以發(fā)現(xiàn)領(lǐng)域驅(qū)動設(shè)計的一大優(yōu)點:
- 系統(tǒng)高度模塊化,代碼重用度高,不會出現(xiàn)太多的重復(fù)邏輯。
筆者經(jīng)驗尚淺,而且文筆拙劣,希望通過這樣的一個場景的分析比較,能讓讀者初步認(rèn)識到基于面向?qū)ο蟮念I(lǐng)域驅(qū)動設(shè)計的威力,并在實際項目中嘗試應(yīng)用。本篇是領(lǐng)取驅(qū)動設(shè)計系列博文的第一篇,在系列文章的第二篇博文中,筆者將會淺析VO、DTO、DO、PO的概念、用處和區(qū)別,敬請各位對本系列博文感興趣的讀者關(guān)注并給予指導(dǎo)修正。