領域驅動設計(DDD)在京東的落地實踐

你好呀,我是 Bella 醬~

今天給大家分享下領域驅動設計(DDD)在京東的落地實踐。

過去幾年,通天塔一直處於快速的業務能力建設和架構完善的階段,以應對不斷增長的業務需求和容量、高可用等技術需求,現在通天塔平臺已經能滿足集團主站的大部分活動、頻道搭建和運營能力,主流程的新需求越來越少,個性化需求和非標準化流程的數據源和服務接入的需求越來越多,有些甚至是京東零售體系外的,同時通天塔技術和產品也在積極主動尋求變化和創新,這些因素結合在一起驅動通天塔孵化出了一個以技術爲導向的項目:通天塔積木,旨在構建一個基於完全開放的前端 SDK 和後端數據源 & 服務、高度靈活和強大的積木畫布、能夠快速移植和部署到任何第三方 IT 環境的活動搭建解決方案,這套方案的初衷和設計理念也契合了京東國際化賦能和 PaaS 化的戰略。

目前通天塔積木已經取得階段性成果,已開始賦能京東國內和國際站,但如何應對異常複雜的積木業務邏輯和不可預知的業務變化,構建業務和底層技術基礎實施的完全解耦的系統,一直是我們面對的巨大挑戰。也是時候從更高視角來看清問題和源頭,思考 一種能應對和控制業務複雜度、具備強擴展性和彈性的解決方案 。縱觀我們的目標,DDD 這個詞不知不覺映入了我的眼簾。

2004 年著名建模專家 Eric Evans 發表了他最具影響力的書籍《Domain-Driven Design –Tackling Complexity in the Heart of Software》(領域驅動設計—軟件核心複雜性應對之道),書中反覆強調領域通用語言 (Ubiquitous Language) 的重要性,全面闡述了 DDD 戰略設計到戰術設計的方法論和實踐。讓軟件研發所有參與者圍繞着一個統一和一致的領域模型建模和設計,分析模型和設計模型不再割裂,並引出了以領域爲核心的分層架構,有效地分離業務和技術複雜度,使得領域層的代碼和領域模型保持高度一致。在戰術上提供了諸多元模式幫助構建職責清晰、內聚和高維護性和可擴展性的代碼 。

領域驅動設計不是新鮮的概念,至今已有十六年時間,一直來不曾大行其道,直到 IT 行業內掀起微服務的狂潮,技術界才重新審視和意識到領域驅動設計的價值。不能說微服務拯救了領域驅動設計,但確實是微服務,讓領域驅動設計又重新煥發了青春。DDD 是一個非常龐大的建模和設計體系,這篇文章只在理論和概念上闡述 DDD 的價值、方法和架構,歡迎任何的問題指正和補充。

DDD 價值

應對複雜業務

引起軟件系統複雜度的主要因素是需求,軟件系統需求又可以分兩個方面:業務需求和技術需求 。我們分析系統的複雜度時就可以從業務複雜度和技術複雜度這兩個維度出發。

業務複雜度跟系統的業務需求規模和需求之間的關係層級有直接關係,需求的數量和關係的層級決定代碼的規模和邏輯循環或遞歸的層級,系統的需求數量越大,需求之間的關係越複雜,系統的業務複雜度就越大。John Ousterhout 的著作《A Philosophy of Software Design》從認知的負擔和開發工作量的角度來定義軟件系統的複雜度,並給出了一個複雜度公式:

子模塊的複雜度(cp)乘以該模塊對應的開發時間權重值(tp),累加後得到系統的整體複雜度(C)。可以看到系統整體的複雜度並不簡單等於所有子模塊複雜度的累加,還要考慮該模塊的開發維護所花費的時間在整體時間中的權重佔比(tp),這個權重比就跟模塊劃分是否內聚、設計是否優雅有直接關係。

技術複雜度則來自於對軟件系統運行的質量需求,包括安全、高性能、高併發、高可用和高擴展性。系統安全性要求對訪問進行控制,無論是加密還是認證和授權,都需要爲整個系統架構添加額外的間接層。不僅對訪問的低延遲產生影響,還極大提升了系統代碼複雜度;爲了讓後端系統能具備高擴展性和彈性,要求所有系統的設計必須是無狀態的;爲了提升用戶端訪問體驗,後端需要增添離線任務對數據加工、異構、預熱、預緩存,以實現用空間換時間,降低實時接口的邏輯複雜度來降低請求的延遲。然而最讓開發者更抓狂的是這些技術需求彼此又是相互影響甚至相互矛盾,在一些複雜流程並要求高響應的業務場景,如下單、秒殺等,會將一個同步的訪問請求拆分爲多級步驟的異步請求,再通過引入消息中間件對這些請求進行整合和分散處理,這種分離一方面增加了系統架構的複雜性,另一方面也因爲引入了更多的資源,使得系統的高可用面臨挑戰,並增加了維護數據一致性的難度。而且技術複雜度與業務複雜度並非孤立,二者複雜度因子混合在一起產生的負作用更讓系統的複雜度變得不可預期,難以掌控,就好比氫氣和氯氣混合在一起遇到光亮發生爆炸一樣。

DDD 的核心思想就是要避免業務邏輯的複雜度與技術實現的複雜度混淆在一起,確定業務邏輯與技術實現的邊界,從而隔離各自的複雜度,業務邏輯並不關心技術是如何實現的。無論採用何種技術,只要業務需求不變,業務規則就不會變化。理想狀態下,應該保證業務邏輯與技術實現是正交的。

DDD 通過分層架構與六邊形架構確保業務邏輯與技術實現的隔離。

DDD 戰略設計指導我們面對客戶的業務需求,由領域專家與開發團隊展開充分的交流,經過需求分析與知識提煉,獲得清晰的問題域,在引入限界上下文和上下文映射對問題域進行合理的分解,識別出核心領域與子領域,並確定領域的邊界以及它們之間的關係,從而把一個大的複雜系統問題拆分成多個細粒度、獨立和內聚的業務子問題,從而很好地分解和控制業務複雜度,各個小組聚焦各自的子領域中。

在架構方面,通過分層架構來隔離關注點,將領域實現獨立出來,利於領域模型的單一性與穩定性;

引入六邊形架構清晰地界定領域與技術基礎設施的邊界;CQRS 模式則分離了查詢場景和命令場景,針對不同場景選擇使用同步或異步操作,提高架構的低延遲性與高併發能力。

分層架構

“分層架構”遵循了 “關注點分離” 原則,將屬於業務邏輯的關注點放到領域層(Domain Layer)中,而將支撐業務邏輯的技術實現放到基礎設施層(Infrastructure Layer)中。同時,領域驅動設計又頗具創見的引入了應用層(Application Layer)。應用層扮演了雙重角色。一方面它作爲業務邏輯的外觀(Facade),暴露了能夠體現業務用例的應用服務接口;另一方面它又是業務邏輯與技術實現的粘合劑,實現二者之間的協作。下圖展現的就是一個典型的領域驅動設計分層架構。藍色區域的內容與業務邏輯有關,灰色區域的內容與技術實現有關,二者涇渭分明,然後匯合在應用層。應用層確定了業務邏輯與技術實現的邊界,通過直接依賴或者依賴注入(DI,Dependency Injection)的方式將二者結合起來。

六邊形架構

由 Cockburn 提出的六邊形架構則以 “內外分離” 的方式,更加清晰地勾勒出業務邏輯與技術實現的邊界,且將業務邏輯放在了架構的核心位置。這種架構模式改變了我們觀察系統架構的視角。體現業務邏輯的應用層與領域層處於六邊形架構的內核,並通過內部的六邊形邊界與基礎設施的模塊隔離開。當我們在進行軟件開發時,只要恪守架構上的六邊形邊界,就不會讓技術實現的複雜度污染到業務邏輯,保證了領域的整潔。邊界還隔離了變化產生的影響。如果我們在領域層或應用層抽象了技術實現的接口,再通過依賴注入將控制的方向倒轉,業務內核就會變得更加的穩定,不會因爲技術選型或其他決策的變化而導致領域代碼的修改。

快速響應業務變化

不確定性和變化是這個時代的主旋律,業務需要快速上線,並根據用戶的反饋不停地調整和升級,有生命力的業務主動尋求變化,不變則亡是很多行業目前的共識,企業應對變化的響應力成了成敗的關鍵。同時一個長期困擾軟件研發的問題是,需求總是在變化,無論預先設計如何 “精確”,總是發現下一個坑就在不遠處。相信很多技術人員都有這樣的經歷,架構和響應能力越來越糟糕,也就是我們常說的架構腐化了,最後大家不得不接受重寫。軟件架構設計的另一個關鍵方面是讓系統能夠更快地響應外界業務的變化,並且使得系統能夠持續演進。在遇到變化時不需要從頭開始,保證實現成本得到有效控制。

DDD 的核心是從業務出發、面向業務變化構建軟件架構,實質是保證面對業務變化時我們能夠有足夠快的響應能力。面向業務變化而架構就要求首先理解業務的核心問題,即有針對性地進行關注點分離來找到相對內聚的業務活動形成子問題域。讓每個字問題的劃分儘可能靠近變化的原點,子問題域內部是相對穩定的,未來的變化頻率不會很高,是符合深模塊特性的,而子問題邊界是很容易變化的。DDD 最後在實現層面利用成熟的技術模式屏蔽掉技術細節的複雜度。

與微服務相得益彰

Martin Fowler 和 James Lewis 提出微服務時,提出了微服務的 9 大架構特質,指導組織圍繞業務組建團隊,把業務拆分爲一個個業務上高度內聚、技術上鬆散耦合、運行在獨立進程中的小型服務,微服務架構賦予了每個服務業務上的敏捷性和技術上的自主性,因此可以針對每個服務進行獨立地迭代、更新、部署和彈性擴展,從而縮短需求交付週期並加速創新。

在面對複雜業務和快速變化需求時,DDD 從業務視角進行關注點分離和應對複雜度,讓業務具備更高的響應力。DDD 戰略設計階段,引入限界上下文(Bounded Context)和上下文映射(Context Map)對問題域進行合理的分解,確定領域的邊界以及它們之間的關係,維持模型的完整性。

限界上下文不僅限於對領域模型的控制,而在於分離關注點之後,使得整個上下文可以成爲獨立部署的設計單元,這就是 “微服務” 的概念,上下文映射的諸多模式則對應了微服務之間的協作。因此在戰略設計階段,微服務擴展了領域驅動設計的內容,反過來領域驅動設計又能夠保證良好的微服務設計。

邊界給了實現限界上下文內部的最大自由度。這也是戰略設計在分治上起到的效用,我們可以在不同的限界上下文選擇不同的架構模式和技術實現,這也正好映照了微服務的特點:在技術架構上,系統模塊之間充分解耦,可以自由地選擇合適的技術架構,去中心化地治理技術和數據。

ThoughtWorks 公司技術專家編寫的《微服務設計》書中,專門有一章節 “限界上下文”,充分說明微服務的落地需要 DDD 來輔助的,起碼在建模階段是需要藉助 DDD 強大的戰略模式來支撐的。微服務不是簡單的指將服務儘可能的拆小,然後一個 RPC 框架搞定了,這太粗糙了,無法落地。

輔助中臺戰略落地

領域驅動設計讓參與者基於統一語言溝通和協作,圍繞一個統一和一致的領域模型工作,傳統的分析模型和設計模型不再割裂;顯式地把業務領域和設計放到了軟件開發的核心,軟件人員和業務人員合作來構建領域模型,使得軟件的交付質量更高且維護成本更低;利用限界上下文來分解問題域,識別核心領域,有效分解和控制了業務的複雜度;

利用 DDD 提倡的分層、六邊形等架構,分離了業務複雜度和技術複雜度,使得系統具備更強的擴展性和彈性;戰術層面提供了元模型(聚合,實體,值對象,服務,工廠,倉儲)幫助構建清晰、穩定,能快速響應變化和新需求能力的應用;

DDD 構建的應用能快速方便地切到微服務;領域驅動設計給企業應用帶來的穩定性、靈活性、擴展性和應對變化的響應力對於建立靈活前臺、穩固中臺能帶來巨大的幫助作用。

DDD 過程

領域驅動設計是一套面對複雜業務進行建模和設計的方法論和實踐,建立了以領域爲核心驅動力的設計體系。領域驅動設計分爲 2 個主要過程:戰略設計、戰術設計 。

在戰略設計階段 ,面對紛繁複雜的業務需求,領域專家和研發團隊進行緊密合作、充分溝通,進行事件風暴或場景驅動設計,分析需求並提煉知識,得到比較清晰的問題域,輸出由領域專家和研發團隊達成共識的統一語言(UL,Ubiquitous Language),基於統一語言對問題域進行分析和建模,識別業務邊界,確定限界上下文,根據限界上下文劃分獨立的領域,建立限界上下文彼此之間的關係,接着引入系統上下文 (System Context) 確定系統的邊界,並確定它的外部環境,包括與其集成的第三方系統與基礎設施。利用 DDD 分層架構或六邊形架構界定業務領域和技術實現的邊界,讓穩定的核心領域模型處於架構的最內部,避免技術實現和架構變動帶來的影響。

接着進入戰術設計階段 ,一個大的業務問題被分解爲多個限界上下文(問題域),團隊視野和專注就可以聚焦到每一個內聚的限界上下文,進行戰術設計。戰術設計的重點是利用領域驅動設計的元模型對領域的複雜性進行分解和建模。

領域驅動設計強調和突出了領域模型的重要性,通過整個領域驅動設計過程,綁定領域模型和技術模型,以保證領域模型和技術模型在貫穿整個軟件開發的生命週期中(需求分析、建模、架構、設計、編碼、測試與持續重構)的強一致性。領域模型指導着軟件設計以及技術編碼實現,接着通過重構實踐來挖掘隱式概念,完善統一語言和模型,運用設計模式改進設計與開發質量。以下是領域驅動設計的粗略過程:

戰略設計

提煉問題域

回顧我們往日的分析和解決問題過程, 面對複雜問題,很多同學還沒完全理解問題的全貌就已經在提出解決辦法,這些解決辦法只是針對問題的局部,經典圖書《第五項修煉》把這種行爲稱爲 “反應式” 的,碰到一個問題給出一個迴應辦法,而從這些問題整體來看這種方式會阻礙團隊找出最佳解決方案。

DDD 作爲一種建模和架構方法,最大的突破是着重明確了區分了問題域和解決方案域,對業務問題的認知不是技術人員最擅長的,很多研發在碰到需求時,腦子本能就閃現表、類、服務、架構,把解決方案當終極問題來追求,而 DDD 要求研發進行痛苦的蛻變,在業務分析和領域建模階段忘記技術解決方案。同時 DDD 要求領域專家和技術人員坐在一起通力合作、密切溝通來分析和建模,領域專家對業務有着深刻的理解,技術人員擅長技術實現和架構設計,而領域專家和技術人員由於工種的差異導致交流產生障礙,開發人員滿腦子是技術語言,領域專家腦子也都是業務概念,如果按照本能基於自己的專業背景進行溝通,效率太低了,即使有翻譯的角色也會產生理解偏差, DDD 的一個核心原則是所有人員包括領域專家和技術的進行任何溝通都使用一種基於模型的通用語言(UL,Ubiquitous Language),在代碼中也是這樣。

DDD 幫助技術人員對需求進行本質思考和理解,關注點不在是聚焦在功能上,而是理解需求的真正意圖和願景,而非開發一個 feature,更深層次地理解隱含的願景才能開發出真正地解決問題和創造價值的系統來。在提煉問題域過程中,領域專家和技術專家通過充分交流,進行需求分析和知識提煉,獲得清晰的問題子域,識別出核心域、通用域、支撐域。通用域是開發該軟件系統根本競爭力所在,也是領域建模的重心,建議分配最精銳的研發;

通用域 是指多個子域依賴的通用功能子域,比如權限、郵件、日誌系統等;支撐域 是指系統中非核心域和通用域的業務域。

需求分析時從用例開始,列出達成業務目標需要的步驟,切忌跳轉到解決方案上,識別出用於構建模型的知識,通過 UML 表示分析模型和業務模型,形成業務和技術人員達成共識的通用語言。

該階段領域專家只專注於問題域而不是解決方案,業務和技術人員基於 UL 溝通,並且考慮投入產出比,團隊只爲核心業務進行領域驅動設計並創建 UL,訂單系統爲下單模塊進行 DDD,訂單監控模塊用普通的事務腳本方式來即可,我們通天塔的活動模板和積木業務非常複雜和核心,非常適合使用 DDD 來建模和架構設計,而通天塔後端的 Man 系統是面向開發者進行後端和線上業務監控的,進行 DDD 就是小題大做。

識別限界上下文(Bounded Context)

Eric Evans 說:“對一個大型系統,領域模型的完全統一將是不可行的或者不划算的。”。DDD 的構建塊不能盲目地應用在一個無限大的領域模型上,一個無限大的領域模型也無助於我們開發出優質的軟件,限界上下文是分解領域模型的關鍵。限界上下文是一種 “分而治之” 的思維,也是一種高層的抽象機制,讓人們對領域進行本質思考,簡化問題和應對複雜性。

限界上下文如同細胞,細胞是上下文,細胞壁是邊界,細胞內的信息負責對代謝和遺傳進行調控,細胞壁對細胞起着支持和保護防禦的作用,控制物質進出,讓對細胞有用的物質不能出來,有害的物質也不能進入細胞。而領域驅動設計中的限界上下文保證領域模型的一致性和完整性,清晰邊界的控制力保證了領域的安全和穩定。

如何識別限界上下文?

明確了系統的問題域和業務期望後,梳理出主要的業務流程,這些業務流程體現了各種參與者在這個過程中通過業務活動共同協作,最終完成具有業務價值的領域功能。業務流程結合了參與角色(Who)、業務活動(What)和業務價值(Why)。在業務流程的基礎上,我們就可以抽象出不同的業務場景,這些業務場景又由多個業務活動組成,可以利用領域場景分析方法剖析場景,以幫助我們識別業務活動,例如採用用例對場景進行分析,此時,一個業務活動實則就是一個用例。業務流程是一個由多個用戶角色參與的動態過程,而業務場景則是這些用戶角色執行業務活動的靜態上下文。

接下來,我們利用領域場景分析的用例分析方法剖析這些場景。通過參與者(Actor)來驅動對用例的識別,這些參與者恰好就是參與到場景業務活動的角色。根據用例描述出來的業務活動應該與統一語言一致,最好直接從統一語言中擷取。一旦準確地用統一語言描述出這些業務活動,我們就可以從語義相關性和功能相關性兩個方面識別業務邊界,進而提煉出初步的限界上下文。

從不同角度看待限界上下文,限界上下文會呈現出對不同對象的控制力。

DDD 驅動我們把每一個限界上下文設計成一個個 “自治” 的單元,自治要滿足四個特點:

最小完備是基礎,只有賦予了限界上下文足夠的信息,才能保證它的自我履行。穩定空間與獨立進化則一個對內一個對外,是對變化的有效應對,而它們又是通過最小完備和自我履行來保障限界上下文受到變化的影響最小。

上下文映射

限界上下文僅是一種對領域問題域的靜態劃分,還缺少一個重要的關注點,即:限界上下文之間是如何協作的?當我們發現彼此協作存在問題時,說明限界上下文的劃分出現了問題,也是識別限界上下文的一種驗證方法。Eric Evans 將這種體現限界上下文協作方式的要素稱之爲 “上下文映射(Context Map)”,並給出了 9 種上下文映射關係:

Open Host Service 相當於微服務之間的協作關係;防腐層(Anti-Corruption)是一種高度防禦性的策略,結合門面(Facade)模式和適配器(Adapter)設計模式,將模型與其需要集成的其他模型隔離開來,以防止被頻繁變更或不穩定的依賴模型污染和腐敗。

架構設計

“DDD 不需要特殊的架構,只要是能將技術問題與業務問題分離的架構即可。” -- Eric Evans

傳統的三層架構分而治之、降低耦合、提高複用,但存在弊端,業務邏輯在不同層泄露,導致替換某一層變得困難、難以對核心邏輯完整測試。領域驅動設計給出了 DDD 分層架構、六邊形架構、整潔架構等分層架構,它們遵循 “關注點分離” 原則,旨在分離和隔離業務複雜度和技術複雜度,凸顯了領域模型,保證了領域模型的穩定性和一致性。

DDD 分層架構

DDD 分層架構將屬於業務邏輯的關注點放到領域層(Domain Layer)中,將支撐業務邏輯的技術實現放到基礎設施層(Infrastructure Layer)中,DDD 創新性地引入了應用層(Application Layer),應用層扮演了兩重角色。一作爲業務邏輯的門面(Facade),暴露了能夠體現業務用例的應用服務接口,又是業務邏輯與技術實現的粘合劑,實現二者之間的協作。下圖展現的是一個典型的領域驅動設計分層架構。藍色區域和業務邏輯相關,灰色區域與技術實現相關,二者涇渭分明,然後匯合在應用層。應用層確定了業務邏輯與技術實現的邊界,通過直接依賴或者依賴注入(DI,Dependency Injection)的方式將二者結合起來。

我們詳細介紹 DDD 分層架構中每一層的用意和設計:

表現層(User Interface Layer):負責向用戶顯示信息和解釋用戶命令,完成前端界面邏輯應用層(Application Layer) 很薄的一層,負責展現層與領域層之間的協調,不包含任何的業務邏輯和業務規則,也不保留業務對象的狀態,是對領域服務的編排和轉發。應用層扮演了兩重角色。一作爲業務邏輯的門面(Facade),暴露了能夠體現業務用例的應用服務接口,又是業務邏輯與技術實現的粘合劑,實現二者之間的協作。一個 Application Service 代表一個 Use Case,一個 Use Case 代表了一個完整的業務場景,對於外部的客戶來說,應用層是與客戶協作的應用服務,接口代表是業務的含義。

我們知道 DDD 分層架構的主要目標是分離業務複雜度與技術複雜度,應用層扮演的就是這樣的分界線。從設計模式的角度來理解,應用層的 Application Service 是一個 Facade,對外部客戶,作爲代表 Use Case 的整體應用,對架構內部,它負責整合領域層的領域邏輯與非業務相關的橫切關注點。

應用中,存在與具體的業務邏輯無關,在整個系統中會被諸多服務調用的橫切關注點實現,他們在職責上是內聚的,散佈在所有代碼層次中,包括異常處理、事務、監控、日誌、認證和授權等。所以與橫切關注點協作的服務應被定義爲應用服務。

領域層(Domain Layer),是業務軟件的核心所在,也是軟件架構的核心,包含了業務所涉及的領域對象(實體、值對象)、領域服務,負責表達業務概念、業務狀態信息以及業務規則,具體表現形式就是領域模型。領域驅動設計提倡富領域模型,將業務邏輯歸屬到領域對象上。基礎設施層(Infrastructure Layer):基礎層爲各層提供通用的技術能力,包括:爲應用層傳遞消息、提供 API 管理,爲領域層提供數據庫持久化機制等。它還能通過技術框架來支持各層之間的交互。

整潔架構(Clean Architecture)

整潔架構中,同心圓代表應用軟件架構的不同部分,也是一種以領域模型爲中心的架構,從裏到外依次是 Entities、Use Cases、Interface Adapters、Frameworks and Drivers。整潔架構明確了各層的依賴關係,越往裏,依賴越低,越抽象,外圓代碼依賴只能指向內圓,內圓不知道外圓的任何事情。

六邊形架構(Hexagonal Architecture)

又稱爲端口 - 適配器,六邊形架構也是一種分層架構,不是從上下或左右分,而是從內部和外部來分。六邊形架構在領域驅動設計和微服務架構設計中扮演了較重要的角色。六邊形架構將系統分爲內部(內部六邊形)和外部,內部代表了應用的業務邏輯,外部代表應用的驅動邏輯、基礎設施(諸如 REST,SOAP,NoSQL,SQL,Message Queue 等)或其他應用,UI 層、DB 層、和各種中間件層實際上是沒有本質上區別的,都只是數據的輸入和輸出。內部通過端口和外部系統通信,端口代表了一定協議,以 API 呈現。

一個端口對應多個適配器,對應多個外部系統,對這一類外部系統的歸納,不同的外部系統需要使用不同的適配器,適配器負責對協議進行轉換。六邊形架構有一個明確的關注點,一開始就強調把重心放在業務邏輯上,外部的驅動邏輯或被驅動邏輯存在可變性、可替換性,依賴具體技術細節。而核心的業務領域相對穩定,體現應用的核心價值。六邊形的六並沒有實質意義,只是爲了留足夠的空間放置端口和適配器,一般端口數不會超過 4 個。適配器可以分爲 2 類,“主”、“從”適配器,也可稱爲 “驅動者” 和“被驅動者”。

代碼依賴只能使由外向內。對於驅動者適配器(也稱主適配器,Driving Adapter),就是外部依賴內部的。但是對於被驅動者適配器(也稱次適配器,Driven Adapter),實際是內部依賴外部,這時需要使用依賴倒置,由驅動者適配器將被驅動者適配器注入到應用內部,這時端口的定義在應用內部,但是實現是由適配器實現。

CQRS(命令與查詢職責分離)

CQRS 使用分離的接口將數據查詢操作 (Queries) 和數據修改操作 (Commands) 分離開來,這也意味着在查詢和更新過程中使用的數據模型也是不一樣的,這樣讀和寫邏輯就隔離開來了。使用 CQRS 分離了讀寫職責之後,可以對數據進行讀寫分離操作來改進性能,可擴展性和安全。DDD 和 CQRS 結合,可以分別對讀和寫建模:

查詢模型是一種非規範化數據模型,不反映領域行爲,只用於數據查詢和顯示。命令模型執行領域行爲,在領域行爲執行完成後通知查詢模型。如果查詢模型和領域模型共享數據源,則可以省略這一步;如果沒有共享數據源,可以藉助於發佈訂閱的消息模式通知到查詢模型,從而達到數據最終一致性。對於寫少讀多的共享類通用數據服務(如主數據類應用)可以採用讀寫分離架構模式。單數據中心寫入數據,通過發佈訂閱模式將數據副本分發到多數據中心。通過查詢模型微服務,實現多數據中心數據共享和查詢。

通天塔從系統維度對數據庫進行了讀寫分離,通天塔的 C 端應用和服務大部分是讀場景,CMS 是多寫應用,所以 CMS 的寫走主庫,讀服務按照使用場景不同訪問不同的從庫,實時請求、同步數據到集市、數據中心等,這點也從數據庫基礎架構上保證了通天塔系統的低延時和穩定。

綜述

六邊形架構的內部六邊形、DDD 分層架構的領域層和應用層、以及整潔架構 Use Cases 和 Entities 區域實現了核心業務邏輯。但是核心業務邏輯又由兩部分來完成:應用層和領域層邏輯。領域層實現了最核心的業務領域部分的邏輯,對外提供領域模型內細粒度的領域服務,應用層依賴領域層業務邏輯,通過服務組合和編排通過 API 網關向前臺應用提供粗粒度的服務。業務需求變幻莫測,但我們總能在這些變化找出一些規律,用戶體驗、操作交互、以及業務流程的變化,往往只會導致 UI 層和流程的變化,總體來說,不管前端和外部如何變化,核心領域邏輯基本不會大變。把握好這個規律,我們就知道如何設計應用層和領域層,如何進行邏輯劃界了。架構模型正是通過分層方式來控制需求變化對系統的影響,確保從外向裏受的影響逐步減小。面向用戶端的展現層可以快速響應外部需求進行調整和發佈,靈活多變;應用層通過服務組合和編排實現業務流程的快速適配上線,以滿足不同的業務場景;領域層是經過抽象和提煉的業務原子單元,是非常穩定的。這些架構設計的好處是可以保證領域層的核心業務邏輯不會因爲外部需求和流程的變動而調整,對於建立前臺靈活、中臺穩固的架構能力是很有好處的。下面是 Herberto Graca 的一張包含了六邊形、整潔、CQRS 等架構的綜合圖,全面的說明了這些架構的設計要點和不同的出發點。

戰術設計

戰略設計爲我們提供一種高層視角來審視我們的軟件系統,而戰術設計則將戰略設計的成果具體化和細節化,它關注的是單個限界上下文內部技術層面的實施。DDD 給我們提供了一整套技術工具集,包括實體、值對象、領域服務和資源庫等,如下:

行爲飽滿的領域對象

讓我們先看幾個概念:

脹血模型是顯而易見不可取的,這裏不做過多討論。失血模型是絕大數企業開發應用的模式,一些火熱的 ORM 工具比如 Hibernate,Entity Framework 實際上助長了失血模型的擴散,而且傳統三層架構中的服務層,承受了太多的職責,如事務管理、業務邏輯、權限檢查等,這違反了單一職責原則和關注分離原則,並且產生了大量的依賴和循環依賴,當業務複雜度上升時,服務層所包含的代碼將會非常龐大和複雜,直接導致了維護成本和測試成本的上升。同時也會導致業務邏輯、狀態會散落到在大量方法中,原本的代碼意圖會漸漸不明確,我們將這種情況稱爲由失血癥引起的失憶症,它會導致系統變得愈發複雜和難以維護。

採用領域模型的開發方式,將數據和業務邏輯封裝在一起,從服務層移動到領域將業務邏輯模型中,這樣服務層可以只負責應用邏輯(事務、日誌、認證、監控、編排等),領域模型可以專門負責其相關的業務邏輯,相關的業務分別內聚到不同的領域模型中,與現實領域的業務對象映射,一些很有可能重複的業務代碼都會被集中到一處,降低重複代碼,提升業務邏輯的複用、可測試性和維護性。貧血模型和充血模型都是滿足數據 + 行爲的,應該採用哪種模式,大家這是一個爭論了曠日持久的問題,關注點還是在於領域模型是否要依賴持久層,我個人還是偏重於貧血模式,依賴持久層就意味着單元測試的展開要更加困難,而且領域對象的生命週期應該交給外部模型才更合理。

領域驅動設計元模型

實體(Entity) 實體是一種具有唯一身份標識的對象,具有持續的生命週期,除唯一標識其他屬性是可變的。實體通過它的唯一標識被區分。例如實體訂單 Order,標識爲 oderId,通天塔的活動實體 Activity,標識爲 activityId。

模型關係

對象概念

VO(View Object):視圖對象,用於展示層,它的作用是把某個指定頁面(或組件)的所有數據封裝起來。DTO(Data Transfer Object)數據傳輸對象,分佈式應用提供粗粒度的數據實體,也是一種數據傳輸協議,以減少分佈式調用的次數,從而提高分佈式調用的性能和降低網絡負載,這裏泛指用於展示層與服務層之間的數據傳輸對象。RPC 對外暴露的服務涉及對象 API 就是 DTO,如 JSF(京東 RPC 框架)、Dubbo。對比 VO:絕大多數應用場景下,VO 與 DTO 的屬性值基本一致,但對於設計層面來說,概念上還是存在區別,DTO 代表服務層需要接收的數據和返回的數據,而 VO 代表展示層需要顯示的數據。

DO(Domain Object):領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。DO 不是簡單的 POJO,它具有領域業務邏輯。PO(Persistent Object):持久化對象。

對比 DO:DO 和 PO 在絕大部分情況下是一一對應的,但也存在區別,例如 DO 在某些場景下不需要進行顯式的持久化,只駐留在靜態內存。同樣 PO 也可以沒有對應的 DO,比如一對多表關係在領域模型層面不需要單獨的領域對象。

下面是這些對象在系統架構中的分佈:

Domain Primitive

Domain Primitive 是一個在特定領域裏,擁有精準定義的、可自我驗證的、擁有豐富行爲和業務邏輯的 Value Object,DP 使用業務域中的原生語言,可以是業務域的最小組成部分、也可以構建複雜組合。Domain Primitive 是 Value Object 的進階版,在原始 VO 的基礎上要求每個 DP 擁有概念的整體,而不僅僅是值對象。在 VO 的 Immutable 基礎上增加了 Validity 和行爲。在項目中,散落在各個服務或工具類裏面的代碼,都可以抽出來放在 DP 裏,成爲 DP 自己的行爲或屬性。原則是:所有抽離出來的方法要做到無狀態,比如原來是 static 的方法。如果原來的方法有狀態變更,需要將改變狀態的部分和不改狀態的部分分離,然後將無狀態的部分融入 DP。因爲 DP 也是一種 Object Value,本身不能帶狀態,所以一切需要改變狀態的代碼都不屬於 DP 的範疇。Domain Primitive 涉及三種手段:

讓隱性的概念顯性化(Make Implicit Concepts Explicit)通天塔活動類型就是一個簡單的 int 類型,屬於隱式概念,但活動類型包含了很多相關業務邏輯,比如類型名稱,不同類型活動具有獨特的 Icon,判斷活動類型是否是判斷等,我們把活動類型顯性化,定義爲一個 Value Object。

讓隱性的上下文顯性化(Make Implicit Context Explicit)當要實現一個功能或進行邏輯判斷依賴多個概念時,可以把這些概念封裝到一個獨立地完整概念,也是一種 Object Value:

封裝多對象行爲(Encapsulate Multi-Object Behavior)常見推薦使用 Domain Primitive 的場景有:

有格式要求的 String:比如 Name,PhoneNumber,OrderNumber,ZipCode,Address 等。

限制的 Integer:比如 OrderId(>0),Percentage(0-100%),Quantity(>=0)等。

可枚舉的 int:比如 Status(一般不用 Enum 因爲反序列化問題)。

Double 或 BigDecimal:一般用到的 Double 或 BigDecimal 都是有業務含義的,比如 Temperature、Money、Amount、ExchangeRate、Rating 等。

複雜的數據結構:比如 Map<String, List> 等,儘量能把 Map 的所有操作包裝掉,僅暴露必要行爲,如通天塔的活動 Map 類。

接口變得清晰可讀,校驗邏輯內聚,在接口邊界外完成,無膠水代碼,業務邏輯清晰可讀,代碼變得更容易測試,也更安全。

最後

DDD 不是一套框架,而是一種面向複雜問題的建模方法論和實踐,所以在代碼層面缺乏了足夠的約束,導致 DDD 在實際應用中上手門檻很高,甚至可以說絕大部分人都對 DDD 的理解有所偏差。

而且 DDD 諸多實踐在真正踐行時面臨很多挑戰,

通天塔後端團隊在高併發和高性能應用構建方面有着非常豐富的經驗,但在 DDD 實踐和享受到它的巨大價值層面我們還是剛起步,千里之行始於足下,我們正在邁出堅實一步,後續我們也會出 DDD 通天塔實踐篇,講述我們的經驗和心得。

作者 | 京東零售技術

出品 | 物流 IT 圈

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7jeRBvr_7bqNj1NuOOwFJQ