編輯推薦:本文來自於作者張建飛,文章主要介紹瞭如何進行領域建模、領域服務以及模型重構等相關內容。
一、為什麼要領域建模維護過企業級業務系統的同學,基本上沒有一個不抱怨業務程式碼爛的,過程式的麵條程式碼充斥著螢幕,程式設計師的心力和體力都經受著極大的考驗,怎麼破?
二、DDD革命DDD革命性在於,領域模型準確反映了業務語言,而傳統三層架構的事務性程式設計模型只關心資料,這些資料物件除了簡單setter/getter方法外,沒有任何業務方法,被比喻成貧血模式。
以銀行賬號Account為案例,Account有“存款”,“計算利息”和“取款”等業務行為,但是傳統經典的方式是將“存款”,“計算利息”和“取款”行為放在賬號的服務AccountService中,而不是放在Account物件本身之中。我們不能因為用了計算機,用了資料庫,用了框架,業務模型反而被技術框架給綁架了,就像人雖然是由母親生的,但是人的吃喝拉撒母親不能替代,更不能以母愛名義剝奪人的正常職責行為,如果是這樣,這個人就是被母愛綁架了。
三、DDD不是銀彈軟體的世界裡沒有銀彈,是用事務指令碼還是領域模型沒有對錯之分,關鍵看是否合適。就像自營和平臺哪個模式更好?答案是都很好,所以亞馬遜可以有三方入住,阿里也可以有自建倉嘛。
實際上,CQRS就是對事務指令碼和領域模型兩種模式的綜合,因為對於Query和報表的場景,使用領域模型往往會把簡單的事情弄複雜,此時完全可以用奧卡姆剃刀把領域層剃掉,直接訪問Infrastructure。
我個人也是堅決反對過度設計的,因此對於簡單業務場景,我強力建議還是使用事務指令碼,其優點是簡單、直觀、易上手。但對於複雜的業務場景,你再這麼玩就不行了,因為一旦業務變得複雜,事務指令碼就很難應對,容易造成程式碼的“一鍋粥”,系統的腐化速度和複雜性呈指數級上升。目前比較有效的治理辦法就是領域建模,因為領域模型是面向物件的,在封裝業務邏輯的同時,提升了物件的內聚性和重用性,因為使用了通用語言(Ubiquitous Language),使得隱藏的業務邏輯得到顯性化表達,使得複雜性治理成為可能。
接下來,讓我們看一個銀行轉賬的例項,對比下事務指令碼和領域模型兩者程式設計模型的不同。
四、DDD初體驗(1)銀行轉賬事務指令碼實現
在事務指令碼的實現中,關於在兩個賬號之間轉賬的領域業務邏輯都被寫在了MoneyTransferService的實現裡面了,而Account僅僅是getters和setters的資料結構,也就是我們說的貧血模型:
public class MoneyTransferServiceTransactionScriptImpl implements MoneyTransferService { private AccountDao accountDao; private BankingTransactionRepository bankingTransactionRepository; . . . @Override public BankingTransaction transfer( String fromAccountId, String toAccountId, double amount) { Account fromAccount = accountDao.findById(fromAccountId); Account toAccount = accountDao.findById(toAccountId); . . . double newBalance = fromAccount.getBalance() - amount; switch (fromAccount.getOverdraftPolicy()) { case NEVER: if (newBalance < 0) { throw new DebitException("Insufficient funds"); } break; case ALLOWED: if (newBalance < -limit) { throw new DebitException( "Overdraft limit (of " + limit + ") exceeded: " + newBalance); } break; } fromAccount.setBalance(newBalance); toAccount.setBalance(toAccount.getBalance() + amount); BankingTransaction moneyTransferTransaction = new MoneyTranferTransaction(fromAccountId, toAccountId, amount); bankingTransactionRepository.addTransaction(moneyTransferTransaction); return moneyTransferTransaction; }}
上面的程式碼大家看起來應該比較眼熟,因為目前大部分系統都是這麼寫的。需求評審完,工程師畫幾張UML圖完成設計,就開始向上面這樣懟業務程式碼了,這樣寫基本不用太費腦,完全是面向過程的程式碼風格。有些同學可能會說,我這樣寫也可以實現系統功能啊,還是那句話“just because you can, doesn’t mean you should”。說句不好聽的,正是有這麼多“沒有追求”、“不求上進”的碼農才造成了應用系統的混亂、敗壞了應用開發的名聲。這也是為什麼很多應用開發工程師覺得工作沒意思,技術含量低,覺得整天就是寫if-else的業務邏輯程式碼,系統又爛,工作繁瑣、無聊、沒有成長、沒有成就感,所以轉向去做中介軟體啊,去寫JDK啊,覺得那個NB。實際上,應用開發一點都不簡單也不無聊,業務的變化比底層Infrastructure的變化要多得多,解決的難度也絲毫不比寫底層程式碼容易,只是很多人選擇了用無聊的方式去做。其實我們是有辦法做的更優雅的,這種優雅的方式就是領域建模,唯有掌握了這種優雅你才能實現從工程師嚮應用架構的轉型。同樣的業務邏輯,接下來就讓我們看一下用DDD是怎麼做的。
(2)銀行轉賬領域模型實現
如果用DDD的方式實現,Account實體除了賬號屬性之外,還包含了行為和業務邏輯,比如debit( )和credit( )方法。
// @Entitypublic class Account { // @Id private String id; private double balance; private OverdraftPolicy overdraftPolicy; . . . public double balance() { return balance; } public void debit(double amount) { this.overdraftPolicy.preDebit(this, amount); this.balance = this.balance - amount; this.overdraftPolicy.postDebit(this, amount); } public void credit(double amount) { this.balance = this.balance + amount; }}
而且透支策略OverdraftPolicy也不僅僅是一個Enum了,而是被抽象成包含了業務規則並採用了策略模式的物件。
public interface OverdraftPolicy { void preDebit(Account account, double amount); void postDebit(Account account, double amount);}
public class NoOverdraftAllowed implements OverdraftPolicy { public void preDebit(Account account, double amount) { double newBalance = account.balance() - amount; if (newBalance < 0) { throw new DebitException("Insufficient funds"); } } public void postDebit(Account account, double amount) { }}public class LimitedOverdraft implements OverdraftPolicy { private double limit; . . . public void preDebit(Account account, double amount) { double newBalance = account.balance() - amount; if (newBalance < -limit) { throw new DebitException( "Overdraft limit (of " + limit + ") exceeded: " + newBalance); } } public void postDebit(Account account, double amount) { }}而Domain Service只需要呼叫Domain Entity物件完成業務邏輯即可。public class MoneyTransferServiceDomainModelImpl implements MoneyTransferService { private AccountRepository accountRepository; private BankingTransactionRepository bankingTransactionRepository; . . . @Override public BankingTransaction transfer( String fromAccountId, String toAccountId, double amount) { Account fromAccount = accountRepository.findById(fromAccountId); Account toAccount = accountRepository.findById(toAccountId); . . . fromAccount.debit(amount); toAccount.credit(amount); BankingTransaction moneyTransferTransaction = new MoneyTranferTransaction(fromAccountId, toAccountId, amount); bankingTransactionRepository.addTransaction(moneyTransferTransaction); return moneyTransferTransaction; }}
透過上面的DDD重構後,原來在事務指令碼中的邏輯,被分散到Domain Service,Domain Entity和OverdraftPolicy三個滿足SOLID的物件中。類的數量比以前多了一些,但是每個類的職責更加單一,程式碼的可讀性和可擴充套件性也隨之提高。
在繼續閱讀之前,我建議可以自己先體會一下DDD的好處。
五、領域建模的好處DDD最大的好處是:接觸到需求第一步就是考慮領域模型,而不是將其切割成資料和行為,然後資料用資料庫實現,行為使用服務實現,最後造成需求的首肢分離。DDD讓你首先考慮的是業務語言,而不是資料。DDD強調業務抽象和麵向物件程式設計,而不是過程式業務邏輯實現。重點不同導致程式設計世界觀不同。
(1)面向物件
封裝:Account的相關操作都封裝在Account Entity上,提高了內聚性和可重用性。
多型:採用策略模式的OverdraftPolicy(多型的典型應用)提高了程式碼的可擴充套件性。
(2)業務語義顯性化
通用語言:“一個團隊,一種語言”,將模型作為語言的支柱。確保團隊在內部的所有交流中,程式碼中,畫圖,寫東西,特別是講話的時候都要使用這種語言。例如賬號,轉賬,透支策略,這些都是非常重要的領域概念,如果這些命名都和我們日常討論以及PRD中的描述保持一致,將會極大提升程式碼的可讀性,減少認知成本。說到這,稍微吐槽一下我們有些工程師的英語水平,有些神翻譯讓一些核心領域概念變得面目全非。
顯性化:就是將隱式的業務邏輯從一堆if-else裡面抽取出來,用通用語言去命名、去寫程式碼、去擴充套件,讓其變成顯示概念,比如“透支策略”這個重要的業務概念,按照事務指令碼的寫法,其含義完全淹沒在程式碼邏輯中沒有突顯出來,看程式碼的人自然也是一臉懵逼,而領域模型裡面將其用策略模式抽象出來,不僅提高了程式碼的可讀性,可擴充套件性也好了很多。
複雜性應對之道 - 領域建模
http://www.uml.org.cn/sjms/201812133.asp