如何用 DDD 給 DDD 建模,破解 DDD 的魔法?

語言: CN / TW / HK

“所有模型都不對,但總有一些是有用的。” —— George Box

DDD 全稱是 Domain-Driven Design,而不是我們所擅長的 Deadline-Driven Design。本來,對於再炒這一波冷飯,實在是沒有啥樂趣。直到,我發現它可以炒成蛋炒飯 —— 加入 Feakin 的圖形生成,適量的編譯器知識,還有半勺 WASM。所以,這就是我們所要做的事件, 為 DDD 建個模 ,基於模型生成架構圖,以展示設計模型與實現的模型的差異。

眾所周知,DDD 的問題域在於:如何將複雜問題控制到人能處理的範圍?所以,我們要做的事情就是:

  • 採用合理的方式拆解不同場景。諸如於戰略、戰術分別是不同的場景。
  • 藉助原則與模式解決人類智商不夠的問題。諸如於圖、設計模式等。
  • 採用廣義 DSL (領域特定語言)來精煉語言描述。

以上就是我們在建模時的三個基本思想。

我們的問題是什麼?

回到標題上,我們用 DDD 給 DDD 進行建模,只是我們想到的解決方案之一,而不是問題。先再回到上面的問題上, DDD 要解決什麼問題 —— 如何將複雜問題控制到人能處理的範圍?

在社區經過了幾年的實踐之後,已經有了文檔和流程之後,接下來,就是工具化了:如何將 DDD 固化到軟件設計與開發流程中?市場上已經有一系列的工具,諸如於大家經常吐槽的 COLA 做了類似的事情。

而我們想做的是:如何實現 DDD 設計與代碼實現的雙向綁定?於是乎,DSL 與雙向圖形化便是我們想到的解。所以,作為解決方案的第一步,那便是對 DDD 進行建模,以進行 DDD 的圖形生成。

統一 DDD 的統一語言

儘管,我司(Thoughtworks)會在各類的 DDD 工作坊中強調,統一語言的重要性。但是,據觀察,我們並沒有在內部達成真正的 DDD 統一語言,只達成了一定範圍內的統一語言。這大抵是形式化表示與文字化的差異,形式化會產生更強的規約,並通過它來構建一個框架。

於是乎,這裏,我們採用 DDD 社區給出了一個詳細的《 DDD 概念參考 》,作為我們構建 DDD 的統一語言的基礎。只是,從名詞的分類上,我更偏向於原始版本的 DDD 一書的分類:

  • 戰略設計(Strategic Design)。
  • 戰術設計(Tactical design)。
  • 應用模式設計。考慮到 DDD 一書本身是圍繞於模式語言構建的,因此諸多內容是關於如何應用模式來改善設計。

如此一來,可以讓不同的利益相關者,關注於自己所關注的部分。架構師和業務人員關注於戰略設計,架構師和開發人員關注於戰術設計,開發人員關注於軟件設計。

戰略設計的模型:DDD 自身的核心子域是什麼?

在有了統一語言之後,我們就可以知道子 系統-領域-子域-限界上文 的關係,毫無疑問都是一對多。唯一比較有意思的是 核心域支撐域通用域 ,如何在後續實現的時候,去設計他們呢?只是一種類型呢,還是?

那麼問題來説,在上一步裏,因為我們對於名詞進行了分類,所以我們得到了三個子域: 戰略設計戰術設計應用模式設計 。那麼,在這三個子域裏,哪個是核心子域呢?

答案是,每個都是,每個也不都不是。作為一個子域,它是不是核心域,取決於你會不會觀測它。“觀測”這種行為,會對被觀測對象造成一定影響 —— 遇事不決,量子力學。在進行 DDD 建模時,DDD 的核心域取決於 scope,也就是會出現因團隊而異的場景。

戰略設計的模型:如何表示上下文間的關係?

接着,我們就為到 DDD 最常被提到的上下文映射圖,即用於表示一個子域內多個上下文的關係,如下圖所示:

從代碼化的方式來考慮,這個圖並不複雜,採用形如 Graphviz 的模式就能表示:

ContextMap {
  ShoppingCarContext -> MallContext;
  ShoppingCarContext <-> MallContext;
}

唯一有意思的點在於,如何表示兩個限界上下文間的關係?依然存在一系列的迷惑點。而除了協作關係之外,我們還要考慮諸多問題:諸如於它們之間是如何通信的?

戰術設計的模型: 限界上下文的表示

接下來,就是表示一下限界上下文了:

一個限界上下文下,包含了多個聚合。所以,從模型的形式上,我們需要 Aggregate 這樣一個容器,用於顯式表達這個概念。一個聚合包含了一系統的實體,而實體和對象間存在着複雜的關係。於是乎,我們用右圖來進一步表示他們的關係。聚合根(Aggregate Root)是眾多實體中的一個,實體之間可能存在一定的關係。

在這時,如何用代碼來表示它們,就變得非常有意思。如下是我們當前設計的一個簡單的 DSL:

Aggregate ShoppingCart {
  Entity Product {
    constructor(name: String, price: Money)
  }
}

從現在的 DSL 設計來看,依舊還有很大的改進空間。

應用模式設計:如何表示?

最後,我們還有考慮的問題是,如何對 DDD 中採用的模式部分進行抽象?諸如於

  • 如何用代碼化的方式,表示採用 Factory、Repository、Service、Event 等開發模式進行表示?
  • 如何將 Domain 作為能力組件向外提供服務,Application、Service、Module,還是 Package ?
  • 如何使用代碼化的方式來描述分層模式?

如下圖所示:

採用何種方式來表達這些模式,變成了一種很有意思的事情。當然, 這也是我們在 Feakin 中想要繼續探索的內容。

DDD 的領域特定語言形式

既然,我們已經抽象到了基礎的模型,那麼就可以基於模型與過程,構建 DDD 的領域特定語言。

業內對於採用領域特定語言來表示 DDD 建模結果,已經相對比較成熟了,典型的方式就是:DDD DSL 與基於現有的工具擴展。

ContextMapper 與 UML

ContextMapper( https://contextmapper.org/ )便是一個不錯的 DDD DSL,雖然在語法設計上不具備概念完整性。但是,還是作為一個參考項目,還是非常不錯的。採用的是 Eclipse 家族的 Xtext 作為 DSL 開發工具,唯一坑的點在於 Intellij IDEA 的 Xtext 非常難用。示例如下:

ContextMap {
    contains CargoBookingContext
    contains VoyagePlanningContext
    contains LocationContext

    CargoBookingContext Shared-Kernel VoyagePlanningContext
    CargoBookingContext Downstream-Upstream [OHS,PL]LocationContext
    LocationContext[OHS,PL] Upstream-Downstream VoyagePlanningContext

}

ConextMapper 比較遵循原書中的定義,只是在語法設計上還有很大的改進空間。

第二類,便是如在 DDD 社區的《 DDD 建模工作坊指南 》裏採用的 UML 示例:

@startuml

namespace user-context {
   User <<Aggregate Root>>
   VerifyCode <<Aggregate Root>>
   Authorization <<Aggregate Root>>
}

namespace question-context {
  Question <<Aggregate Root>>
  Anwser <<Entity>>
  Question "1" *-- "N" Anwser
}

namespace space-context {
  Space <<Aggregate Root>>
  SpaceMember <<Entity>>
  Space "1" *-- "N" SpaceMember
  SpaceApply <<Entity>>
  Space "1" *-- "1" SpaceApply
}

@enduml

Feakin Language

於是乎,為了更好的進行 DDD 建模:圖示方式 + 代碼生成 + 與實現的雙向綁定。我們在 feakin 內部創建了一個 FKL: fkl-parser ,用於支撐軟件架構的創建。採用了 Pest.rs 作為解析器生成器,現在的語法還比較簡單:

declarations = _{ SOI ~ declaration* ~ EOI }

declaration = {
  context_map_decl
  | context_decl
  | ext_module_decl
  | aggregate_decl
  | entity_decl
}

context_map_decl = {
  "ContextMap" ~ identifier? ~ "{" ~ (context_node_decl | context_node_rel | inline_doc)* ~ "}"
}

...

形式上類似於 Antlr。基於此的 DSL 示例如下:

ContextMap {
  SalesContext <-> SalesContext;
}

Context SalesContext {
  Module Sales {
    Aggregate SalesOrder
  }
}

Entity SalesOrderLine {
  constructor(product: Product, quantity: Quantity)
}

當前只完成了基本的 DDD 戰略和戰術設計,還有應用模式設計需要考慮。如果你也有興趣的話,歡迎來加入我們。

小結

我不併擅長建模,我一直覺得模型在重構的過程中,自然而然就會浮現出來。而除了重構的這種方式,還有一種額外的方式是藉助 DSL(領域特定語言)進行抽象。所以,我嘗試以此作為一些出發點,借而來 Driven 中系統的模型。與得到一個有用的結果相比,在過程中對於 DDD 的抽象,構建 DDD 的 DDD 模型,顯得更有意思。

如果你對於使用 DSL 作為協作設計有興趣,歡迎一起來用愛發電: https://github.com/feakin/feakin