1. 程式人生 > >架構師如何應對複雜業務場景?領域建模的實戰案例解析

架構師如何應對複雜業務場景?領域建模的實戰案例解析

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

阿里妹導讀:你還在用面向物件的語言寫面向過程的程式碼嗎?你是否正在被複雜的業務邏輯折磨?是否有時覺得應用開發沒意思、沒挑戰、技術含量低?其實,應用開發一點都不簡單,也不無聊,業務的變化比底層基礎實施的變化要多得多,封裝這些變化需要很好的業務理解力,抽象能力和建模能力。

今天我們邀請阿里高階技術專家張建飛,一起來聊聊為什麼需要領域建模,什麼是好的模型,又該如何搭建。

為什麼要領域建模?

軟體的世界裡沒有銀彈,是用事務指令碼還是領域模型沒有對錯之分,關鍵看是否合適。實際上,CQRS就是對事務指令碼和領域模型兩種模式的綜合,因為對於Query和報表的場景,使用領域模型往往會把簡單的事情弄複雜,此時完全可以用奧卡姆剃刀把領域層剃掉,直接訪問Infrastructure。我個人也是堅決反對過度設計的,因此對於簡單業務場景,我強力建議還是使用事務指令碼,其優點是簡單、直觀、易上手。但對於複雜的業務場景,你再這麼玩就不行了,因為一旦業務變得複雜,事務指令碼就很難應對,容易造成程式碼的“一鍋粥”,系統的腐化速度和複雜性呈指數級上升。

目前比較有效的治理辦法就是領域建模,因為領域模型是面向物件的,在封裝業務邏輯的同時,提升了物件的內聚性和重用性,因為使用了通用語言(Ubiquitous Language),使得隱藏的業務邏輯得到顯性化表達,使得複雜性治理成為可能。talk is cheap,直接上一個銀行轉賬的例子,對事務指令碼和領域模型進行比較,孰優孰劣一目瞭然。

銀行轉賬事務指令碼實現

在事務指令碼的實現中,關於在兩個賬號之間轉賬的領域業務邏輯都被寫在了MoneyTransferService的實現裡面了,而Account僅僅是getters和setters的資料結構,也就是我們說的貧血模式。

640?wx_fmt=png

上面的程式碼大家看起來應該比較眼熟,因為目前大部分系統都是這麼寫的。需求評審完,工程師畫幾張UML圖完成設計,就開始向上面這樣懟業務程式碼了,這樣寫基本不用太費腦,完全是面向過程的程式碼風格。

有些同學可能會說,我這樣寫也可以實現系統功能啊,還是那句話“just because you can, doesn't mean you should”。說句不好聽的,正是有這麼多“沒有追求”、“不求上進”的碼農才造成了應用系統的混亂、敗壞了應用開發的名聲。這也是為什麼很多應用開發工程師覺得工作沒意思,技術含量低,覺得整天就是寫if-else的業務邏輯程式碼,系統又爛,工作繁瑣、無聊、沒有成長、沒有成就感,所以轉向去做中介軟體啊,去寫JDK啊,覺得那個NB。

實際上,應用開發一點都不簡單也不無聊,業務的變化比底層Infrastructure的變化要多得多,解決的難度也絲毫不比寫底層程式碼容易,只是很多人選擇了用無聊的方式去做。其實我們是有辦法做的更優雅的,這種優雅的方式就是領域建模,唯有掌握了這種優雅你才能實現從工程師嚮應用架構的轉型。同樣的業務邏輯,接下來就讓我們看一下用DDD是怎麼做的。

銀行轉賬領域模型實現

如果用DDD的方式實現,Account實體除了賬號屬性之外,還包含了行為和業務邏輯,比如debit( )和credit( )方法。

640?wx_fmt=png

而且透支策略OverdraftPolicy也不僅僅是一個Enum了,而是被抽象成包含了業務規則並採用了策略模式的物件。

而Domain Service只需要呼叫Domain Entity物件完成業務邏輯即可。

640?wx_fmt=png

通過上面的DDD重構後,原來在事務指令碼中的邏輯,被分散到Domain Service,Domain Entity和OverdraftPolicy三個滿足SOLID的物件中,在繼續閱讀之前,我建議可以自己先體會一下DDD的好處。

領域建模的好處

面向物件

  • 封裝:Account的相關操作都封裝在Account Entity上,提高了內聚性和可重用性。

  • 多型:採用策略模式的OverdraftPolicy(多型的典型應用)提高了程式碼的可擴充套件性。

業務語義顯性化

  • 通用語言:“一個團隊,一種語言”,將模型作為語言的支柱。確保團隊在內部的所有交流中,程式碼中,畫圖,寫東西,特別是講話的時候都要使用這種語言。例如賬號,轉賬,透支策略,這些都是非常重要的領域概念,如果這些命名都和我們日常討論以及PRD中的描述保持一致,將會極大提升程式碼的可讀性,減少認知成本。

  • 顯性化:就是將隱式的業務邏輯從一推if-else裡面抽取出來,用通用語言去命名、去寫程式碼、去擴充套件,讓其變成顯示概念,比如“透支策略”這個重要的業務概念,按照事務指令碼的寫法,其含義完全淹沒在程式碼邏輯中沒有突顯出來,看程式碼的人自然也是一臉懵逼,而領域模型裡面將其用策略模式抽象出來,不僅提高了程式碼的可讀性,可擴充套件性也好了很多。

如何進行領域建模?

建模方法

領域建模這個話題太大,關於此的長篇大論和書籍也很多,比如什麼通過語法和句法深入分析法,在我看來這些方法論有些繁瑣了。好的模型應該是建立在對業務深入理解的基礎上,如果業務理解不到位,你再怎麼分析句子也不可能產出好的模型。就我自己的經驗而言,建模也是一個不斷迭代的過程,所以一開始可以簡單點來,就採用兩步建模法抓住一些核心概念,然後寫一些程式碼驗證一下run一下,看看順不順,如果很順滑,說明沒毛病,否則就要看看是不是需要調整一下模型,隨著專案的進行和對業務理解的不斷深入,這種迭代將持續進行。


那什麼是兩步建模法呢?也就是隻需要兩個步驟就能建模了,首先從User Story找名詞和動詞,然後用UML類圖畫出領域模型。是不是很簡約?簡約並不意味著簡單,對於業務架構師和系統分析師來說,見功力的地方往往就在於此。

舉個栗子,比如讓你設計一箇中介系統,一個典型的User Story可能是“小明去找工作,中介說你留個電話,有工作機會我會通知你”,這裡面的關鍵名詞很可能就是我們需要的領域物件,小明是求職者,電話是求職者的屬性,中介包含了中介公司,中介員工兩個關鍵物件;工作機會肯定也是關鍵領域物件;通知這個動詞暗示我們這裡用觀察者模式會比較合適。然後再梳理一下領域物件之間的關係,一個求職者可以應聘多個工作機會,一個工作機會也可以被多個求職者應聘,M2M的關係,中介公司可以包含多個員工,O2M的關係。對於這樣簡單的場景,這個建模就差不多了。


當然我們的業務場景往往比這個要複雜,而且不是所有的名詞都是領域物件也可能是屬性,也不是所有的動詞都是方法也可能是領域物件,所以要具體問題具體對待,這個對待的過程需要我們有很好的業務理解力,抽象能力以及建模的經驗(知道為什麼公司的job model裡那麼強調技術人員的業務理解力和抽象能力了吧),比如通常情況下,價格和庫存只是訂單和商品的一個屬性,但是在阿里系電商業務場景下,價格計算和庫存扣減的複雜程度可以讓你懷疑人生,因此作為電商中臺,把價格和庫存單獨當成一個域(Domain)去對待是很必要的。

另外,建模不是一個一次性的工作,往往隨著業務的變化以及我們對業務的理解越來越深入才能看清系統的全貌,所以迭代重構是免不了的,也就是要Agile Modelling。

模型統一和模型演化

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


世界上唯一不變的就是變化,模型和程式碼一樣也需要不斷的重構和精化,每一次的精化之後,開發人員應該對領域知識有了更加清晰的認識。這使得理解上的突破成為可能,之後,一系列快速的改變得到了更符合使用者需要並更加切合實際的模型。其功能性及說明性急速增強,而複雜性卻隨之消失。

這種突破需要我們對業務有更加深刻的領悟和思考,然後再加上重構的勇氣和能力,勇氣是專案工期很緊你敢不敢重構,能力是你有沒有完備的CI保證你的重構不破壞現有的業務邏輯。還是以開篇的轉賬來舉個例子,假如轉賬業務開始變的複雜,要支援現金,信用卡,支付寶,比特幣等多種通道,且沒種通道的約束不一樣,還要支援一對多的轉賬。那麼你還是用一個transfer(fromAccount, toAccount)就不合適了,可能需要抽象出一個專門的領域物件Transaction,這樣才能更好的表達業務,其演化過程如下:

640?wx_fmt=png

領域服務

什麼是領域服務?

有些領域中的動作,它們是一些動詞,看上去卻不屬於任何物件。它們代表了領域中的一個重要的行為,所以不能忽略它們或者簡單地把它們合併到某個實體或者值物件中。當這樣的行為從領域中被識別出來時,最佳實踐是將它宣告成一個服務。這樣的物件不再擁有內建的狀態。它的作用僅僅是為領域提供相應的功能。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為例,下圖給出了劃分的建議:

640?wx_fmt=png

業務視覺化和可配置化

好的領域建模可以降低應用的複雜性,而視覺化和可配置化主要是幫助大家(主要是非技術人員,比如產品,業務和客戶)直觀地瞭解系統和配置系統,提供了一種“code free”的解決方案,也是SaaS軟體的主要賣點。要注意的是視覺化和可配置化難免會給系統增加額外的複雜度,必須慎之又慎,最好是能使視覺化和配置化的邏輯與業務邏輯儘量少的耦合,否則破壞了原有的架構,把事情搞的更復雜就得不償失了。


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


所以我能想到的一種還比較優雅的方式,是通過Annotation註解的方式對領域能力和擴充套件點進行標註,然後在系統bootstrap階段,通過程式碼掃描的方式,將這些能力點和擴充套件點收集起來上傳到中心伺服器,然後再通過GUI的方式呈現出來,從而做到業務的視覺化和可配置化。大概的示意圖如下:

640?wx_fmt=png

有同學可能會問流程要不要視覺化,這裡要分清楚兩個概念,業務邏輯流和工作流,很多同學混淆了這兩個概念。業務邏輯流是響應一次使用者請求的業務處理過程,其本身就是業務邏輯,對其編排和視覺化的意義並不是很大,無外乎只是把程式碼邏輯可視化了,在我們的SOFA框架中,是通過擴充套件點和策略模式來處理業務的分支情況,而我看到我們阿里很多的內部系統將這種響應一次使用者請求的業務邏輯用很重的工作流引擎來做,美其名曰流程可編排,實質上往往是把簡單的事情複雜化了。而工作流是指完成一項任務所需要不同節點的連線,節點主要分為自動節點和人工節點,其中每個人工節點都需要使用者的參與,也就是響應一次使用者的請求,比如審批流程中的經理審批節點,CRM銷售過程的業務員的處理節點等等。

此時可以考慮使用工作流引擎,特別是當你的系統需要讓使用者自定義流程的時候,那就不得不使用視覺化和可配置的工作流引擎了,除此之外,最好不要自找麻煩。當然也不排除有用的特別合適的案例,只是我還沒看見,如果有看見的同學也請告訴我一聲,一起交流學習。

640?

你可能還喜歡

點選下方圖片即可閱讀

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=png

關注「阿里技術」

把握前沿技術脈搏