從軟體複雜度的角度去理解DDD

来源:https://www.cnblogs.com/88223100/archive/2023/04/06/Understanding-DDD-from-the-perspective-of-software-complexity.html
-Advertisement-
Play Games

從我們作為業務開發主要的職責深入到DDD的本質是什麼?複雜度應處理?規範設計怎麼做?本文將全方位為大家解答。 ...


從我們作為業務開發主要的職責深入到DDD的本質是什麼?複雜度應處理?規範設計怎麼做?本文將全方位為大家解答。

一、作為業務開發,我們的主要的職責是什麼的

 

業務開發的職責

在文章的開始我想和大家一起思考一個問題:作為一個工程開發,我們最主要的職責是什麼? 
我極度認可 <<淺談什麼是技術一號位>>文章的觀點 - 切實解決業務問題才是每一個工程開發最主要的職責 - 所以每個業務開發都必須要結合業務的視角去思考自己系統的建設和發展,而不是只是做一個“編程的”碼農。
這裡摘錄一下文章中要點
  • 技術一號位是負責使用技術能力解決業務問題,提供穩定可靠的技術支撐;
  • 負責向業務各方提供各種必要的技術支撐,通過合理的數據分析為業務決策提供依據;
  • 通過對技術領域的積累和發展,通過業務領域的理解和落地影響業務決策;
  • 負責構建梯隊完整、能力全面、制度完善的技術團隊來支撐業務發展。
文中也提到了雖然不是每個人都負責一塊完成的業務,也不是每個人都帶領團隊,但是至少每個人都是自己所負責的那塊系統的技術一號位。

 

業務在實際開展中遇到的問題

那實際業務開展中,業務到底會遇到有哪些問題呢?我們按業務的生命周期進行切分,然後具體查看每個業務生命周期的訴求:
  • 業務啟動期:業務能力快速搭建 - 系統提供快速試錯的能力
  • 業務發展期:業務能力擴展 - 系統需要支持原來越多的業務功能
  • 業務平臺期:業務能力複製 - 系統需要支持原來越多的業務場景
  • 業務衰退期:業務能力創新 - 系統提高生產力延長業務的生命周期

圖片

我們技術要做的事情是:在業務驗證沒有問題的情況下,如果儘可能的延長業務的發展和平臺期,讓業務獲取的利益最大化。所以為了支持業務的發展,業務的本身的功能支持訴求以及業務對技術的要求也會越來多,在這種情況下考驗軟體開發人員的一個非常關鍵的能力就是: 軟體複雜度的控制的能力

 

軟體複雜度

軟體複雜度其實是一種多維度的概念,其可能來源於多個方面,前阿裡資深技術專家李運華在他的《從0開始學架構的》課程中從6個方面闡述了軟體複雜度【2】,列舉如下:
  • 高性能
    • 單機性能
    • 集群性能
  • 高可用

    • 計算高可用
    • 存儲高可用
  • 可擴展性

  • 低成本
  • 安全
  • 規模
    • 業務規模
    • 系統物理規模

二、DDD的本質是什麼

DDD本質上我認為就是一種減低軟體複雜度的手段, 其推薦的方法論可以適用於上麵包括了業務規模,可擴展性兩個維度的複雜度應對。其實業務規模的複雜度的處理包括了對可擴展性的支持。
DDD實施給系統之後,我們依然需要關註系統其它的複雜度,這裡列舉一些示例措施:
  • 容量規劃
  • 架構設計
  • 資料庫設計
  • 緩存設計
  • 框架選型
  • 發佈方案
  • 數據遷移、同步方案
  • 分庫分表方案
  • 回滾方案
  • 高併發解決方案
  • 一致性選型
  • 性能壓測方案
  • 監控報警方案
那麼我們進一步對業務規模的複雜度進行拆解,又分為下麵兩類:
1、領域複雜度
  • 領域模型描述問題域的準確性

2、技術實現的複雜性

代碼沒有按照業務綁定的”分析模型”去編碼,軟體變成一個大泥潭
  • 軟體的可擴展性較差
  • 軟體變成面向過程
  • 分層不合理
  • 沒有規範

那DDD是如何處理上面提到的軟體複雜度的?

  • 提供了一個領域劃分的方法:讓軟體系統產生邊界。
  • 提供一個一系列的戰略模式:限界上下文的映射,分層架構等。
  • 提供一個一系列的戰術模式:如何規劃領域層 內部

DDD不是什麼?

  • 不光光只是一種編程方法
  • 不光光只是一種架構風格
  • 不具體指導如何具體建模

三、複雜度處理-領域模型描述問題域的準確性

DDD的原名是模型驅動的設計方法:通過領域模型(Domain Model)捕捉領域知識,使用領域模型構造更易維護的軟體。

 

合理性證明

圖片
DDD的核心思想,大家都清楚,就是分析模型要和代碼模型保持一致。 
那麼如果不保持一致到底會產生什麼樣的負面影響
圖片
如果技術實現和業務實現不在用一水平線上,那技術模型的行進路線只會考慮劈開技術障礙並且可能會撞在未來的業務障礙的牆上。這樣就很容易出現,業務持續演進等技術想實現的時候,卻發現當前的實現依賴於“業務不會這樣發展”的假設上。這也是為什麼會出現現在眾多業務需求,技術無法實現或者是需要花大量時間去實現的原因。
但是如果技術和業務通過統一語言打破知識的壁壘保持一致,那麼如果後面技術遇到問題即是業務碰到的問題,業務人員需求的變更和迭代會自然而然的幫助技術同學越過一些門檻。也就是說業務方與技術方參與到對方的工作中,就在雙方之間帶來了更好的協同,形成1+1>2的功效。

 

什麼是問題域

根據百度百科的解釋【3】 在軟體工程中,問題域是指待開發系統的應用領域,即在客觀世界中由該系統處理的業務範圍
圖片
那麼問題域內的組成是什麼呢?就是我們的域模型。 
這裡直接摘抄一段前阿裡P10"阿白"在阿裡內部發表的域模型的觀點:
域模型(domain model)英文又稱為問題域模型(problem space model)。維基百科(Wikipedia)對它的定義是” A conceptual model of all the topics related to a specific problem” 可以翻譯成:“域模型是針對某個特定問題的所有相關方面的抽象模型”。這個定義有幾個要點:第一是“特定問題”, 也即是說域模型是針對性某個問題域而言的, 脫離的這個特定問題,域模型的構建其實不存在一個最優或者是最合理的構建。第二是抽象, 域模型是一個抽象模型, 不是對某個問題的各個相關方面的一個映射, 也不是解決方案的構建。 
圖片

 

如何實現問題域的分析

在 DDD 中,Eric Evans 提倡出一種叫做知識消化(Knowledge Crunching)的方法幫助我們去提煉領域模型。簡單來說就是五個步驟:
  • 關聯模型與軟體實現;
  • 基於模型提取統一語言;
  • 開發富含知識的模型;
  • 精煉模型;
  • 頭腦風暴與試驗。

圖片

開發人員和業務專家在一起通過一個個業務用例仔細討論應用程式的應用場景,從而使得業務人員深刻理解業務知識,開發人員和業務人員就重要的業務概念建立起統一的語言,開發人員將這些概念根據業務用例的上下文抽象出模型,並且這些模型將會最終成為最終軟體實現中的領域模型。隨後隨著更多的業務用例的輸入,開發人員和業務人員會逐漸對已經構建的模型進行精化,並且也會用新的用例去檢驗之前構建模型的合法性和適用性。
DDD在這一步其實沒有給出詳實標準的如何建模的方法,畢竟建模還是來自於每個人的世界觀,其過程還是傾向於經驗的。但是還是有不少人總結一些標準的建模方法論例如:

1 四色原型法  

http://apframework.com/2020/03/22/ddd-color/

2 用例分析法 

https://baike.baidu.com/item/%E7%94%A8%E4%BE%8B%E5%88%86%E6%9E%90/2859078?fr=aladdin

 

問題域的拆分

大家應該發現上面的知識消化的流程是一個非常耗時和複雜耗腦力的過程, 涉及到產品,業務,技術等多方團隊, 所以為了讓有限的資源投入到最最核心的子域,我們需要對問題域進行這份,把重點的精力放到最核心的領域上。
核心領域一定是業務價值最高的,而非技術難度最高或者是基礎設施框架部分。 
要切分問題域,首先需要瞭解問題域的種類:

1 通用域: 非應用獨有的,多個應用都會有的功能。例如發送郵件,觸達等

2 核心域:和競爭對手區別開來的區域,或者是在市場上被賦予了競爭優勢的區域。

3 支撐子域:其餘的區域
如何確定核心域,這裡有幾個提示:
  • 系統哪部分最難用
  • 手動處理過程阻止了他們進行了根據創造性, 有附加值的工作
  • 哪些修改能提高收益
  • 哪些修改能提高運營效率

取哪些提示,取決於業務系統的性質。

那如何決定支撐子域/通用子域,以及支撐子域/通用子域的切分呢?
目前在我查閱的資料中,還暫時沒有人提及到具體的操作方法,感覺主要還是依靠經驗主義在做劃分。我個人總結了一個方法,主要就是就是關註業務的核心實體和核心流程。以核心實體和核心流程作為切分支撐子域的基礎。
核心實體:核心實體是存在於核心流程中,對核心流程的決策和扭轉可能起到關鍵的作用。有的時候業務上為了能讓核心實體在業務流程中起到更大或者更高效的作用,會添加一些讓核心實體更好服務於業務流程一些業務功能,從而使業務實體從整體上看變得相對複雜,這個時候我們應該以核心實體為基礎進行切割,把所有和核心實體CRUD相關的操作還有讓其變得更高效的業務功能劃分為單獨的一個領域。
核心流程: 當某個業務流程足夠複雜也可以當成一個子域。
在實現領域驅動設計【4】書中,提到了為線上拍賣網站系統劃分問題域的一個例子,我們以此來驗證上面等構想
圖片
劃分子域
圖片
  • 賣家 + 會員身份:這兩者都是核心實體,網站可能為了讓促進會員能夠多參與拍賣可能提供了分層,或者積分等工功能。網站為了能讓賣家能夠更加提供更加有拍賣價值或者是轉化率高的品類可能為賣家提供了數據分析等業務功能。 
  • 名冊:這也就是核心實體,網站會對名冊提供一系列拍賣相關的功能,例如倒計時,一口價等,所以也需要形成一個領域。 
  • 拍賣:網站最核心的業務流程,核心域無疑。
  • 爭議解決:買賣家的售後衝突解決流程向來很複雜,所以會獨立成為一個域無疑。

四、複雜度處理-進一步降低問題域的複雜度-限界上下文

 

限界上下文的誕生背景

一般情況下,一個複雜系統由一系列的模型來表示解答域, 理想狀態是一個子域一個模型。但是有些當業務需要且系統複雜的時候,一個模型可能被多個域共用,這個時候這個模型的概念可能變得不清楚。因此為了保護這些模型概念的完整性, 清晰的定義模型的責任邊界很重要。
實現領域驅動設計【4】書中舉了下麵這個例子:
圖片
為了維護模型的概念的完整性,最直觀的方法就是為這個模型化一個邊界,e.g. 這個商品所表現的意思就是履約的時候用到的"商品",而不是下單的時候的"商品"。只要有一個這樣的邊界定義,系統就會但是出現多個邊界,畢竟"商品"在不同業務上下文中有不同的含義, 例如庫存域的貨品,物流域的運輸品, 價格域的商品等等。這樣的一個邊界就是DDD的“限界上下文”。 
限界上下文給人直觀的感受其實和子域很像,我很早以前曾讀過一些關於微服務的書籍,也提到過要把DDD中的限界上下文作為微服務劃分的重要依據。這裡其實就給我很大的疑惑:
1 限界上下文到底是怎麼劃分的?我們劃分限界上下文難道真的是用一個基礎概念,然後找這個基礎概念不同的“上下文”嗎?
2 限界上下文和子域到底區別是啥?

 

限界上下文的本質

DDD理論中提到了DDD的四個邊界 
圖片
所以在DDD中是把限界上下文作為某個子域的內部模塊的劃分,其實無論是子域的劃分,限界上下文的識別,和聚合的劃分他們的本質是一樣的,他們都是對複雜問題的分解之後,然後歸類分組。只不過“聚合”面向的是領域層內部,“領域”劃分面向的是業務問題域,而“限界上下文”面向的是解答域,但是我跟傾向於把限界上下文理解為更加深一層次的業務問題域的劃分,而不是面向的解答域。 
如果這樣看的話,那麼其實就可以回答上面的疑問, 領域和限界上下文沒有本質的區別,就像樹的父節點和位元組點一樣都是樹節點。而限界上下文的劃分完全可以使用子域劃分的理論。(可以回顧下上面問題域拆分的段落)

 

上下文映射

上下文的映射是什麼, 簡單來說就是描述不同上下文之間的關係的描述。舉個例子
圖片
DDD對於限界上下文直接提煉了幾種方式,這裡這邊阿裡內部文章《領域驅動設計:軟體複雜性應對之道》解釋的比較好,描述如下:
shared kernel
共用內核 shared kernel :通常是共用核心領域或者是一組通用子領域。
圖片

customer/supplier

客戶/供應商關係 customer/supplier:上下游關係。不同客戶需要協商來平衡,上游團隊需要有自動測試套件。
圖片

conformist

跟隨者模式 conformist:單方面跟隨模式。上游的設計質量較好,容易相容,可以採用嚴格遵循上游團隊的模型。
圖片

anticorruption layer

防腐層 anticorruption layer:防腐層、隔離層,使用 facade or adapter 等模式。可以減少其它系統變動對本系統的影響。
圖片

separate way

各行其道 separate way:聲明一個與其它上下文毫無關聯的 bounded context,使開發人員能夠在這個小範圍內找到簡單、專用的解決方案。
圖片

open host service

開放主機服務 open host service:開放子系統供其他系統訪問。其核心思想是開放出一個標準的各個領域都認可的協議,減輕各個領域實施ACL的負擔和成本。
圖片

published language

共用語言 published language:把一個良好文檔化、能夠表達領域信息的共用語言作為公共的通信媒介,必要時在其它信息與該語言之間進行轉換。
在當前電商領域的範疇,目前我個人覺得只有ACL,Seperate Way, publish language 有比較好可行性,其他的關係都不是很靠譜:
  • shared kernel:如果使用共用二方庫,誰來維護這個二方庫,如何防止在不同上下文使用不同kernal版本所帶來的問題。 
如果一定能保證shared kernel的維護在一個團隊內,且所有使用shared kernel版本一定能保持一致, 那是可以使用的。 
  • customer/supplier:我曾經因為匯率包升級而去重構一個應用,因為匯率包變更太大,且應用沒有防腐層,所以不論從開發還是測試都是非常痛苦的過程。
  • conformist:和customer/supplier類似, 但是在互聯網領域沒有靠譜的設計, 只有有人維護和沒有人維護的設計。conformist從長期來看其實就是customer/supplier。
  • Open Host Service 沒有任何一個領域保證自己的介面一定不會變,就算不會,其他領域的同學會相信嗎,他們會忍住不用ACL嗎?如果他們用ACL,OHS的意義何在?
  • publish language 目前阿裡內部MTOP,TOP等協議正是使用這樣的協議。

另外限界上下文之間真的能夠隨便無規則無條件的互相依賴,互相調用嗎?在下麵的章節將會解釋論述。

五、複雜度處理 - 分層不合理

架構分層主要的作用就是關註點隔離,如果和今天的話題聯繫起來就是領域模型和技術的關註點隔離(領域和存儲,領域和展示)。

 

傳統的三層架構

圖片
這種傳統架構的缺點
1、業務邏輯層和數據訪問層有明顯的耦合。 
2、沒有領域的概念,所有的邏輯沉澱到service中。
所以傳統架構只能針對小型的,沒有過多的業務邏輯場景。由於這種架構能夠保有領域能力的沉澱,所以在現在電商業務場景基本不會被使用。

 

六邊形架構

Alistair Cockburn在 2005 年時演示了 六邊形架構
圖片
圖片
從六邊形架構開始,其強調了領域模型。並且確立了領域模型的核心位置,以及其不應該依賴於其他的層次。六邊形架構也強化了適配器的概念,其還把適配器分類為input適配器,和output適配器。所有input適配器用於對接不用的外部請求形式, 所有output適配器用於對外部的依賴 (e.g. 資料庫, 外部服務,記憶體調用等)
這種結構模式樹立了以領域模型為核心的先河,但是其忽略了在領域層中跨模型業務邏輯的實現方式-領域服務的沉澱。這也間接導致了其需要強化應用層,並且通過應用層和output適配器的聯合去完成一些可以應該在領域層應該完成的事情。 

 

洋蔥架構

圖片
洋蔥架構的提出更加進化了一步,推出了域服務層,並且支持域服務層是支持了那些需要多個領域實體聯合中作用的領域邏輯. 其層次由外向內依次是領域模型,領域服務,應用服務和外層的基礎設施和用戶終端。其依賴的關係也只能是由外向內. 在洋蔥結構中其把存儲層,文件系統和網路服務放到了基礎設施層。由於基礎設施和用戶終端一樣在最外層,所以洋蔥架構也提倡用依賴倒置來解決應用邏輯和基礎設施的耦合問題。
洋蔥架構的架構圖從其依賴順序上來看,其依賴應用層必須先依賴域服務層,再依賴域模型層, 這樣很容易造成領域模型的邏輯外泄到領域服務層,造成領域模型變成貧血模型。

 

DDD 架構

 

圖片
DDD的架構大家都非常熟悉了,領域服務和領域模型都歸屬域領域層。適配層依賴域應用層,應用層依賴域領域層,也可以直接直接調用基礎設施層(大多數是查詢場景)。領域層理論上不依賴於任何層次,其通過依賴倒置和基礎設施層產生關聯。在DDD架構中,應用層是可以通過直接訪問聚合根(某個實體類),併進行方法的執行和操作的。應用層也可以直接訪問基礎設施層。可以看出DDD的架構其實更加的貼切實際一些。 
圖片
上面這張圖也很好的闡述了DDD各個架構層次依賴的關係。

 

CQRS

 

很多情況產品構建出來的數據展示,需要橫跨幾個領域的數據的支撐,也就是我們日常構建的大寬表,在這種情況使用CQRS模式可以完美解決這個問題。其主導視圖模型和領域模型分開,讓領域模型更加專註業務邏輯,流程和規則而非業務視圖。 
圖片
CQRS的思想很簡單,就是把服務中對數據的更新操作(Command)和讀取操作(Query)分離, 一部分邏輯只處理和數據更新有關的業務,另外一部分只處理和數據讀取有關的邏輯。這種處理方式,可以讓我們辛苦構建的領域模型不被業務中所需要的這類視圖需求所干擾。 
CQRS 的兩種實現方式
基於event- sourcing
圖片
不基於event - sourcing
圖片
以上的圖片摘自於文章《CQRS模式及其應用》

 

我們團隊裡面的架構實踐

 

在我們自己的應用中我們構建了基於COLA【5】規範的層次架構,如下圖: 
圖片
我們也對自己的架構定義的一些額外的規範:

1、依賴關係(除了依賴倒置)只能是從上當下;

2、同層之間永遠不能互相依賴;

3、如果同層之間需要互相用到對方的服務,那麼就需要下沉出一層。例如在上圖中,我們的業務層就分為了兩層 "Executor"層次和 “Handler”層此, Handler層次用來保存業務的一些通用邏輯。

六、複雜度 - 軟體變成一個大泥潭

從這一章節開始介紹DDD的"戰術模式",也就是向大家介紹DDD是如何構建和組織自己領域層的。值得一提的是在DDD中,領域的劃分, 領域層次的建立, 領域之間關係的建立我們一般叫做DDD的"戰略模式",而此章節提到的值對象,實體,域服務,工廠,repository, 聚合/聚合根, 領域事件等都是DDD的戰術模式。戰略模式的重要性是要遠大於DDD的戰術模式的,我們如果在領域劃分,領域通信協議,分層方面沒有大的問題, 那麼即使再糟糕系統整體也還是可控的。 
在領域層面, DDD通過聚合/聚合根的概念來劃分單個領域中的類似於類集合的邊界,從而降低單個領域層的複雜度。DDD通過實體,值對象,領域服務,repository, factory 來規劃集合內部的類組織, 另外DDD也通過領域事件來處理領域之間的交互,來匹配非同步和需要解耦的業務場景。

 

實體

 

當我們需要考慮一個對象的個性特征,獲取需要區分對象的時候,就需要引入實體。一般我們發現實體概念,是在和業務產品人員或者領域專家討論發現的那些需要有唯一標示性或者生命周期連續性很重要的時候。
舉個例子加入用戶需要預定酒店,如果領域專家說了我們定了A酒店了,就不能定B酒店了,哪怕A,B其他的屬性完全一樣。從領域專家扣中我們可以識別出酒店是有唯一標示性的,且哪怕A,B屬性一樣,也不能認為A,B 是一樣的,這也說明瞭酒店的唯一性不是從屬性來的。這兩點我們可以推斷酒店是一個實體。唯一標示性可以是現實有意義的,例如工商註冊號,也可以無現實意義,例如資料庫主鍵。 

實體建模的註意點

1、為實體分配唯一標識符
  • 現實意義標識符
  • 人工生成的標識
    • 自增
    • guid/uuid/
    • 資料庫主鍵
    • 自定義sequence

2、驗證和不變行

實體必須自己負責自己保持自己狀態的合法性 (validation) 和不變性(Invariants)。他們的區別是合法性是根據上下文的,而不變性是不用考慮上下文且必須正確的。例如酒店必須有房間這個就是不變性,而酒店的營業時間就是validation. 一般使用規則和規約模式來實現validation和invariants。

規模模式: 

https://baijiahao.baidu.com/s?id=1717403406288752234&wfr=spider&for=pc
3、聚焦在行為,而不是屬性狀態
不要暴露屬性給外面,如果外面得到屬性,很可能就自己實現了一些領域邏輯,那麼領域邏輯就外漏了。 
4、把一些行為邏輯下方到值對象中
需要警惕實體邏輯膨脹,從而混繞了實體所要表達的概念。 
例如預定是一個實體, 現在要加上邏輯預定的天數不能小於N天。這個時候我們可以為Booking 抽象出 Stay 對象,讓Stay對象去管理規則邏輯。而不是讓預定這個實體去做。讓預定只關註預定。 
5、不要為世界建模
不要過度設計,只要滿足需求就好。不要讓技術需求污染領域設計,除非真的萬不得已。
6、分散式的設計
不需要讓領域概念橫跨多個bounded context, 如果我們域模型所涉及的概念橫跨了,我們就需要用兩種設計方法.
  1. 只是用id引用
  2. value objects

 

值對象

 

什麼時候需要使用到值對象?

  • 概念需要凸顯的時候。 
e.g. 拍賣系統的能夠一口價獲取拍賣的價格, 就算是我們用一個int 就能表示也需要用類來凸顯概念
public class WinningBid
{
...
public int Price { get; private set; }
...
}
  • 表述一個描述性的,但是沒有實體編號的概念的時候。 

值對象的特征

  • 無標識:他們只是標識對象的屬性。
  • 基於屬性的相等性: 所有的屬性值相等即值對象相等
  • 富含行為:值對象實現業務概念的抽象,其也有自己的行為
  • 內聚:將不同的相關屬性組成一個概念整體,例如Money, 是由一個long 和一個currency組成的
  • 不變性:值對象是不變的對象,如果需要改變屬性,那最好是建立一個新的對象並且進行值對象替換。如果一定是需要改變,那就需要考慮設置為值對象是否合理。
不變性是值對象非常重要的一個屬性,是可以保障值對象不會被"壞味道"代碼侵入的一個原則之一。 例如如果一個值對象引入了另外一個類實例, 另外一個值對象也引入了相同的類實例, 如果值對象允許改動,當一個值對象對這個類實例的內容進行修改,勢必會影響另外一個值對象。 所以最安全的方式還是通過對象替換的方式。

 

域服務 

 

什麼時候用域服務

發現和多個實體相關聯,但是放入任何一個單獨的實體都不適合,這個適合用域服務.

域服務應該包含什麼內容

域服務應該包含業務/系統流程和業務規則,不應該包含技術的元素在內,技術的元素都應該在業務服務(Application Service)中實現。

應用服務與領域服務的區別:

一些可以在網上搜索到的老生常談:
  • 應用服務里不要處理業務邏輯,只在領域服務里處理業務邏輯。(如何判斷某段邏輯是否是業務邏輯?)
  • 領域服務掌握領域知識,而應用服務只是對領域服務的編排。
  • 應用服務是領域服務的客戶方,也就是說應用服務會調用領域服務里的方法。
  • 當領域中的某個操作過程不屬於實體或者值對象的職責時,需要將個操作放在領域服務中。而且確保領域服務是無狀態的(這句話很有意思,也就是說領域服務中不應該有任何記錄狀態的行為,在任何情況下調用這個服務,它都不會有副作用,也就是說它是個純記憶體操作)。
  • 領域服務中包含的是業務邏輯,而應用服務關註的應該是安全和事務等非業務邏輯。
  • 對事務的管理絕對不能放在領域服務層,事務管理需要放在應用服務層。因為和領域模型相關的操作的粒度都很細,無法用於事務管理。而且領域模型也不應該意識到事務的存在。
  • 通常的可以放在應用服務中的邏輯有:參數驗證、錯誤處理、監控日誌、事務處理、認證與授權。
除了第一條之外,上面的條例只是舉出了應用服務與領域服務兩者非常易於告之的差別,但是會有一些“業務邏輯”比較難以取捨, 例如
例如轉賬操作:

A ->B

A.accountDecrease(10);

B.accountIncrease(10);
我們在現在可以非常肯定的說上面的轉賬一定在領域服務,因為"轉賬"就是一個領域概念。但是如果假設世面上所有的銀行以前只有存錢和取錢兩種功能,"轉賬"是一個新概念和業務的時候,技術就沒有那麼容易判別轉賬是一個臨時性的一次性的需求,還是會長久發展。這個時候技術有兩個選擇:
1、構建轉賬域服務,讓應用服務調用域服務
2、讓應用服務獲取A,B的實體,然後在應用層直接調用方法,在應用層做事務保持一致性。
如果域能力在其他團隊手裡,我相信大多數的團隊會使用第二種。那遇到這種情況我們到底應該怎麼辦?我個人的意見和阿裡前技術高級專家張建飛在他的文章《一文教會你如何寫複雜業務代碼》的觀點保持一致:

圖片

我們在新邏輯出現難以判斷的時候優先講能力放入到app層,如果我們發現會有第二個業務場景使用到了相同的能力,就需要考慮是否應該把此能力下發到領域層以增強內聚性和復用性。

 

工廠

 

  • 只負責複雜邏輯對象的構建,讓構建邏輯中心化
  • 減少外部對象對構建對象內部變數的理解
  • 工廠方式不是在任何構建對象的時候使用,一定用在對象構建邏輯複雜,有子依賴或者是有invariant規則的場景。 

 

Repository

 

reponsitory主要用來處理集合根的存儲和獲取的。提供一個facade介面且是面向domain層的,是domain model和data model的橋梁。其最大的作用就是通過反向依賴的方式充分隔離數據層和領域層。Repository最常見的用法是被applicaiton service層去使用獲取聚合根。
在repository實現中,我們一般會有下麵的一些邏輯:
  • uniqe ID 的生成
  • 資料庫的操作
  • 數據模型到領域模型的相互
  • 橫跨多個數據模型構建出一個實體模型。

repository的反模式

  • 定義出比較通用化的介面, e.g.
List<Customer> findBy(CusomterQuery query)
  • 使用了延遲載入,延遲載入就是設計錯誤的標誌, 有可能說明我們聚合的邊界不催。 
  • 不要為了報表的訴求使用reponsitory, 領域的case和業務報告很不一樣,可能需要多個聚合的數據,這種情況可以考慮用一個離線的store去做,和其他的讀服務去做,不見得一定需要用領域的Repository模式。

 

領域事件

 

領域事件所想要解決的問題其實和metaQ消息機制想要解決的問題一致,都是跨領域驅動型業務邏輯實現的最佳方法,讓領域和領域之前解耦。
領域事件消費教科書的說法是可以在領域層, 也可以在應用服務層, 但是我覺得領域消息如果用metaQ是這樣的消息中間件去實現的話,那用領域層和應用服務層去消費就不是很方便,有可能破壞一些分層原則。所以我個人傾向於在adapter層去承接消費,統一化掉。
當前可以實現領域事件和消費比較方便的工具有:
  • google guava - EventBus
  • COLA框架 - 事件支持

 

聚合/聚合根

 

聚合是什麼?

其實聚合的原理和領域劃分,限界上下文劃分的原理是一致的,都是為了通過歸類分組的方式讓整個系統巨集觀上 N * N 的關係複雜度減低為 T * T 的複雜度。 
T遠小於N。 
聚合前: 
圖片
聚合後
圖片

如何劃分聚合

1、根據業務規則和不變數來決定。例如 customer 聚合是有一套業務規則來維持的,例如信用卡要存在,必須先有一個customer, 有customer 必須有address, address必須有code.
2、強關聯的對象應該放在一塊。什麼是強關聯,那就是必鬚生命周期是一樣的。例如customer和creditcard在,電商網站中,如果customer被刪除了,那麼他的信用卡也應該被設置為失效的。而訂單和客戶不一樣,客戶下了個訂單,然後客戶註銷,但是訂單還是一直存在的。所以customer 和 creditcard 在一個聚合中, 而用戶和訂單則不在。訂單裡面可以有客戶的ID或者是一個值對象。 
3、靈活設置:有些可以根據業務情況,進行可以靈活的設置,下麵列舉一個論壇系統,帖子和回覆聚合思考的例子:
大家都知道一個帖子有多個回覆,沒有帖子,回覆就沒有意義;所以很多人就會認為帖子應該聚合回覆;但實際上不需要這樣,如果你這樣做了,那對於一個論壇來說,同一個帖子被多個人同時回覆的可能性是非常高的,那這樣的話,多個人同時回覆一個帖子,就會導致多個人同時修改同一個帖子對象,那就導致大家都回覆不了,因為會有併發衝突或者資料庫事務的等待超時,因為大家都在修改同一個帖子聚合根;實際上如果我們從業務規則的角度去思考一下,那可以發現,其實帖子和回覆之間,只有一個簡單的規則,那就是回覆一旦被創建,那他所對應的帖子不能被修改即可;這樣的話,要實現這個規則其實很簡單,把回覆作為聚合根,然後把帖子傳入回覆聚合根的構造函數,然後回覆保存帖子ID,然後回覆將帖子ID設置為不允許外部修改(private set;即可),這樣我們就實現了這個業務規則,同時還做到了多人同時推一個帖子回覆時,不會對同一個帖子對象就併發修改,而是每個回覆都是並行的往資料庫插入一條回覆記錄即可。-- 摘自阿裡內部文檔<<關於DDD領域驅動設計中聚合設計的一些思考>>

聚合設計的原則

  1. 聚合是用來封裝真正的不變性,而不是簡單的將對象組合在一起;
  2. 聚合應儘量設計的小;
  3. 聚合之間的關聯通過ID,而不是對象引用;
  4. 聚合內強一致性,聚合之間最終一致性;

什麼是聚合根

聚合根就是聚合的入口,聚合外部只能通過聚合根和聚合內部通信。由於聚合外部只能通過聚合根和聚合內部通信, 這也就意味著外部不能操作除聚合根以外的任何類進行資料庫操作,因為這樣有可能會導致破壞業務的規則。舉個例子,一個汽車四個輪子,如果我們用 Repo 直接操作輪子,對輪子採取delete,而這個時候汽車對象的狀態卻可能是 "running".

如何選出聚合根

1、聚合根一定至少有一個對應的datastore 

2、聚合根一定更夠完全描述一組後者多組業務規則(invariant)。絕對不會存在一個業務規則需要多個聚合根聯合作用才能做判斷的。

3、聚合根一定有自己的獨立的生命周期。 

七、規範設計

項目/應用的規範設計的長期價值一定是不可忽視的,規範就相當於一個架構內部組件的收納的容器,架構指定了這些組件在邏輯上的組織形式,但而規範則是則是妥妥的物理組織形式。如果沒有合理清晰的規範設計,項目很快就會因為個人開發習慣的不同,導致物理結構混亂,給接下來的應用/項目的維護和擴展產生影響。
規範設計總共分為兩類:

放對位置

  • 程式架構目錄

貼好標簽

  • 類名約定
  • 方法名約定
  • 錯誤碼約定
  • Domain Event約定
  • 測試約定

下麵是我們小團隊參考了阿裡COLA框架建議的命名規範做出的開發規範:

 

程式架構組織

 

層次
包名
功能
Adapter層
web
處理頁面請求的Controller
mtop
處理mtop的請求
hsf
處理hsf的請求
scheduler
處理定時器的請求
message
處理消息的請求
App層
executor
處理request,包括command和query
convertor
包含轉換層類的目錄
interceptor
包含攔截器目錄
extpoint
擴展點目錄
extention
擴展實現目錄
handler
app 層的一些中間處理邏輯
Domain層
model
領域模型
 
ability
領域能力,包括DomainService
gateway
領域網關,解耦利器
extpoint
擴展點目錄
extention
擴展實現目錄
Infra層
gatewayimpl
網關實現
mapper
ibatis資料庫映射
config
配置信息
Client SDK
api
服務對外透出的API
dto
服務對外的DTO

 

類命名規範

 

種類
對象
示例
API class
增刪改服務入參
XXXCmd. 
e.g. MetricsRegistrationCmd
查詢服務入參
XXXQry. 
e.g. ListAllMetricsQry
API service
XXXServiceI.java
e.g. MoMetricsAdminServiceI
出參
如果是無需返回返回使用Cola框架的Response, 
如果是單個概念返回使用Cola框架的SingleResponse, 
如果是多個概念返回使用Cola框架的MultiResponse. 
如果概念是某個明確的對象,例如MultiResponse<MoMetricsCO>
如果是複合對象, 使用XXXResult 例如
MultiResponse<ListMetricsValuesResult>
領域層
實體
XXXE.java
值對象
XXXV.java
工廠
XXXFactory.java
Reponsitory
XXXRepository.java
域服務
XXXDomainService.java
防腐服務
XXXGateway.java
枚舉
XXXEnum.java
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Vue.js 3.0 是 Vue.js 框架的最新版本,於 2020 年 9 月正式發佈。Vue.js 3.0 主要的改進和新特性包括: 更好的性能:Vue.js 3.0 使用了更快的虛擬 DOM 實現,比 Vue.js 2.0 更快。 更小的體積:Vue.js 3.0 的代碼體積比 Vue.js ...
  • 1 <template> 2 <el-form ref="form" :model="form" :rules="rules" label-width="100px"> 3 <div v-for="(input, index) in inputs" :key="index"> 4 <el-form- ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 瞭解 console ● 什麼是 console ? console 其實是 JavaScript 內的一個原生對象 內部存儲的方法大部分都是在瀏覽器控制台輸出一些內容 並且還提供了很多的輔助方法 ● 最常見的 console 我們在開發 ...
  • 基於 Node.js、Express.js 和 MongoDB 通過Mongoose驅動進行 REST API 開發的輕量級樣板。集成了Swagger UI、JWT、session、發送郵箱驗證、日誌管理、統一的預定義狀態碼響應格式等,對於為前端平臺構建純凈的Web API非常有用。 ...
  • 前面已經介紹過CSS偽類的知識,具體可見前文 CSS偽類知識詳解。 偽元素常常被誤解為偽類,主要在於他們的語法相似,都是對於選擇器功能的擴展,相似程度很高導致被混淆。 本文通過詳細介紹偽元素和常見的使用方法,最後也會分析下偽元素與偽類的基本區別。 基本描述 CSS偽元素也是應用於選擇器的關鍵字,允許 ...
  • MVC模式(Model-View-Controller):是一種前端和後端都廣泛應用的設計模式。它將應用程式的業務邏輯、數據表示和用戶界面分離,使得開發人員可以獨立地修改各部分而不影響其他部分。MVC設計模式有助於提高代碼的可讀性、可維護性和可重用性。 MVC是Model-View-Controll ...
  • 如何為組件添加 CSS 的 class? 傳遞一個字元串作為 className 屬性: render() { return <span className="menu navigation-menu">Menu</span> } CSS 的 class 依賴組件的 props 或 state 的情 ...
  • 過濾器模式(Filter Pattern)或標準模式(Criteria Pattern),是一種結構型模式。這種模式允許使用不同的標準條件來過濾一組對象,並通過邏輯運算的方式把各條件連接起來,它結合多個標準來獲得單一標準。 例子將創建一個 Person 對象、Criteria 介面和實現了該介面的實... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...