聚合分組法和它的問題

在事件風暴工作坊中,常用的劃分限界上下文的方法是:

對前一步(事件風暴)產生的聚合進行分組,通過業務的內聚性和關聯度劃分邊界,結合限界上下文的定義進行判斷,並給出上下文名稱。

[服務化設計階段路徑方案]

我將其稱之爲“聚合分組法”。然而面對一堆聚合,要得出一套合理的分組是非常困難的:

  1. “相關性”全憑經驗

    相關性是一個過於抽象的規則,非常依賴經驗。

    舉個例子。在一個活動運營系統中,有“註冊獎勵活動”、“註冊獎勵規則”、“任務獎勵活動”、“任務獎勵規則”等概念。是把所有的“活動”分爲一組,所有“規則”分爲一組,還是把“註冊”相關的分爲一組,把“任務”相關的分爲一組?這是個讓人頭疼的問題。也許你會說需要業務人員的輸入,但是業務人員很可能只會告訴你這些概念之間都有關係。

  2. 不健康的聚合上下文

    聚合分組法很容易導向一種按照聚合劃分的架構。服務圍繞聚合建設,而非針對某個業務價值,也就無法提供正確的業務價值。圍繞聚合建設的服務,看上去可以複用,但是會造成服務間的緊耦合,容易成爲最糟糕的分佈式單體架構:

    當架構是分佈式單體時,往往需要同時修改多個服務,同時部署多個服務、服務之間調用非常頻繁。

    [You’re Not Actually Building Microservices]

    聚合分組法也無法很好的識別“重複的概念”問題([領域驅動設計]14.1,指某一個概念,應該被設計成多個模型,因爲它們有不同的規則,甚至有不同的數據)。使用聚合分組法往往導致把帶着這樣的聚合簡單的放到某個限界上下文中。

  3. 隱藏的劃分方案

    還很可能是這種情況:在使用聚合分組法時,架構師已經有一個隱藏在心裏的模糊的劃分方案,在劃分限界上下文時都是往該方案上靠。但是由於這個劃分方案只是模糊存在於架構師的腦中,並沒有拿出來討論,很可能經不起推敲,最終無法言說,淪爲“by experience”。

如何劃分限界上下文

如何劃分限界上下文?在回答這個問題前,讓我們先看看限界上下文到底是什麼。

在[領域驅動設計]第14章提出了著名的限界上下文。限界上下文是爲了分解大型模型:

然而在幾乎所有這種規模的組織中,整個業務模型太大且過於複雜以至於難以管理,甚至很難把它作爲一個整體來理解。我們必須把系統分解爲較小的組成部分,無論在概念還是在實現上。

有時,企業系統會集成各種不同來源的子系統,或者包含諸多屬於完全不同領域的應用程序。要把這些不同部分中隱含的模型統一起來是不可能的。通過爲每個模型顯式地定義一個限界上下文,然後在必要的情況下定義它與其他上下文的關係,建模人員就可以避免模型變得混亂。

[領域驅動設計]第四部分

bounded-context

限界上下文告訴我們,同一個概念,不必總是對應於一個單一模型,也可以對應於多個模型。用限界上下文明確模型要解決的問題,可以保持每個模型的清晰。限界上下文領域模型的邊界,也就是領域知識的邊界。和上下文主題緊密相關的模型內聚在上下文內,而其他模型被會分到其他限界上下文中。限界上下文內的領域知識是高內聚低耦合的。

bounded-context-2

限界上下文的主題是什麼呢?我認爲是子域。每個限界上下文專注於解決某個特定的子域的問題。每個子域都對應一個明確的問題,提供獨立的價值,所以每個子域都相對獨立。子域及其對應的限界上下文中的模型會因爲其要解決的問題變化而變化,不會因爲其他子域的變化而變化,即低耦合;當一個子域發生變化時,只需要修改其對應限界上下文中的模型,不需要變動其他子域的模型,即高內聚。

Evans也談論了限界上下文和子域的關係:

One confusion that Evans sometimes notices in teams is differentiating between bounded contexts and subdomains. In an ideal world they coincide, but in reality they are often misaligned.
Evans有時會在團隊中發現的一個困惑,就是如何區分限界上下文和子域。在理想的世界中它們是重合的,但在現實世界中它們常常是錯位的。
[Defining Bounded Contexts — Eric Evans at DDD Europe]

當我們設計一個新系統或者設計遺留系統的目標架構時,我們往往會按照理想的方式進行設計。而在理想情況下,子域和限界上下文是重合的。

[領域驅動設計精粹]中也講述了一個 通過尋找核心域相關的概念來識別限界上下文 的方法。

如何分解子域

根據子域來識別限界上下文,那麼子域如何得到呢?我們通過分解問題域的方式,將整個問題域分解成若干個更小、更簡單、更容易解決的問題子域。

我們需要某種方法,將領域分解成邏輯上相互獨立且沒有交叉的子域。在這裏的方法是 通過產品願景,識別核心域,進而識別核心域周邊的子域。

識別核心域

由於核心域是最明顯、最容易識別出來的子域,所以我們先從核心域開始。

每一個子域甚至每一個領域模型都是爲了產品願景而存在的。我們分解子域的第一步,就是從產品願景中獲取核心域。 產品願景包含“相對抽象的產品價值”,以及“實現該價值的主要功能”。 其中,主要功能就是我們尋找核心域的依據。想象一下,如果要做MVP的話,我們會挑選最能夠提供其核心價值的功能來開發,以驗證產品價值。MVP往往就是核心域。

以上述活動運營系統爲例,其產品願景是通過各種吸引用戶的優惠活動,以幫助客戶通過活動提升用戶量和知名度。其核心域是給客戶提供吸引用戶的多樣的靈活的活動,包括活動形式、活動規則和多種獎勵。

識別核心域周邊的子域

核心域識別出來了,接下來就是識別核心域周邊的子域。核心域往往不會獨立存在,會有其他子域同核心域一起才能達成業務目標。這裏需要回答的問題是:

  • 有哪些子域是用來支撐核心域的?

    這些子域是幫助核心域更好的工作。例如提供審批流程以配置核心域,提供各種輔助功能更好的爲核心域提供內容。

  • 有哪些子域是核心域衍生出來的?

    核心域經常會產生一些數據,這些數據也有其價值。比如產生各種報表,活動獎勵的發放記錄。

  • 有哪些子域是用來支撐或衍生自這些新識別出的子域的?

    用來支撐核心域的子域、以及核心域衍生的子域,也有各自的支撐子域和衍生子域。

活動運營系統

識別出來的每個子域只對應一個問題,子域之間是相互獨立的,沒有交叉,不是包含關係。所以子域加起來就是整個領域。

也可以通過角色、時間等因素分解子域。解決不同角色的問題可能分屬不同子域,比如用戶參與活動、運營人員配置活動分屬不同子域,兩個子域的變化原因不同;不同時間使用的功能可能屬於不到子域,比如先有運營人員配置活動,再有用戶參與活動,配置活動和參與活動分屬不同子域。

如果按照聚合分組劃分限界上下文,很可能出現“活動上下文”,同時活動模型,即承擔運營人員配置的職責,又承擔用戶參與規則校驗的職責,這會導致職責過多,違背了單一職責。另外活動規則校驗的模塊需要支持高併發,需要使用和配置模塊不同的技術架構。如果這些相似的概念和不同的技術實現屬於不同的上下文,就可以保持各自模型的完整,技術上也可以做到獨立演進。

子域的粒度

理論上子域仍然可以被分解。例如活動子域可以分解爲活動參與規則子域、獎勵子域等。那麼子域粒度多大是合適的呢?

我們希望每個子域可以解決某個特定的問題,讓這個問題的解決方案都內聚在子域對應的限界上下文內,所以如果問題的再分解沒有的邊界並不清晰,建議先不分解。隨意的拆分會導致成爲“分佈式單體”。

識別限界上下文

一個限界上下文封裝了一個相對獨立子領域的領域模型和服務。

子域subdomain和限界上下文某種意義上是互相印證的

[DDD戰術篇:領域模型的應用]

這個時候我們通過事件風暴得到的領域模型就可以出場了。領域模型和子域都是從業務知識裏分析得到的,將兩者匹配起來可以再次驗證我們對於業務的理解、子域的分解和領域模型是否合理。

爲每個子域創建一個解決其問題的限界上下文,然後 爲每個領域模型找到其歸屬的限界上下文。 每個領域事件都是爲了解決某個問題,它和它相關的領域模型就應該放在這個問題子域對應的限界上下文裏。

比如“活動已上線“這個事件,由運營人員在配置時觸發,會導致用戶可以開始參與活動。那麼這個事件及其對應的“活動”概念應該被分爲兩個模型,分別歸屬於活動配置子域對應的“活動配置上下文”和活動子域對應的“活動上下文”。

爲領域模型尋找歸屬完成後,我們會發現這麼幾個情況。

  • 同一個概念可能會出現在多個限界上下文中。發生這種情況很正常,說明這多個子域都需要這個概念,而且很可能不同子域的領域模型不完全相同。

    比如剛纔說到“活動”既存在於“活動上下文”中,又在“活動配置上下文”中。這裏我們就很好的識別出了“重複的概念”問題。

  • 也有一些概念重複在多個限界上下文中,這些概念和該上下文的主題並沒有緊密的關係。這些模型可以單獨出一個限界上下文,用以同時支撐多個限界上下文,以減輕限界上下文的負擔。

  • 有時候某個模型找不到合適的限界上下文,說明很可能是遺漏了一個子域,那就需要回到“分解子域”步驟,重新審視產品願景。

聚合分組法採用“相關性”來劃分限界上下文,其問題在於缺少一個主題,而子域恰好可以用來提供這個主題。本文的“願景”-“核心域”-“周邊子域”方法,不是唯一分解問題域的方法,任何可以將領域分解成高內聚低耦合的子域的方法都是可行的方法。

更多精彩洞見,請關注微信公衆號:ThoughtWorks洞見

相關文章