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

clipboard.png

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

今天咱們邀請阿里高級技術專家張建飛,一塊兒來聊聊爲何須要領域建模,什麼是好的模型,又該如何搭建。服務器

爲何要領域建模?

軟件的世界裏沒有銀彈,是用事務腳本仍是領域模型沒有對錯之分,關鍵看是否合適。實際上,CQRS就是對事務腳本和領域模型兩種模式的綜合,由於對於Query和報表的場景,使用領域模型每每會把簡單的事情弄複雜,此時徹底能夠用奧卡姆剃刀把領域層剃掉,直接訪問Infrastructure。我我的也是堅定反對過分設計的,所以對於簡單業務場景,我強力建議仍是使用事務腳本,其優勢是簡單、直觀、易上手。但對於複雜的業務場景,你再這麼玩就不行了,由於一旦業務變得複雜,事務腳本就很難應對,容易形成代碼的「一鍋粥」,系統的腐化速度和複雜性呈指數級上升。數據結構

目前比較有效的治理辦法就是領域建模,由於領域模型是面向對象的,在封裝業務邏輯的同時,提高了對象的內聚性和重用性,由於使用了通用語言(Ubiquitous Language),使得隱藏的業務邏輯獲得顯性化表達,使得複雜性治理成爲可能。talk is cheap,直接上一個銀行轉帳的例子,對事務腳本和領域模型進行比較,孰優孰劣一目瞭然。架構

銀行轉帳事務腳本實現

在事務腳本的實現中,關於在兩個帳號之間轉帳的領域業務邏輯都被寫在了MoneyTransferService的實現裏面了,而Account僅僅是getters和setters的數據結構,也就是咱們說的貧血模式。框架

clipboard.png

上面的代碼你們看起來應該比較眼熟,由於目前大部分系統都是這麼寫的。需求評審完,工程師畫幾張UML圖完成設計,就開始向上面這樣懟業務代碼了,這樣寫基本不用太費腦,徹底是面向過程的代碼風格。有些同窗可能會說,我這樣寫也能夠實現系統功能啊,仍是那句話「just because you can, doesn't mean you should」。說句很差聽的,正是有這麼多「沒有追求」、「不求上進」的碼農才形成了應用系統的混亂、敗壞了應用開發的名聲。這也是爲何不少應用開發工程師以爲工做沒意思,技術含量低,以爲成天就是寫if-else的業務邏輯代碼,系統又爛,工做繁瑣、無聊、沒有成長、沒有成就感,因此轉向去作中間件啊,去寫JDK啊,以爲那個NB。學習

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

銀行轉帳領域模型實現

若是用DDD的方式實現,Account實體除了帳號屬性以外,還包含了行爲和業務邏輯,好比debit( )和credit( )方法。編碼

clipboard.png

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

clipboard.png

而Domain Service只須要調用Domain Entity對象完成業務邏輯便可。設計

clipboard.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,這樣才能更好的表達業務,其演化過程以下:

clipboard.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爲例,下圖給出了劃分的建議:

clipboard.png

業務可視化和可配置化

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

在可擴展設計中,我已經介紹了咱們SOFA架構是如何經過擴展點的設計來支撐不一樣業務差別化的需求的,那麼能否更進一步,咱們將領域的行爲(也叫能力)和擴展點用可視化的方式呈現出來,並對於一些不須要編碼實現的擴展點用配置的方式去完成呢。固然是能夠的,好比仍是開篇轉帳的例子,對於透支策略OverdraftPolicy這個業務擴展點,新來一個業務說透支額度不能超過1000,咱們能夠徹底結合規則引擎進行配置化完成,而不須要編碼。

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

clipboard.png

有同窗可能會問流程要不要可視化,這裏要分清楚兩個概念,業務邏輯流和工做流,不少同窗混淆了這兩個概念。業務邏輯流是響應一次用戶請求的業務處理過程,其自己就是業務邏輯,對其編排和可視化的意義並非很大,無外乎只是把代碼邏輯可視化了,在咱們的SOFA框架中,是經過擴展點和策略模式來處理業務的分支狀況,而我看到咱們阿里不少的內部系統將這種響應一次用戶請求的業務邏輯用很重的工做流引擎來作,美其名曰流程可編排,實質上每每是把簡單的事情複雜化了。而工做流是指完成一項任務所須要不一樣節點的鏈接,節點主要分爲自動節點和人工節點,其中每一個人工節點都須要用戶的參與,也就是響應一次用戶的請求,好比審批流程中的經理審批節點,CRM銷售過程的業務員的處理節點等等。

此時能夠考慮使用工做流引擎,特別是當你的系統須要讓用戶自定義流程的時候,那就不得不使用可視化和可配置的工做流引擎了,除此以外,最好不要自找麻煩。固然也不排除有用的特別合適的案例,只是我還沒看見,若是有看見的同窗也請告訴我一聲,一塊兒交流學習。

本文做者:張建飛

詳情請閱讀原文

相關文章
相關標籤/搜索