領域驅動設計示例

技術瑣話發表於2022-12-05

我過去看過很多IT專案。其中一些設計非常好,同時也有一些非常糟糕。基於這些經驗,我想寫一些示例專案,我還想展示如何使用UML建模示例專案,以及如果我們將領域驅動設計原則應用於模型會發生什麼。

在開始講述本文之前,您應該閱讀Eric Evans撰寫的“Domain-driven Design”和Vaughn Vernon的“實現領域驅動設計”。 文中例子的大部分是基於他們的工作,如果你想深入研究領域驅動的設計,他們的書是必讀的。

需求分析

一家公司提供時間出租服務。他們有一些員工,還有很多自由職業者作為分包商存在。目前,他們使用Excel工作表來管理他們的客戶,自由職業者,時間表等。Excel解決方案無法很好地進行擴充套件。它無法應對多使用者使用的場景,也不提供安全和審計日誌。因此他們決定構建一個新的基於Web的解決方案。以下是核心要求:

  • 搜尋自由職業者分類的功能

  • 用於儲存聯絡自由職業者的不同渠道的解決方案

  • 搜尋專案分類的功能

  • 搜尋客戶分類的功能

  • 維護合同中自由職業者的時間表

基於這些要求,開發團隊決定使用UML對所有內容進行建模,以全面瞭解新的解決方案。現在讓我們看看他們做了什麼。

架構總覽

以下是他們第一次設計的情況:

領域驅動設計示例

這很簡單。有客戶,自由職業者,專案和時間表。還有一種基於角色安全的使用者管理。但是等等,這裡有些不對勁。 裡頭有一些隱藏的設計缺陷。你看得到他們嗎? 缺陷如下:

  • 這是一個非常大的物件圖。如果他們在這裡不使用Hibernate / JPA延遲載入,那麼在重負載下它肯定會耗盡記憶體

  • 為什麼使用者和角色之間的關聯是雙向的?`

  • ContactType有一些布林標誌來顯示它是什麼型別,電子郵件,電話,移動

  • Freelancer類包含Projects列表。這也意味著在不修改Freelancer物件的情況下無法新增專案。這可能會導致重負載下的事務失敗,因為可能有多個使用者正在為同一客戶新增專案。

  • ContactInformation是什麼意思? 需求規定的“溝通渠道”。兩者是一個概念?

整個模型似乎更像是一種全關係圖而不是軟體模型。另外,這是商業邏輯嗎?該團隊希望圍繞模型建立一些商業服務來儲存和檢索資料,實體只是由JPA管理的POJO。

當前解決方案有很大的程式碼氣味,一個貧血的領域模型。團隊也會認識到這一點。但是什麼可以解決方案呢?好吧,一位資深團隊成員建議使用域驅動設計原則來為解決方案建模。好的,現在讓我們看看DDD如何改進設計。

DDD的方式

在我們深入研究領域驅動設計之前,我們應該先談談DDD背後的原理。

DDD背後的一個原則是透過使用相同的語言來建立相同的理解來彌合領域專家和開發人員之間的差距。另一個原則是透過應用物件導向的設計和設計模式來降低複雜性,以避免重新發明輪子。

但什麼是域?域是一個“知識領域”,例如公司運營的業務。域也稱為“問題空間”,因此我們必須設計解決方案的問題。

好的,讓我們來看看要求。我們可以認為有一個“時間租賃”領域,這是完全正確的。但是,如果我們深入瞭解Domain,我們會看到一些名為“Subdomain”的東西。以下子域名可能是包括以下內容:

  • Identity and Access Management Subdomain

  • Freelancer Management Subdomain

  • Customer Management Subdomain

  • Project Management Subdomain

我們可以把大問題分成小問題。這可以幫助我們設計出更好的解決方案。

分離的域可以很容易地視覺化。在DDD術語中,這稱為上下文對映,它是任何進一步建模的起點。

領域驅動設計示例

現在我們需要將子問題空間與我們的解決方案設計對齊,我們需要形成一個解決方案空間。DDD術語中的解決方案空間也稱為有界上下文,並且最好將一個問題空間/子域與一個解空間/有界上下文對齊。

構建模組

領域驅動設計的構建模組分為戰術和戰略模式。我寫了一篇關於DDD構建塊的文章,所以如果你想深入瞭解,請訪問這篇文章。

請注意,以下架構模式和類圖不依賴於技術。該解決方案可以使用Java SE / EE,C#甚至JavaScript實現。無關緊要,我們可以使用每種目標技術存檔相同的好處。

新的架構總覽

讓我們看下新的架構

領域驅動設計示例

好的,這裡發生了什麼?現在每個已識別的子域都有有界上下文。有界上下文是孤立的,彼此一無所知。它們僅由一組常見型別粘合在一起,如UserId,ProjectId和CustomerId。在DDD中,這組常見型別稱為“共享核心”。我們還可以看到什麼是“核心領域”的一部分,什麼不是。如果有界上下文是我們試圖解決的問題的一部分,並且不能被另一個系統替換,那麼它就是“核心域”的一部分。如果它可以被另一個系統替換,那麼它就是“通用子域”。“身份和訪問管理”上下文是“通用子域”,因為它可以由現有的IAM解決方案替換,例如Active Directory或其他。

我們將一組戰術和戰略模式應用於模型。這些模式有助於我們構建更好的模型,提高容錯能力並提高可維護性。

在每個有界上下文中都有聚合和值物件。聚合是物件層次結構,但只能從聚合外部訪問層次結構的根。聚合處理業務常量。對物件樹的每次訪問都必須透過Aggregate,而不是透過其中的一個元素。這大大增加了封裝性。

AggregatesEntites是我們模型中具有唯一ID的東西。值物件不是事物,它們是值或度量,如UserId。值物件被設計為不可變的,它們不能改變它們的狀態。每個狀態更改方法都返回值Object的新例項。這有助於我們消除不必要的副作用。

設計行為

讓我們設計一些行為,“Freelancer遷移到新位置”用例。在沒有DDD的情況下,我們可以建立一個簡單的POJO,如下所示:

領域驅動設計示例

我們可以透過呼叫例項的setter來更改Freelancer的名稱。可是等等!我們的用例在哪裡?可以從其他地方呼叫setter。實施基於角色的安全性可能會變得很麻煩。因為我們在呼叫setter時沒有呼叫上下文。此外,這個模型中還缺少一個概念,即地址。它只是透過Freelancer類的簡單屬性以非常隱式的方式建模。

透過應用域驅動設計,我們得到以下結果:

領域驅動設計示例

這要好得多。現在有一個顯式的Address類,它封裝了整個地址狀態。現在,地址更改用例顯式建模為Freelancer聚合提供的moveTo()方法。我們只能使用此方法更改Freelancer狀態。當然,這種方法可以透過某種安全模型輕鬆保護。

完整的用例和永續性

好的,我們繼續模仿“自由職業者遷移到新位置”的用例。首先,我們需要為Freelancer Aggregate提供一種儲存空間。DDD將這樣的儲存稱為儲存庫。使用儲存庫,我們可以按名稱搜尋Freelancer,透過Id載入現有的Freelancer,從儲存中刪除它或向儲存新增新的Freelancer。根據經驗,每種型別的聚合都應該有一個儲存庫。請注意,儲存庫是業務術語中描述的介面。我們將在下一章討論實現。

下圖顯示了建模的用例。你會看到一些新的工件。首先是使用者介面,我們的域模型的客戶端。客戶端可以是一切,從JSF 2.0前端到SOAP Web服務或REST資源。所以請以一般方式考慮客戶。客戶端向ApplicationService傳送命令。ApplicationService將命令轉換為域模型用例呼叫。因此,FreelancerApplicationService將從FreelancerRepository載入Freelancer Aggregate,並在Freelancer Aggregate上呼叫moveTo()操作。FreelancerApplicationService也構成了事務邊界。每次呼叫都會導致新的事務。基於角色的安全性也可以使用FreelancerApplicationService實現。將事務控制保留在域模型之外始終是一個很好的選擇。事務控制更多是技術問題,而不是業務問題,因此不應在域模型中實現。

領域驅動設計示例

應用架構

好的,現在讓我們來看看應用程式架構。對於每個有界上下文,應該有一個單獨的部署單元。這可以是Java WAR檔案或EJB JAR。這取決於具體的技術實現。我們將有界上下文設計為彼此獨立,並且此設計目標也應該反映在獨立的部署單元中。

每個部署單元包含以下部分:

  • 領域層

  • 基礎實施層

  • 應用層

域層包含我們之前在此示例中建模的基礎架構獨立域邏輯。基礎實施層提供了與技術相關的工件,例如基於Hibernate的FreelancerRepository實現。應用層充當具有整合事務控制的業務邏輯的閘道器。

領域驅動設計示例

使用這種架構,我們的業務邏輯的域層不依賴於任何東西。我們可以將Repository實現從Hibernate更改為JPA,甚至可以將NoSQL更改為Riak或MongoDB,而不會影響任何業務邏輯。

領域層

領域層包含真實的業務邏輯,但不包含任何基礎結構特定的程式碼。基礎架構特定的實現由基礎架構層提供。域模型的設計應遵循CQS(命令 - 查詢 - 分離)原則的描述。可以有查詢方法只返回資料而不影響狀態,並且有命令方法,它們影響狀態但不返回任何內容。

應用層

應用層從使用者介面層獲取命令,並將這些命令轉換為域層上的用例呼叫。應用層還為業務操作提供事務控制。應用程式層負責透過Mediator或Data Transformer模式將聚合資料轉換為客戶特定的表示模型。

基礎實施層

基礎實施層為所有其他層提供基礎架構相關部分,如Hibernate或JPA支援的實現。聚合資料可以儲存在像Oracle或MySQL這樣的RDMBS中,也可以儲存為基於鍵值或基於文件的NoSQL引擎的XML / JSON甚至Google ProtocolBuffers序列化物件。這取決於您,只要儲存提供事務控制並保證一致性。基礎設施可以最好地描述為“域模型周圍的一切”,因此,如果我們與其他系統互動,則資料庫,檔案系統資源甚至Web服務消費者。

客戶端/使用者介面層

客戶端層使用應用程式服務並在這些服務上呼叫業務邏輯。每次呼叫都是一個新事務。

客戶端層幾乎可以是任何東西,從作為檢視控制器的JSF 2.0 Backing Bean到SOAP Web服務端點或RESTful Web資源。甚至可以使用Swing,AWT或OpenDolphin / JavaFX來建立使用者介面。

請檢視UI級別的服務整合與伺服器端包含(SSI)以獲得想法。

上下文整合

現在我想寫一下Context Integration。這是怎麼回事?考慮身體租賃領域的以下要求:

  • 只有在未分配專案的情況下才能刪除客戶

  • 輸入時間表後,需要向客戶收費

同步整合

讓我們從第一個開始吧。在這種情況下,客戶管理有界上下文需要在刪除客戶之前檢查是否有為給定客戶註冊的專案。這需要一種兩種有界上下文的同步積分。

有很多機會。首先,我們希望保持上下文彼此獨立。那我們該怎麼處理呢?這是客戶有界上下文與專案管理有界上下文互動的設計:

領域驅動設計示例

有一個新術語:領域服務。什麼是領域服務?域服務實現了Entity,Aggregate或ValueObject無法實現的業務邏輯,因為它不屬於那裡。例如,如果業務邏輯呼叫包括跨多個域物件的操作,或者在這種情況下與另一個有界上下文整合。

ApplicationService呼叫CustomerService的deleteCustomerById方法。如果給定CustomerId存在專案,CustomerService將透過呼叫customerExists()來詢問ProjectManagementAdapter。僅當它返回false時,才會從CustomerRepository中刪除Customer。

ProjectManagementAdapter有兩種實現方式,一種是SOAP和一種基於REST的實現。我們可以使用SOAP來使用XML編組呼叫完整的Web服務操作並使用完整的JAX-WS堆疊,或者我們可以使用REST並呼叫http://example.com/customers/customerId/projects並獲取404(不是找到)或20x(確定)HTTP響應程式碼。這取決於您,但REST可以不那麼複雜,更容易整合,也可以更好地擴充套件。我們也可以從REST開始,如果需要,可以切換到SOAP。在不影響域層的情況下更改實現非常容易,我們只使用介面卡的另一種實現。

在Project Management Bounded Context端,有一個ApplicationWebService公開為REST資源或SOAP服務,實現了通訊的伺服器部分。此服務或資源委託給ProjectApplication Service,後者委託ProjectDomainService詢問是否為給定的CustomerId註冊了Project。

領域驅動設計示例

無論如何,我們必須處理交易邊界。Web Service或REST資源呼叫不會觸發開箱即用的事務,並且使用XA /兩階段提交會增加複雜性並降低可伸縮性。最好不要在物理上刪除客戶,而是將其標記為邏輯刪除。在事務失敗或併發問題的情況下,將客戶恢復到其原始狀態將很容易。

在這裡,您還可以看到基礎架構層位於所有其他層之上的原因。它必須能夠根據以下層中定義的介面委託給它或實現特定於技術的工件。

一個同步例子

好的,現在我們繼續一個更復雜的例子。考慮一下要求,即一旦輸入時間表,就需要向客戶收費。

這是一個非常有趣的。這很有趣,因為它不需要同步呼叫。賬單可以及時傳送,也可以在幾個小時後或月末與其他賬單一起傳送。或者可以透過客戶的大客戶經理或其他任何方式豐富賬單,Freelancer管理上下文並不關心。

我們如何使用DDD模式對此進行建模?這裡的關鍵是“一旦時間表是......”,這是我們域中的業務相關事件,這些事件可以建模為域事件!

建立域事件並將其轉發到事件儲存庫並儲存在那裡以進行進一步處理。EventStore是Bounded Context Deployment Unit的一部分,在Store中儲存Event是在ApplicationService管理的執行事務下完成的。在基礎結構方面,有一個Timer將儲存的事件轉發到最終的訊息傳遞基礎結構,例如基於JMS或AMQP,甚至可以將REST資源的呼叫視為訊息傳遞。

那麼為什麼我們需要本地EventStore呢?好吧,訊息傳遞基礎結構可能暫時不可用,但這不應該影響我們執行的Bounded Context。因此,當基礎架構再次可用時,事件將排隊並交付。如果我們將訊息傳遞基礎結構直接與Event生成器耦合,則生成器可能無法在發生基礎結構錯誤時傳送。即使我們使用訊息傳遞,如果出現問題,這可能會對整個基礎架構造成連鎖反應,這也是我們使用訊息傳遞的原因:系統解耦

以下是Freelancer Management Bounded Context的建模方式:

領域驅動設計示例

FreelancerService建立一個TimesheetEntered域事件並將其轉發到EventStore,它基本上是另一個Repository。然後,JMSMessagingAdapter從EventStore獲取掛起的事件,並嘗試將它們轉發到目標訊息傳遞基礎結構,直到傳遞成功。但是這種轉發在另一個事務中處理,並且可以由例如計時器觸發。

好的,客戶管理上下文如何處理事件?建模如下:

領域驅動設計示例

同樣,基礎架構層必須位於所有其他層,因為它必須在上下文整合呼叫應用程式服務的情況下。

以下是JMSMessageReceiver位於基礎結構層中的來源。MessageReceiver還負責重複資料刪除。這可能發生在系統故障,已經傳送事件被重新傳遞或其他錯誤的情況下。由於基礎架構層位於應用層之上,因此它可以呼叫CustomerApplicationService,CustomerApplicationService本身呼叫CustomerService,後者實現業務邏輯以傳送賬單。

在此方案中,事務邊界位於ApplicationService。我們可以爭辯說JMSMessageReceiver可以呼叫CustomerService,並圍繞JMS Transaction進行。這也是一個可行的解決方案。

棘手的部分是重複資料刪除。如果發生基礎設施故障或系統中斷,可能會發生這種情況。透過為每個事件提供唯一ID,並跟蹤已處理的ID,可以避免這種情況。

另一個棘手的部分是事件排序。這取決於訊息傳遞基礎結構。如果基礎設施支援事件排序,一切都很好。如果沒有,這必須由我們自己實施。無論如何,將事件設計為冪等操作是一種很好的做法。這意味著每個事件都可以多次處理,並且每次都具有相同的結果而沒有不必要的副作用。

查詢來自多個有界上下文或聚合的資料

有時我們需要收集分佈在多個聚合甚至是有界上下文的資料。這可能是一項艱鉅的任務。在一個有界上下文中,我們可以使用專門的資料庫檢視並使用Hibernate或JPA檢索資料,但是將資料分佈在多個有界上下文中可能會導致許多遠端方法呼叫和其他問題;此解決方案可能無法很好地擴充套件我們還要考慮使用檢視可能會破壞精心設計的Aggregate的業務不變性。這是我們真正需要照顧的問題!

現在,可能是什麼解決方案?我們可以考慮CQRS或Command-Query Responsibility Segregation!基本上我們將模型劃分為包含業務邏輯的命令模型和用於檢索資料的查詢模型。因此,對於此示例,命令模型將包含我們要查詢的所有有界上下文,以及查詢模型,該模型用於查詢聚合資料(並且被最佳化以有效地查詢資料)。使用域事件同步命令模型和查詢模型!在命令模型中觸發業務操作後,將由查詢模型發出並處理域事件,並更新資料。

使用CQRS,我們可以設計高效能資料處理系統,並且與商業智慧整合也不再是問題。想一想:查詢模型基本上可以是資料倉儲。

結束語

我非常喜歡Domain-driven Design背後的想法。使用這種技術,即使非常複雜的域邏輯也可以輕鬆地進行提取和建模。這可以帶來更好的系統,改善的使用者體驗以及更可靠和可維護的解決方案。感謝Eric Evans和Vaughn Vernon!DDD /域驅動設計是物件導向的程式設計。

原文地址:https://www.mirkosertic.de/blog/2013/04/domain-driven-design-example/

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562044/viewspace-2656295/,如需轉載,請註明出處,否則將追究法律責任。

相關文章