在我們對問題有共識之前談解決方案是沒有意義的,在我們對解決方案有共識之前談實行步驟也是沒有意義的。 p. 1
核心子領域是公司與競爭對手的不同之處,這可能涉及發明新產品或服務,或透過最佳化現有流程來降低成本。 p. 4
易於實行的核心子領域只能提供短暫的競爭優勢。 p. 5
重要的是要注意,核心子領域不一定是技術性的,並非所有業務問題都透過演算法或其他技術方案解決,一家公司的競爭優勢可以來自各種來源。 p. 5
通用子領域 (Generic subdomains) 是所有公司都以相同方式執行的業務活動。就像核心子領域一樣,通用子領域通常很複雜而且難以實行,但通用子領域卻不會為公司提供任何競爭優勢。這裡不需要創新獲最佳化:經過實戰考驗的實踐已經廣為可用,所有公司都在使用它們。 p. 6
相信我,任何能從外部找到的通用 solution,一定有不合腳要修改的部分。
支援子領域 (supporting subdomains) 支持公司的業務。然而,與核心子領域相反,支持子領域並沒有提供任何的競爭優勢。 p. 6
區分核心子領域和支持子領域有時候可能具有挑戰性。複雜度是一個有用的指導原則,詢問正在討論中的子領域是否可以變成副業,有人會自己付錢嗎?如果會,這就是一個核心子領域。類似的推理也是用於區分支持子領域和通用子領域:用你自己的實踐會比整合外部的實踐更簡單、更便宜嗎?如果是,這就是一個支持子領域。 p. 8
核心子領域的工作永遠不會完成,公司要不斷創新和發展核心子領域。 p. 9
外包核心子領域的實行也是不明智的。這是一項戰略性投資,在核心子領域上走捷徑不僅在短期內是有風險的,而且長遠來看可能會產生致命的後果。 p. 9
避免在內部實行支持子領域是合理的,因為它缺乏競爭優勢,但和通用子領域不同的是,它沒有現成的解決方案可以使用。所以公司別無選擇,只能自己實行支持子領域。 p. 10
在生產環境中發布出去的不是領域專家的知識,而是開發人員的 (錯誤) 理解。 p. 19
正如 Alberto Brandolini 所言,軟體開發是一個學習的過程;運作的程式碼是一個附帶結果。 p. 20
統一語言要求每個術語都要是單一的含意。 p. 24
抽象化的目的不是模糊不清,而是創造一個可以絕對精確 (absolutely precise) 的全新語義 (semantic) 層次。 p. 25
在發展統一語言時,我們正在有效地建立業務領域的模型。該模型應該捕捉領域專家的心智模型 — 他們的思維過程,有關業務要如何運作以實踐其功能。該模型必須反映所涉及的業務實體及行為、因果關係、固定規則 (invariants)。 p. 26
若有領域專家會較容易,但個人不認為每個領域都有領域專家,全新的領域是靠摸索出來的,當然最後仍然會形成一個心智模型。
事實上,統一語言只有在其限界上下文的邊界內統一,該語言僅專注於描述限界上下文包含的模型。 p. 35
因此,保持你的模型有用,並使限界上下文的大小和你的業務需求、組織限制保持一致,有件事需要注意,要是把連貫的功能分成多個限界上下文,這種區分將會阻礙獨立發展每個上下文的能力。 p. 36
在一些情境下,限界上下文和子領域的一對一對應關係可以完全是合理的。然而在其它的情境下,不同的分解戰略可能更為合適。 p. 38
關鍵的是要記住,子領域是被發現的,而限界上下文是被設計的,子領域由業務戰略所定義,但是我們可以設計軟體解決方案和它的界限上下文,以解決特定專案的上下文和限制。 p. 39
架構設計是系統設計,系統設計是上下文設計 — 它本質上是和邊界 (什麼在內、什麼在外、什麼橫跨,什麼在之間移動) 及權衡 (trade-offs) 有關,它重塑了什麼是外部,如同它形塑了什麼是內部一樣。 p. 39
共享核心模式的首要適用標準是複製的成本對比協調的成本。由於該模式在參與的限界上下文之間引入了高度依賴關係,因此只有在複製的成本高於協調的成本時才應該應用它。 p. 50
解耦限界上下文的實作和整合模型,史上由的限界上下文可以自由地發展其實作,而不會影響到下游的上下文。當然,只有在修改後的實作模型可以轉譯成客戶已使用的釋出語言才有可能。 p. 53
程序必須履行的唯一要求是事務行為,每個腳本都應該不是成功或是失敗,而絕不會導致無效狀態 (invalid state)。即使事務腳本的執行在最不方便的時刻失敗了,系統也應該保持一致 —— 透過轉返 (rolling back) 它做的任何更改直到失敗時,或是執行補償動作。 p. 62
確保事務行為的一種方法是使操作冪等 (idempotent):即使重複操作多次也會產生相同的結果。 p. 66
事務腳本模式的主要優點是簡單性,它引入最少的抽象化,並最小化運行時效能和理解業務邏輯的負擔。儘管如此,這種簡單性也是該模式的缺點,業務邏輯越複雜,事務間的業務邏輯就越容易重複,進而導致不一致的行為 — 當重複的程式碼不同步時。因此,事務腳本永遠不該用於核心子領域 (core subdomains),因為這種模式無法應對核心子領域業務邏輯的高複雜度。 p. 67
和事務腳本模式的情況一樣,主動紀錄模式有助於支持子領域、用於通用子領域外部解決方案的整合,或是模型轉換的任務。模型之間的差異在於主動紀錄解決了將複雜資料結構映射到資料庫綱要的複雜度。 p. 69
主動記錄模式也被稱為貧血領域模型 (anemic domain model) 反模式;換句話說,是一個設計不當的領域模型。我傾向於避免貧血和反模式這兩個詞的負面含意,這個模式是一個工具,就像任何工具,它可以解決問題,但如應用到錯誤的上下文 (context) 中,它帶來的弊可能會大於利。 p. 69
領域模型模式目的是處理複雜業務邏輯的情況。在這裡,我們處理的並非 CRUD 介面,而是複雜的狀態轉換、業務規則和固定規則 (invariants):這些規則必須永遠受到保護。 p. 73
領域模型是包含行為和資料的領域物件模型 (object model)。DDD 的戰術模式 — 聚合、植物件、領域事件 (domain events)、領域服務 (domain services) — 是這種物件模型的建築區塊。所有這些模型都有一個共通的主題:它們把業務邏輯放在首位。 p. 74
領域的業務邏輯本身就很複雜,所以用來對它建模的物件不該引入任何額外的意外複雜度。該模型應該沒有任何基礎設施或技術問題,例如實作對資料庫或系統其它外部元件的呼叫 (calls)。這個限制要求模型的物件是簡單物件 (plain old objects),實作業務邏輯的物件不依賴或是直接整合任何基礎設施的元件或框架。 p. 75
不知從何時開始,自己一直都是如此,不喜歡過度依賴框架。
指定值之前不需要驗證值,因為驗證邏輯留在值物件本身當中。但是,值物件的行為不僅限於驗證,當值物件集中處理值的業務邏輯時,它的光芒最為耀眼,內聚 (cohesive) 邏輯被時坐在一個地方而且易於測試。最重要的是,值物件表達了業務領域的概念:它們使程式碼使用統一語言。 p. 78
這讓我想起研究所時,DVB-H 的國科會專案中,MPEG-TS 的 Packet
物件,滿滿的業務邏輯,不是一個 byte array。
何時使用值物件。簡單的答案是,只要你能,隨時都可以。值物件不僅使程式碼更具備能力,並封裝了趨於分散的業務邏輯,而且該模式使程式碼更安全。因為值物件是不可變的,所以值物件的行為沒有副作用,而且是執行緒安全 (thread safe)。 p. 80
聚合是一個實體:它需要一個明確的識別欄位。而且它的狀態預期會在實力的生命週期內改變,但它不僅僅是一個實體,該模式的目標是保護資料的一致性。 p. 82
聚合是一致性的強制邊界,聚合的邏輯必須驗證所有傳入的更改,並確保更改不會牴觸業務規則。 p. 82
一致性的強制是透過只讓聚合的業務邏輯更改其狀態。聚合外部的所有程序或物件只允許讀取聚合的狀態,聚合的狀態只能夠過執行聚合公開介面 (public interface) 的相對應方法來更改。 p. 82
千萬拜託,這裡的公開介面不是 setter 啊!
聚合的公開介面負責驗證輸入並執行所有相關的業務規則和固定規則。這種嚴格的邊界還確保與聚合相關的業務都被時做於一個地方:聚合本身。這使得在聚合上編排操作的應用層 (application layer) 相當簡單:它所要做的就是載入聚合的當前狀態、執行所需的操作、保持修改後的狀態,並將操作的結果回傳給呼叫者。 p. 83
任何系統操作都不能假設是一個多聚合 (multi-aggregate) 事務,只能單獨提交對聚合狀態的更改,一個資料庫事務一個聚合。 p. 85
只有當聚合的業務邏輯要高度一致性的資訊時才應該是聚合的一部分,所有在最終可以一致的資訊都應該位於聚合的邊界之外。 p. 87
由於領域事件描述的是已經發生的事情,所以它們的名稱應該用過去式來表述。 p. 89
遲早,你可能會遇到不屬於任何聚合或值物件的業務邏輯,或者似乎和多個聚合相關的業務邏輯。在這種情況下,領域驅動設計建議將邏輯時作為領域服務 (domain services)。 p. 90
聚合模式在一個資料庫事務中只能修改一個聚合實例。領域服務不是圍繞此限制的漏洞,一個事務、一個實例的規則仍然適用;相反的,領域服務有助於實作需要讀取多個聚合資料的計算邏輯。 p. 91
儲存系統事件的資料庫是唯一高度一致的存放區:系統的適時來源。用於持久化事件的資料庫,它公認的名稱是事件存放區 (event store)。 p. 105
事件存放區不該允許更改或是刪除事件,因為它是唯附加 (append-only) 的存放區。 p. 106
當你附加新事件時,你還指定了你決策基於的實體版本。如果它是過時的,就在預期的版本之後增加新事件,事件存盎區應該引發並行例外 (exception)。 p. 106
業務邏輯不依賴於任何下面的層,這是時做領域模型和事件源領域模型 (event-sourced domain model) 模式所需要的。 p. 127
我倒覺得這是好的設計本來就是這樣,跟是不是 DDD 無關。
使用多個模型的另一個原因可能和混和持久化 (polyglot persistence) 的概念有關。沒有完美的資料庫,或正如 Greg Young 所言,所有資料庫都有其各自不同方面的缺陷:我們必須經常平衡對規模、一致性,或所支援查詢模型 (querying models) 的需求。尋找完美資料庫的替代方案是混和持久化模型:使用多個資料庫來時做不同的資料相關需求。 p. 129
CQRS 使用單一模型來執行修改系統狀態的操作 (系統命令),該模型被用來實作業務邏輯、驗證規則,以及強制執行固定規則 (invariants)。 p. 130
最後,讀取模型是唯讀 (read-only) 的,系統的任何操作都不能直接修改讀取模型的資料。 p. 130
一個關於基於 CQRS 系統的常見誤解是:命令只能修改資料,而且只能透過讀取模型獲取資料來顯示。換句話說,執行方法的命令永遠不該回傳任何資料。這是錯誤的,這種方法會產生意外的複雜度,並導致糟糕的使用者體驗。 p. 133
我還真的被別人問過這問題,於是跟對方解釋說你誤解 CQRS 了。
這裡唯一的限制是回傳的資料應該來自高度一致的模型 — 命令執行模型 — 因為我們不能期望最終一致的投影會被立即更新。 p. 134
要轉譯非同步溝通中使用的模型,你可以實作訊息代理 (message proxy):訂閱來自來源限界上下文訊息的中間元件。代理將應用所需要的模型轉換,並將結果訊息發給目標訂閱者。 p. 142
在某些使用案例中,你可以透過使用現成的產品來避免為有狀態轉譯實作自定義的解決方案;例如,串流 (stream) 處理平台 (Kafka、AWS Kinesis 等) 或批次處理解決方案 (Apache NiFi、AWS Glue、Spark 等)。 p. 144
寄件匣模式保證訊息至少傳遞一次:如果中繼在發布訊息後,但在將其標記為已在資料庫中發布之前失敗,則相同的訊息將在下一次迭代中再次發布。 p. 149
saga 監聽相關元件發出的事件,並向其它元件發出後續命令。如果其中一個執行步驟失敗,saga 會負責發出相關的補償動作,以確保系統狀態保持一致。 p. 149
寬廣的限界上下文邊界或包含多個子領域的邊界,使其所含子領域的邊界或模型能更安全地發生錯誤。重構邏輯邊界比重構邏輯 (physical) 邊界的成本低得多,所以在設計縣界上下文時,從較寬的邊界開始。當你獲得領域知識時,如果有需要,把寬的邊界分解為較小的邊界。 p. 163
事件源領域模型需要 CQRS。否則,系統在其資料查詢選項將受到極大的限制。 p. 165
主動記錄模式最好與有額外應用 (服務) 層 (application (service) layer) 的分層架構相搭配,這是用於控制主動紀錄的邏輯。 p. 165
當使用主動繼錄模式時,根據定義,系統的業務邏輯分布在服務層和業務邏輯層。因此,要專注於整合這兩層。 p. 167
在設計系統時,我們不能忽略這個事實,特別是如果我們打算設計能夠很好地適應業務領需求的軟體。如果變化沒有被視當地管理,即使是最複雜、最周密的設計最終也將成為無會和發展的夢魘。 p. 171
警覺到子領域的演化同樣重要。隨著組織的成長和演化,它的一些子領域從一個類型轉變為另一個類型並不罕見。 p. 172
支持子領域可以外包或當作新員工的「輔助輪」。核心子領域必須在內部實行,盡可能地接近領域知識的來源。 p. 175
當在事務腳本中處理資料變得具有挑戰性時,重構它成為主動記錄模式,尋找複雜的資料結構並把它們封裝在主動記錄的物件 (objects) 中。與其直接存取資料庫 (database) ,不如使用主動記錄來抽象化其模型和結構。 p. 175
對於每個聚合,確定其根 (root) 或公開介面 (public interface) 的進入點。將聚合中所有其它內部物件的方法 (methods) 設為私有,並且只能在聚合中呼叫。 p. 177
領域驅動設計的核心原則是:對於設計成功的軟體系統,領域知識是必要的。獲取領域知識是軟體工程中最具挑戰性的面向之一,尤其是核心子領域。核心子領域的邏輯不緊複雜,而且預期會經常改變。 p. 181
子領域的邊界很難識別,所以我們必須努力尋找有用的邊界,而不是追求完美的邊界。也就是說,子領域應該讓我們識別具有不同業務價值的元件,並用適當的工具來設計並實行解決方案。 p. 182
我們可以建立多個模型,每個模型都專注於解決特定的問題,而不是建立一個「萬事通」的模型。 p. 183
事件風暴是一項低技術的活動,供一群人集思廣益並快速為業務流程建模。就某種意義上來說,事件風暴是一個共享業務領域知識的戰術工具。 p. 187
請記住,工作坊的目的是盡可能在最短的時間內盡量地學習。我們邀請關鍵人物來到工作坊,我們不想浪費它們寶貴的時間。 p. 187
事件應該從「快樂路徑 (happy path) 的情境」開始:描述成功業務情境的流程。一旦完成「快樂路徑」,就可以增加另一種情境—例如,遇到錯誤或做出不同業務決策的路徑。 p. 190
領域事件描述了已經發生的事情,而命令描述了適什麼觸發了事件或事件流。命令描述系統的操作,而且和領域事件相反,命令是強行制定出來的。 p. 192
一旦所有的事件和命令都被表示出來,參與者就可以開始考慮將相關的概念組織起來,聚合 (aggregate) 會接受命令並產生事件。 p. 195
事件風暴會議的真正價值在於過程本身—不同利害關係人之間的知識共享、它們業務心智模型 (mental models) 的一致性、分席模型的發現,以及最後但同樣重要的,統一語言的制定。 p. 197
諷刺的是,最能從 DDD 中受益的傳是棕地 (brown-field) 專案:那些已經證明其業務可行性,而且需要重組以對抗累積的技術債和設計熵 (design entropy) 的專案。巧合的是,我們在軟體工程職業生涯中大部分的時間都是在處理這些棕地、舊有 (legacy)、大泥球 (big-balls-of-mud) 的程式碼庫。 p. 201
最主要的原因是這樣才有領域專家,很多新創在設計產品時根本找不到領域專家,但即便是一團泥球,公司裡已經有從泥巴戰中生存下來的領域專家了。
你必須制定的下一個決策涉及現代化策略:逐步替換系統的全部元件 (絞殺者模式 (strangler pattern)),或逐步重構既有的解決方案。 p. 207
在實作絞殺者模式時可以放寬這個限制。為了避免上下文之間的複雜整合,現代化和舊友上下文都可以使用相同的資料庫。 p. 209
當您重構就有系統時,必要時使用防腐層來保護新程式碼庫免受舊模型的影響,並藉由實作開放主機服務和揭露釋出語言 (published language) 來保護客戶免受舊有程式碼庫更改的影響。 p. 210
許多這樣的努力都沒有好結果。這些公司最終得到的不是談性的架構,而是分散式的大泥球 — 這些設計比公司想要分解的單體 (monoliths) 更脆弱、更笨重、更昂貴。 p. 217
遵循讓每個服務只接露一個方法,這個簡單的分解啟發方法被證明是不理想的,原因有很多。第一,這根本不可能,由於服務必須共同運作,我們被迫使用與聚合相關的公開方法來擴展它們的公開介面。第二,我們贏得了戰鬥卻輸掉戰爭。每個服務最終都比原始設計簡單得多,但最終的系統卻變得複雜了幾個數量級。 p. 221
局部複雜度是每個個別微服務的複雜度,而全域複雜度是整個系統的複雜度。局部複雜度取決於服務的實作;全域複雜度由服務之間的互動和依賴關係來定義。 p. 221
將微服務錯誤地定義為具有不超過 x 行程式碼的服務,或者是作為應該更容易重寫而非修改的服務,專注於單一服務,而忽略了架構中最重要的面向:系統。 p. 224
微服務和限界上下文之間的關係是不對稱的。儘管為服務是限界上下文,但並非每個限界上下文都是微服務。 p. 226
子領域中包含使用案例的連貫性本質也確保了產生模組的深度。在許多情況下切分它們會導致較複雜的公開介面和較淺的模組,所有這些都使子領域成為設計為服務的安全邊界。 p. 229
事件不是一種秘方,讓您能把他倒在舊有系統上並把它轉換為鬆散耦合的分散式系統。恰好相反:馬虎的 EDA 應用會把一個模組化的單體變成一個分散式的大泥球。 p. 233
事件和命令都可以作為訊息非同步地溝通。但是,可以拒絕命令:命令的對象可以拒絕執行命令 ... (中略) ... 事件的接收者不能取消該事件。事件描述了已經發生的事情。要推翻一個事件唯一能做的就是發出一個補償動作。 p. 235
事件可以分為以下三種類型之一:事件通知 (event notification)、事件攜帶狀態轉移 (event-carried state transfer),或是領域事件。 p. 235
事件通知不該是冗長的:目的是通知該事件給感興趣的各方,但通知不該包含訂閱者對事件做出反應所需的所有資訊。 p. 236
就概念上說,使用事件攜帶狀態轉移的訊息是一種非同步資料複製的機制,這種方法使系統更具容錯性,這表示即使生產者無法取用時,消費者也可以繼續運作。這也是一種提高必須處理來自多個來源資料的元件之效能的方法,不用每次需要資料時都查詢資料來源,所有資料都可以在本地快取。 p. 238
在設計事件驅動系統時將此作為指導原則:
在編排需要發布補償動作的跨限界上下文程序時,利用 saga 和流程管理器 (process manager) 模式。 p. 245
分析模型 (OLAP) 和營運模型 (OLTP) 服務於不同類型的客戶,能夠實現不同類型的使用案例,因此被設計來遵循其它的設計原則。 p. 249
分析模型目的在為營運系統提供不同的洞察,分析模型不是實作即時的事務,而是旨在提供對業務活動績效的洞察,更重要的是,企業如何最佳化其營運已達成更大的價值。 p. 250
事實記錄永遠不會被刪除或修改:分析資料是唯附加 (append-only) 資料:表示當前資料已過時的唯一方法是附加上具有目前狀態的新紀錄。 p. 251
維度高度標準化 (normalization) 的原因是分析系統需要支援彈性的查詢。這是營運模型和分析模型之間的另一個差異。可以預測營運模型如何被查詢以支援業務需求,而分析模型的查詢模式是不可預測的。 p. 252
資料倉儲 (DWH) 的架構相對簡單。從企業的所有營運系統中提取資料,將來源資料轉換為分析模型,並將產生的資料載入到資料分析導向的資料庫中,這個資料庫 (database)就是資料倉儲。此資料管理架構主要基於提取 — 轉換 — 載入 (extract-transform-load,ETL) 腳本。 p. 255
資料湖架構基於相同的概念,即接收營運系統的資料並把它轉換為分析模型。然而,這兩種方法之間存在概念上的差異。基於資料胡的系統會接收營運系統的資料,但是資料並沒有裡即轉換為分析模型,而是以其原始形式保存,即在原始的營運模型中。 p. 257
由於營運系統的資料以其起初、原始的形式保留,並且僅在之後進行轉換,因此資料湖允許使用多個任務導向的分析模型。 p. 258
由於資料湖是無綱要 (schema-less) 的 — 沒有對傳入資料施加模式 — 並且無法控制傳入資料的品質,因此資料湖的資料在一定規模上會變的混亂。 p. 258
根據系統的限界上下文對分析模型進行分解時,分析資料的產生就成為相對應產品團度的職責。 p. 259
要實施資料即產品的原則,產品團隊需要加上資料導向的專家。 p. 261
《Domain-Driven Design: Tackling Complexity in the Heart of Software》這本厚重的原文書還躺在我的書架上,大概只翻了三分之一,相較《Clean Architecture》的易讀,我對 Evans 的文筆有點感冒,所以閱讀上一直沒進入狀況。但實在不想在有原文書的情況下再買本翻譯書,恰巧看到 O'Reilly 推出關於 DDD 的新書就買回來了。
第一次真正接觸 DDD,是參加社群的事件風暴工作坊,但心中一直有疑問,雖然 Evans 的書我沒看完,但我怎麼沒印象那本書有提到事件風暴,回去後再次快速翻了一下,還是沒看到,於是就很納悶到底是在提到的,也許要找《Implementing Domain-Driven Desing》來看看了。
這本書確實比較像是學習手冊,或者說像是補習班的講義,書中也確實提到事件風暴該如何進行,但看完後,我對於 DDD 的感想,其實沒有特別的強烈。看 DDD 之前,在進行系統架構時思考子系統或是模組的邊界其實就很接近子領域的概念,只是不會特定去思考子領域是核心、通用或是支援,DDD 裡提到的戰術模式也並非只用在 DDD,很多都是常見的模式。另外,像是領域與微服務的關係,和我自己原本的心得就很接近。
若真要說獲得最大的感想:(1) 統一語言僅適用於一個 bounded context (翻成限界上下文其實有點難懂,還不如不要翻譯);(2) 只有當聚合的業務邏輯要高度一致性的資訊時才應該是聚合的一部分,所有在最終可以一致的資訊都應該位於聚合的邊界之外。前者解決很多時候一個 term 是可以有多個解讀的,後者則是讓我在設計上有更好的思維,即便我不是使用 DDD 在設計系統。
但對於怎麼使用 DDD,這本倒是可以參考看看,算是實用的好書。