天天看點

複雜性應對之道 - 領域模組化什麼時候需要領域模組化如何進行領域模組化領域服務業務可視化和可配置化

什麼時候需要領域模組化

軟體的世界裡沒有銀彈,是用

事務腳本

還是

領域模型

沒有對錯之分,關鍵看是否合适。就像自營和平台哪個模式更好?答案是都很好,是以亞馬遜可以有三方入住,阿裡也可以有自建倉嘛。實際上,CQRS就是對事務腳本和領域模型兩種模式的綜合,因為對于Query和報表的場景,使用領域模型往往會把簡單的事情弄複雜,此時完全可以用奧卡姆剃刀把領域層剃掉,直接通路Infrastructure。我個人也是堅決反對過度設計的,是以對于簡單業務場景,我強力建議還是使用事務腳本,其優點是簡單、直覺、易上手。但對于複雜的業務場景,你再這麼玩就不行了,因為一旦業務變得複雜,事務腳本就很難應對,容易造成代碼的“一鍋粥”,系統的腐化速度和複雜性呈指數級上升。目前比較有效的治理辦法就是領域模組化,因為領域模型是面向對象的,在封裝業務邏輯的同時,提升了對象的内聚性和重用性,因為使用了通用語言(

Ubiquitous Language

),使得隐藏的業務邏輯得到顯性化表達,使得複雜性治理成為可能。talk is cheap,直接上一個銀行轉賬的例子,對事務腳本和領域模型進行比較,孰優孰劣一目了然。

銀行轉賬事務腳本實作

在事務腳本的實作中,關于在兩個賬号之間轉賬的領域業務邏輯都被寫在了

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;       }     }           

銀行轉賬領域模型實作

如果用DDD的方式實作,Account實體除了賬号屬性之外,還包含了行為和業務邏輯,比如

debit( )

credit( )

方法。

// @Entity     public 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的好處。

領域模組化的好處

面向對象

  • 封裝:Account的相關操作都封裝在Account Entity上,提高了内聚性和可重用性。
  • 多态:采用政策模式的OverdraftPolicy(多态的典型應用)提高了代碼的可擴充性。

業務語義顯性化

  • 通用語言:“一個團隊,一種語言”,将模型作為語言的支柱。確定團隊在内部的所有交流中,代碼中,畫圖,寫東西,特别是講話的時候都要使用這種語言。例如賬号,轉賬,透支政策,這些都是非常重要的領域概念,如果這些命名都和我們日常讨論以及PRD中的描述保持一緻,将會極大提升代碼的可讀性,減少認知成本。說到這,稍微吐槽一下我們有些工程師的英語水準,有些神翻譯讓一些核心領域概念變得面目全非。
  • 顯性化:就是将隐式的業務邏輯從一推if-else裡面抽取出來,用通用語言去命名、去寫代碼、去擴充,讓其變成顯示概念,比如“透支政策”這個重要的業務概念,按照事務腳本的寫法,其含義完全淹沒在代碼邏輯中沒有突顯出來,看代碼的人自然也是一臉懵逼,而領域模型裡面将其用政策模式抽象出來,不僅提高了代碼的可讀性,可擴充性也好了很多。

如何進行領域模組化

極簡模組化方法

領域模組化這個話題太大,關于此的長篇大論和書籍也很多,比如什麼通過文法和句法深入分析法。我這個人懶,很多方法論的東西記不住也懶得記。就寫點自己的體會,我的方法很簡單就兩個步驟,首先從User Story找名詞和動詞,然後用UML類圖畫出領域模型。比如讓你設計一個中介系統,一個典型的User Story可能是“小明去找工作,中介說你留個電話,有工作機會我會通知你”,這裡面的關鍵名詞很可能就是我們需要的領域對象,小明是求職者,電話是求職者的屬性,中介包含了中介公司,中介員工兩個關鍵對象;工作機會肯定也是關鍵領域對象;通知這個動詞暗示我們這裡用觀察者模式會比較合适。然後再梳理一下領域對象之間的關系,一個求職者可以應聘多個工作機會,一個工作機會也可以被多個求職者應聘,M2M的關系,中介公司可以包含多個員工,O2M的關系。對于這樣簡單的場景,這個模組化就差不多了。

當然我們的業務場景往往比這個要複雜,而且不是所有的名詞都是領域對象也可能是屬性,也不是所有的動詞都是方法也可能是領域對象,是以要具體問題具體對待,這個對待的過程需要我們有很好的業務了解力,抽象能力以及模組化的經驗(知道為什麼公司的job model裡那麼強調技術人員的業務了解力和抽象能力了吧),比如通常情況下,價格和庫存隻是訂單和商品的一個屬性,但是在阿裡系電商業務場景下,價格計算和庫存扣減的複雜程度可以讓你懷疑人生,是以作為電商中台,把價格和庫存單獨當成一個域(Domain)去對待是很必要的。另外,模組化不是一個一次性的工作,往往随着業務的變化以及我們對業務的了解越來越深入才能看清系統的全貌,是以疊代重構是免不了的,也就是要Agile Modelling。

模型統一和模型演化

模組化的過程很像盲人摸象,不同背景人用不同的視角看同一個東西,其了解也是不一樣的。比如兩個盲人都摸到大象鼻子,一個人認為是像蛇(活的能動),而另一個人認為像消防水管(可以噴水),那麼他們将很難內建。雙方都無法接受對方的模型,因為那不符合自己的體驗。事實上,他們需要一個新的抽象,這個抽象需要把蛇的“活着的特性”與消防水管的“噴水功能”合并到一起,而這個抽象還應該排除先前兩個模型中一些不确切的含義和屬性,比如毒牙,或者卷起來放到消防車上去的行為。這就是模型的統一。

世界上唯一不變的就是變化,模型和代碼一樣也需要不斷的重構和精化,每一次的精化之後,開發人員應該對領域知識有了更加清晰的認識。這使得了解上的突破成為可能,之後,一系列快速的改變得到了更符合使用者需要并更加切合實際的模型。其功能性及說明性急速增強,而複雜性卻随之消失。這種突破需要我們對業務有更加深刻的領悟和思考,然後再加上重構的勇氣和能力,勇氣是項目工期很緊你敢不敢重構,能力是你有沒有完備的CI保證你的重構不破壞現有的業務邏輯。還是以開篇的轉賬來舉個例子,假如轉賬業務開始變的複雜,要支援現金,信用卡,支付寶,比特币等多種通道,且沒種通道的限制不一樣,還要支援一對多的轉賬。那麼你還是用一個

transfer(fromAccount, toAccount)

就不合适了,可能需要抽象出一個專門的領域對象Transaction,這樣才能更好的表達業務,其演化過程如下:

複雜性應對之道 - 領域模組化什麼時候需要領域模組化如何進行領域模組化領域服務業務可視化和可配置化

領域服務

什麼是領域服務

有些領域中的動作,它們是一些動詞,看上去卻不屬于任何對象。它們代表了領域中的一個重要的行為,是以不能忽略它們或者簡單地把它們合并到某個實體或者值對象中。當這樣的行為從領域中被識别出來時,最佳實踐是将它聲明成一個服務。這樣的對象不再擁有内置的狀态。它的作用僅僅是為領域提供相應的功能。Service往往是以一個活動來命名,而不是Entity來命名。例如開篇轉賬的例子,轉賬(transfer)這個行為是一個非常重要的領域概念,但是它是發生在兩個賬号之間的,歸屬于賬号Entity并不合适,因為一個賬号Entity沒有必要去關聯他需要轉賬的賬号Entity,這種情況下,使用

MoneyTransferDomainService

就比較合适了。

識别領域服務,主要看它是否滿足以下三個特征:

  1. 服務執行的操作代表了一個領域概念,這個領域概念無法自然地隸屬于一個實體或者值對象。
  2. 被執行的操作涉及到領域中的其他的對象。
  3. 操作是無狀态的。

應用服務和領域服務如何劃分

在領域模組化中,我們一般将系統劃分三個大的層次,即應用層(Application Layer),領域層(Domain Layer)和基礎實施層(Infrastructure Layer),關于這三個層次的詳細内容可以參考我的另一篇SOFA架構的

分層設計

。可以看到在App層和Domain層都有服務(Service),這兩個Service如何劃分呢,什麼樣的功能應該放在應用層,什麼樣的功能應該放在領域層呢?

決定一個服務(Service)應該歸屬于哪一層是很困難的。如果所執行的操作概念上屬于應用層,那麼服務就應該放到這個層。如果操作是關于領域對象的,而且确實是與領域有關的、為領域的需要服務,那麼它就應該屬于領域層。總的來說,涉及到重要領域概念的行為應該放在Domain層,而其它非領域邏輯的技術代碼放在App層,例如參數的解析,上下文的組裝,調用領域服務,消息發送等。還是銀行轉賬的case為例,下圖給出了劃分的建議:

複雜性應對之道 - 領域模組化什麼時候需要領域模組化如何進行領域模組化領域服務業務可視化和可配置化

業務可視化和可配置化

好的領域模組化可以降低應用的複雜性,而可視化和可配置化主要是幫助大家(主要是非技術人員,比如産品,業務和客戶)直覺地了解系統和配置系統。要注意的是可視化和可配置化難免會給系統增加額外的複雜度,必須慎之又慎,最好是能使可視化和配置化的邏輯與業務邏輯盡量少的耦合,否則破壞了原有的架構,把事情搞的更複雜就得不償失了。

可擴充設計

中,我已經介紹了我們SOFA架構是如何通過擴充點的設計來支撐不同業務差異化的需求的,那麼可否更進一步,我們将領域的行為(也叫能力)和擴充點用可視化的方式呈現出來,并對于一些不需要編碼實作的擴充點用配置的方式去完成呢。當然是可以的,比如還是開篇轉賬的例子,對于透支政策OverdraftPolicy這個業務擴充點,新來一個業務說透支額度不能超過1000,我們可以完全結合規則引擎進行配置化完成,而不需要編碼。

是以我能想到的一種還比較優雅的方式,是通過Annotation注解的方式對領域能力和擴充點進行标注,然後在系統bootstrap階段,通過代碼掃描的方式,将這些能力點和擴充點收集起來上傳到中心伺服器,然後再通過GUI的方式呈現出來,進而做到業務的可視化和可配置化。大概的示意圖如下:           
複雜性應對之道 - 領域模組化什麼時候需要領域模組化如何進行領域模組化領域服務業務可視化和可配置化

有同學可能會問流程要不要可視化,這裡要厘清楚兩個概念,業務邏輯流和工作流,很多同學混淆了這兩個概念。業務邏輯流是響應一次使用者請求的業務處理過程,其本身就是業務邏輯,對其編排和可視化的意義并不是很大,無外乎隻是把代碼邏輯可視化了,在我們的SOFA架構中,是通過擴充點和政策模式來處理業務的分支情況,而我看到我們阿裡很多的内部系統将這種響應一次使用者請求的業務邏輯用很重的工作流引擎來做,美其名曰流程可編排,實質上往往是把簡單的事情複雜化了。而工作流是指完成一項任務所需要不同節點的連接配接,節點主要分為自動節點和人工節點,其中每個人工節點都需要使用者的參與,也就是響應一次使用者的請求,比如審批流程中的經理審批節點,CRM銷售過程的業務員的處理節點等等。此時可以考慮使用工作流引擎,特别是當你的系統需要讓使用者自定義流程的時候,那就不得不使用可視化和可配置的工作流引擎了,除此之外,最好不要自找麻煩。我曾在銀行工作過,親眼看見過IBM是怎麼忽悠銀行使用它們的BPM系統,然後把系統弄的巨複雜無比,是以我對工作流引擎的印象并不好,當然也不排除有用的特别合适的案例,隻是我還沒看見,如果有看見的同學麻煩告訴我一聲,學習一下。因為我們現在還沒有讓使用者自定義流程的訴求,是以使用工作流引擎并不在我們現階段的考慮範圍之内。