從程式碼戰術角度解釋領域驅動設計 - Cyrille
在您當前的應用程式中,您的業務邏輯有多複雜?它的範圍可能從微不足道到極其複雜。人們不應該為一個微不足道的問題使用複雜的工具。
我們大多數人,包括我自己,都習慣於編寫所謂的事務指令碼。我們編寫一個控制器來處理請求、處理資料庫並返回結果,所有這些都直接在控制器內部進行。
對於大多數簡單的用例來說都很好。它的可讀性很強,幾乎不可測試,而且都在一個地方。
它只是完成工作。但是有很多警告:
- 程式碼根本不反映域,只有函式名createUser說明發生了什麼
- 該方法處理不同層次的抽象。從一個物件中提取資料是一個低階操作,做一個SQL查詢就更底層了。這是兩個不同層次的細節。
- 該方法有太多責任:它處理從請求中獲取輸入、進行資料庫查詢和傳送響應。您必須瞭解許多實施細節:此程式碼明確使用 SQL 資料庫,並且可能透過 HTTP API 呼叫進行呼叫。
我們不會一次解決所有這些問題。但是我們可以從向程式碼新增更多域模型開始,進入下一步:Active Records。
Active Record 是一個物件,是資料庫中一行的直接表示(如果您使用的是 NoSQL,則為文件)。它嵌入了可用於改變其屬性的setter和gettersave ,以及一種將更改持久儲存到資料庫的方法。
透過這個小動作,我們抽象出了資料庫實現細節。無論使用者如何儲存,只要知道該save方法會為您完成就足夠了。
但是現在我們有另一個問題:邏輯重複。如果另一個用例需要進行相同的操作(例如,從管理皮膚建立使用者),您別無選擇,只能複製並貼上相同的程式碼。
聽起來不是很重要嗎?那是因為我們的用例非常簡單。如果註冊實際上是一項複雜的操作,需要傳送電子郵件、OTP、簽署檔案、建立實體、填寫 CRM、通知報告和分析等等,該怎麼辦?突然之間,您需要做很多工作來確保一致性。
這就是您進入 DDD 領域的地方。作為非常簡單的第一步,您可以轉換您的 Active Record 並讓它封裝域邏輯而不是使用 setter。
我們得到了更少的重複和更高的可讀性,因為方法名稱揭示了之前被命令式程式碼(what)隱藏的意圖(why)。除其他好處外:
- 您可以透過以下方式測試您的域模型User.createAccount
- 需求的變化被本地化到一個單一的類(使用者)
- 實施起來仍然簡單快捷
我們正處於DDD 的邊緣。您可以更進一步,將使用者建立程式碼包裝在Service中。現在,您有一個呼叫Service的Controller ,這是大多數後端開發中非常常用的模式。每一層都有責任:
- 控制器捕獲請求,將其分派到正確的服務並在傳送回之前格式化響應。
- 該服務接受請求,對其進行清理並執行。
大多數簡單的工作流都對此設定感到滿意。它是可測試的並且已充分分解。然而,這可能還不夠:訂閱新使用者所產生的副作用(例如傳送電子郵件、填寫 CRM ……)又如何呢?
有兩種處理方法:
- 同步地,在登入程式碼之後
- 與事件驅動架構非同步
同步法簡單快捷,但容易產生大泥團。此外,它使請求越來越長,這是不必要的,因為初始命令已經完成(使用者已建立)。顯然,它無法擴充套件。
非同步方法在 DDD 中得到了緩解:當使用者被建立時,我們派發一個域事件,稱為UserCreated通知我們的系統關於新使用者的資訊,並允許其他感興趣的各方對此事件做出反應。這種方法稱為事件驅動架構,是一整套實踐(尤其是CQRS,一種在 DDD 社群中佔有重要地位的模式)的切入點。
我們很快就從事務指令碼轉到了事件驅動。至此,我們的需求已經滿足了,沒必要再深究了吧?
但不要忘記:我們在這裡談論的是 DDD。
交易事務邊界
除非我們正在處理事務邊界,否則我們不會談論下一步。
簡而言之,事務邊界封裝了必須持續更新的元素(關聯式資料庫中的行、NoSQL 資料庫中的文件等)。要麼事務作為一個整體成功,要麼回滾到之前的一致狀態(稱為Atomicity的特性,即[url=https://en.wikipedia.org/wiki/ACID]ACID[/url]中的 A )
假設您正在管理一組使用者,並且您的程式碼將一個新使用者新增到該組中。您首先載入使用者組,確保您可以新增此使用者(例如,透過檢查該使用者不在該組中),然後新增它。
如果沒有事務邊界,當這個用例被同時呼叫多次時會出現很多問題。例如,在組中新增同一使用者的兩次呼叫可能會導致該組具有兩次相同的使用者。你可能不想要的東西。
透過事務邊界, 資料庫訪問層 (DAL)和資料庫本身採用一組複雜的技術來確保您一致地更新資料。 在此示例中,如果兩個事務對同一組進行操作,則第一個提交將成功,但第二個將失敗,因為資料庫檢測到更改發生在第二個提交之前,從而導致回滾。
這就是聚合發揮作用的地方。
聚合
最難掌握的概念之一(連同限界上下文)是聚合。但既然我們已經看到了事務邊界,就更容易理解聚合的需求了。
回到我們的例子,我們的服務將:
- 取一組
- 驗證我們要新增的使用者不在組中
- 將使用者新增到組
但是我們剛剛看到如果沒有事務邊界,這會如何變壞。那麼,現在我們有了解決問題的策略(想法),它如何轉化為具體程式碼?答案是聚合。
聚合的作用是封裝必須被視為單個單元的實體。在我們的例子中,我們必須更新一組使用者,所以我們管理兩個實體:組和使用者。聚合本身有一個介面:
- 新增和刪除組的使用者
- 設定使用者數量限制
- 確保組中沒有兩次相同的使用者
- 以及其他業務定義的不變數。
當然,您必須始終使用聚合來對組進行操作。您不能在其他地方人為地更改事務指令碼中的組,這是出於我們首先編寫聚合的非常準確的原因:保持資料和程式碼的一致性。
每個聚合都有一個稱為聚合根的霸主,其職責是維護聚合的完整性。在這裡,聚合根是Group。
現在有一個問題:每個實體要麼屬於一個聚合,要麼是聚合的聚合根。在我們的例子中:
- GroupUserAggregate是一個聚合,其中Group是根,這個聚合允許我們對管理組內的使用者進行操作
- UserAggregate是一個以User為根的聚合,你透過UserAggregate對單個使用者進行操作
如您所見,一個 User 屬於許多聚合,因為它可以在不同的上下文中以不同的方式使用:
- GroupUserAggregate管理組內的使用者並在組內執行規則(新增/刪除使用者,更改組名)
- UserAggregate管理使用者並圍繞使用者本身執行規則(例如更改名稱、電子郵件地址/密碼等)
架構模式
事情變得越來越複雜,我們的戰術工具集演變為:
- 一個控制器
- 一項服務
- 事件分發器
- 許多聚合體
我們還沒有完成一半,但這些涵蓋了大部分可用的實現模式:
- 事務指令碼只是實現你的行為的方法,沒有域封裝。您直接訪問資料庫(例如使用原始 SQL)
- Active Records將資料訪問抽象為代表資料庫中一行的物件
- 領域模型將行為封裝在具有代表業務的精確名稱和用例的類中(您不再使用 setter,而是使用清晰的域名)
但這些不是架構,它們是實現模式:它們是實現域本身的工具。
另一方面,架構模式是支援這些實現的工具:
- 事務指令碼最常透過出色的3 層架構(表示、業務、資料)實現。呼叫控制器的 UI/CLI 是表示層,控制器內部的程式碼是我們的業務層,我們的資料庫儲存資料層。
- Active Records也可以透過 3 層模式充分實現,但有些人喜歡使用4 層架構,在表示和業務之間新增一個應用程式(或服務)層。
- 域模型本身就是另一塊蛋糕,因為它經常與六角形架構一起使用(稱其為Clean、Onion等)。您的域變得越來越不瞭解實現細節(例如資料庫或第三方 API 呼叫),並且越來越依賴介面(透過神奇的控制反轉模式)。
現在架構模式是另一個完全獨立於領域驅動設計的主題,因為你真的可以使用上述任何架構來構建領域模型。六邊形架構非常適合 DDD,因為它最大限度地提高了領域模型的獨立性。但這不是必需的,特別是如果您已經有一個程式碼庫並打算逐步遷移它。
不要讓任何人讓你認為沒有完整的DDD工具,你就不能做一個合適的領域模型。
限界上下文呢?
限界上下文就像 Monad。一旦你明白了,就很難向別人解釋。希望有界上下文不是內函子類別中的么半群。
甚至我自己也很難給出一個令人滿意的定義,而其他人提供的定義也無濟於事:
- 限界上下文是領域驅動設計的核心模式,Martin Fowler。
- 有界上下文只是應用特定域模型的域內的邊界,Microsoft。
- 有界上下文定義了某些子域適用性的有形邊界。這是一個特定子域有意義而其他子域沒有意義的區域。瓦迪姆薩莫欣。
也許對你來說太大了
讓我們縮小並離開程式碼區域,純粹從戰略的角度思考:我們正在為之編碼的業務如何運作。
對我來說最困難的事情之一是“上下文”的概念。這是一個沒有規模意義的詞:相對於我們的應用程式的上下文有多大?
如果你有一個非常非常大的公司/軟體有多個領域,那麼限界上下文通常是有意義的。讓我們來處理一個電子商務系統的案例。這樣的軟體可以處理:
- 目錄管理
- 存貨
- 訂購
- 支付
- 船運
- 支援
所有這些區域(我們稱它們為子域)都屬於一個大區域:Commerce Bounded Context。與此同時,您的企業可能還有其他顧慮:廣告管理(在 Google Ads 和 Facebook Ads 上)、客戶管理(透過 CRM)、分析等。所有這些都是其他的上下文Bounded Contexts。
無處不在的語言
每個限界上下文都有一種通用語言:它是在每個限界上下文中工作的人們用來談論某事的語言:一個實體、一個事件、一個動作……
通常,當人們在Commerce Bounded Context中談論 Users 時,他們與Customer Management中的談論方式不同。
- 在 Commerce中,我們談論身份驗證機制、付款和送貨地址等。
- 在 CRM中,我們談論他從哪裡來,他買東西的頻率,它的滿意率等等。
它們在概念上共享相同的身份:Commerce 中的使用者與 CRM 中的使用者相同,但它們攜帶不同的資訊,並且在這些領域工作的人們對它們的談論也不同。
但是Analytics也有一個 User 實體,這個與 Commerce & CRM Users 非常不同。在這裡,使用者是與網站互動的任何人,例如甚至沒有適當身份的簡單訪問者。
所以你看,簡單的User這個詞,有時可以用不同的資訊描述同一個人(身份),或者完全不同的人。一種限界上下文與另一種限界上下文之間的通用語言可以完全不同或相對接近。
但實際上,什麼是限界上下文?
到目前為止,我給出的是啟發式而不是定義。獲得這些知識後,理解限界上下文的定義可能會更容易。引用微軟:
有界上下文只是應用特定領域模型的領域內的邊界
就是這樣:Commerce 限界上下文中有關使用者的程式碼將不同於 CRM 限界上下文中的程式碼。
- 在 Commerce中,我們將有方法來驗證和建立帳戶、關聯支付資訊、傳送折扣券等。
- 在 CRM中,我們將有方法來獲取使用者、支付和訂單的歷史記錄、聯絡資訊(如果銷售代理聯絡過他)以及之前代理輸入的註釋的歷史記錄。
在 CRM 中獲取有關使用者的身份驗證資訊是沒有意義的。在 Commerce 中獲取銷售代理呼叫歷史記錄是沒有意義的。
限界上下文字質上非常大,您希望它儘可能大以最小化限界上下文之間的互動(防止耦合)。
你說的領域模型?
啊,你終於注意到領域模型的使用了,對吧?在我們的示例中,我們有一個存在於我們域中的使用者實體。但是不同的元件將與使用者的不同部分進行互動:一些會談身份驗證,其他的個人資訊和其他支付歷史。這是同一使用者域實體上的 3 個不同的域模型。
領域模型只是對 Domain 的同一元素的不同檢視。六個盲人將以相同的方式觸控大象的不同部分。
子域
對於大型限界上下文,您有稱為子域的細分。在 Commerce 示例中,Inventory 和 Authentication 是兩個子域。每個子域都適合由單個團隊負責(工作分工的另一種啟發式)。
子域分為三類:核心、支援和通用。在定義它們之前,讓我們(再次)看一些啟發式方法。
- 核心:它們是您域的秘訣。他們處理對您的業務至關重要的職責和用例,使您的業務與眾不同並有助於實現利潤最大化的事情。
- 支援:那些是支援您的業務核心但不提供任何特定競爭優勢的子域。他們通常會解決顯而易見但不是很複雜的問題。
- 通用:它們是已經解決的明顯問題(由第三方或開源解決)。您的每個競爭對手或多或少都以相同的方式解決問題,不值得投入時間。
請注意,我們仍在戰略性地討論:子域甚至可能不是技術程式碼。
讓我們以您銷售手錶的電子商務為例,假設它是一個奢侈手錶電子商務。
- 核心:您的核心子域是手錶製造。因為這就是您與競爭對手的區別:手錶的質量。您可能正在製作自己的手錶,因為這是您的秘方,或者讓製造商使用某種特殊配方為您製作。
- 支援:所有編目、庫存管理和訂購系統都是支援子域:它們不提供任何特定的競爭優勢,但它們支援公司的核心業務。它們專門與您的電子商務業務相關聯。您可以在內部解決它們,也可以外包。
- 通用:支付和身份驗證是眾所周知的,解決了大多數公司必須解決的問題。在內部實施它們沒有任何好處,而是由專門的實體實施。
如您所見,我們的核心子域沒有程式碼表示(您沒有影響手錶建立的程式碼)。最終,您作為 CEO 決定他想要軟體來幫助建立手錶,突然間您開始擁有代表這個核心子域的程式碼。這個決定涉及另一個:製造限界上下文。
現在,即手錶核心子域,跨越了多個有界上下文:
- 商務BC:關於手錶是如何製造的知識以及證明它是頂級質量手錶的論據是商務的一部分
- 製造BC:製造手錶的知識以及如何確保其質量、透過控制和指揮各個部件都是製造過程的一部分。
擁有跨界的子域不一定是壞事,實際上可能是可取的:子域是團隊負責的乾淨邊界。一個擁有手錶質量和製造知識的團隊可以同時實施軟體來構建和確保質量,同時為電子商務展示提供論據和資訊。畢竟,負責建立電子商務的團隊可能不知道這些資訊,並且可能更關心可伸縮性、可用性和其他分散式系統問題。
這是一個漫長的旅程
總結
我們只是觸及了表面。DDD 是一種非常龐大的方法論,涵蓋從戰略 規劃和分解(透過會議、事件風暴和所有爵士樂)到戰術 實施(模式、層次和邊界之間的通訊)。
問題區域非常密集和模糊:如何限制邊界,子域是什麼?它們是如何相互聯絡的?誰擁有什麼?沒有靈丹妙藥,需要經驗和成熟度才能擅長。
解決方案領域也非常大:有很多模式和工具、不同的層和架構可供選擇。我們幾乎沒有提到事件驅動架構、事件溯源和 CQRS。正確確定要使用的工具需要嚴謹和仔細的思考。
但是再一次。
不要讓任何人讓您認為沒有完整的 DDD 工具集就無法進行領域驅動設計。
您可以擁有一個跨越整個專案的限界上下文:它使事情更容易推理。
您可以在任何地方使用交易指令碼,獲得很好的效果,尤其是當您正在測試您的想法並且不確定它是否適合市場時。
隨著時間的推移,總是可以向您的專案中新增越來越多的領域驅動設計方法。所有增強都是漸進的。
結束語
最近,工匠精神已經在整個開發界掀起了一場風暴。越來越多的開發者熱衷於建立可長期維護的軟體。畢竟,我們大部分時間都在維護軟體,而不是編寫新程式碼。
這個領域很寬廣,因為有很多書籍和技術可以學習,以使我們在這個領域變得更好。領域驅動設計只是一個可以研究的地方,它可能是最深入和最難走的路。請記住,DDD決不是一個普遍的真理,只是一些人決定採用的觀點。
我相信它是一套很好的原則、實踐和工具。但不幸的是,學習曲線是非常陡峭的。
請帶著一點鹽來接受這些知識,在做任何事情之前,請根據你的需要來調整它。再一次,沒有銀彈。
相關文章
- 領域驅動設計戰術模式--領域事件模式事件
- 一張圖解釋DDD領域驅動設計的戰術概念圖解
- 領域驅動設計戰術模式--領域服務模式
- 領域驅動設計--戰術模式簡介模式
- 領域驅動設計戰術模式--值物件模式物件
- 領域驅動設計的概念解釋 -DEVdev
- 什麼是DDD領域驅動設計的戰術設計?
- 領域驅動設計最佳實踐--程式碼篇
- DDD領域驅動設計:領域事件事件
- 領域驅動設計示例
- MasaFramework -- 領域驅動設計Framework
- 理解領域驅動設計
- 聊聊領域驅動設計與編碼思想
- 什麼是DDD領域驅動設計的戰略設計?
- 戲說領域驅動設計(廿五)——領域事件事件
- 實現領域驅動設計
- 領域驅動設計核心概念
- 領域驅動設計簡介
- 再談領域驅動設計
- DDD領域驅動設計pdf
- DDD領域驅動設計總結和C#程式碼示例C#
- 戲說領域驅動設計(十七)——實體實戰
- 戲說領域驅動設計(廿一)——領域服務
- 前端開發-領域驅動設計前端
- DDD-領域驅動設計示例
- 淺談DDD(領域驅動設計)
- JavaScript中的領域驅動設計JavaScript
- 淺談 DDD 領域驅動設計
- 何時使用領域驅動設計
- 微服務領域驅動設計 - semaphoreci微服務
- DDD領域驅動設計:倉儲
- 戲說領域驅動設計(五)——子域
- 問題驅動設計與領域驅動設計的區別 - abdullin
- 最常見領域驅動設計錯誤
- 領域驅動設計整合與架構架構
- 領域驅動設計(DDD)入門&概要
- 整潔的領域驅動設計 - George
- DDD-領域驅動設計簡談