讀書筆記 - 實現領域驅動設計

書中其實也在權衡技術細節與 DDD 的實現,有非常多的取捨的地方,所以完美的 DDD 實現在現有技術組件下幾乎是不存在的。這反而有種爲了實現 DDD 而實現 DDD 的感覺,而關於 DDD 到底能帶給我們什麼,由於我沒有實際 DDD 的經驗,所以我並不能很真實的感覺到。

第 1 章:DDD 入門

1、將領域專家引入到團隊

領域專家並不是一個職位,他可以是精通業務的任何人。他們可能瞭解更多的關於業務領域的背景知識,他們可能是軟件產品的設計者,甚至有可能是銷售員。

在實施 DDD 的過程中,最好將那些不怎麼使用技術語言的人加進自己的團隊。就像你會向他們學習一樣,他們也會向你學習。

2、什麼是領域模型

領域模型是關於某個特定業務領域的軟件模型。通常,領域模型通過對象模型來實現,這些對象同時包含了數據和行爲,並且表達了準確的業務含義。

3、爲什麼我們需要 DDD

使領域專家和開發者在一起工作,這樣開發出來的軟件能夠準確的傳達業務規則。當然,對於領域專家和開發者來說,這並不表示單單的包容對方,而是將他們組成一個密切協作的團隊。

“準確傳達業務規則” 的意思是說,此時的軟件就像如果領域專家是編碼人員時所開發出來的一樣。

可以幫助業務人員自我提高。沒有任何一個領域專家或者管理者敢說他對業務已經瞭如指掌,業務知識也需要一個長期的學習過程。在 DDD 中,每個人都在學習,同時每個人又是知識的貢獻者。

關鍵在於對知識的集中,因爲這樣可以確保軟件知識並不只是掌握在少數人手中。

在領域專家、開發者和軟件本身之間不存在 “翻譯”,意思是當大家都使用相同的語言進行交流時,每人都能聽懂他人所說。

設計就是代碼,代碼就是設計。設計是關於軟件如何工作的,最好的編碼設計來自於多次試驗,這得益於敏捷的發現過程。

DDD 同時提供了戰略設計和戰術設計兩種方式。戰略設計幫助我們理解哪些投入是最重要的;那些既有軟件資產是可以重新拿來使用的;哪些人應該被加到團隊中?戰術設計則幫助我們創建 DDD 模型中各個部件。

4、難以捉摸的業務價值:P6

5、DDD 如何幫助我們:P7

6、處理領域複雜性:P8

4、什麼樣的軟件系統值得做出 DDD 投入

應該將 DDD 應用在最重要是業務場景下,值得投入的是那些最重要的、複雜的東西,因爲這些東西將帶來可觀的回報。DDD 的作用是簡化,而不是複雜化。在使用 DDD 時,我們應該採用最簡單的方式對複雜領域進行建模,而不是使問題變得更加複雜。

5、如何評價業務領域的複雜性

可參考書中的 DDD 計分卡。(P8)

6、什麼是通用語言

通用語言是團隊共享的語言。領域專家和開發者使用相同的通用語言進行交流。事實上,團隊中每個人都使用相同的通用語言。不管你在團隊中的角色如何,只要你是團隊的一員,都將使用通用語言。團隊成員通過討論、參考資料、引用標準、查閱字典等對通用語言進行改進。有時我們發現,有些我們曾經認爲能很好表達業務的詞彙不再適用了,而另外的一些詞彙具有更好的效果。

7、使用通用語言的幾點注意事項

這裏的 “通用” 意思是“普遍的”,或者“到處都存在的”。通用語言在團隊範圍內使用,並且只表達一個單一的領域模型。

“通用語言” 並不表示全企業、全公司或者全球性的萬能的領域語言。

限界上下文和通用語言間存在一對一的關係。在一個限界上下文中使用其專屬的通用語言,對於那些不包含在通用語言中的概念,應該拒絕使用。

限界上下文是一個相對較小的概念,通常比我們起初想象的要小。限界上下文剛好能夠容納下一個獨立的業務領域所使用的通用語言。

只有當團隊工作在一個獨立的限界上下文中時,通用語言纔是 “通用” 的,

雖然我們只工作在一個限界上下文中,但是通常我們還需要和其他限界上下文打交道,這時可以通過上下文映射圖對這些限界上下文進行集成。每個限界上下文都有自己的通用語言,而有時語言間的術語可能有重疊的地方。

如果你試圖將某個通用語言運用在整個企業範圍之內,或者更大、跨企業的範圍內,你將失敗。

8、DDD 的業務價值

獲得了一個非常有用的領域模型

業務得到了更準確的定義和理解

領域專家可以爲軟件設計做出貢獻

更好的用戶體驗

清晰的模型邊界

更好的企業架構

敏捷、迭代式和持續建模

使用戰略和戰術新工具

9、實施 DDD 所面臨的挑戰

爲創建通用語言騰出時間和精力

持續的將領域專家引入項目

改變開發者對領域的思考方式

(自己額外增加的)現有技術生態多是爲貧血模型和設計的,要在這樣的情況下實現 DDD,是有難度的。

10、如何選擇開發方式

團隊是否有領域專家,如果有,你如何圍繞領域專家組織自己的團隊?

雖然目前來說,你的業務領域是簡單的,但它將來會變得複雜嗎?對於複雜的系統來說,使用事務腳本是存在風險的。當領域變得複雜時,是否有可能將系統重構到富含行爲的領域模型?

DDD 的戰術模式是否可用簡化與其他限界上下文的集成,不管是第三方的還是定製開發的?

使用事務腳本是否的確可用減少代碼量?(經驗表明,不管對於哪種開發方式,事務腳本都不能減少代碼量。這可能是由於在項目計劃階段,領域複雜性並沒有得到正確的認識所致。因此,我們需要在領域複雜性上下足功夫。)

你項目的進度安排是否允許在戰術建模上有所投入?

你核心域上的戰術投入能否消除架構變化所帶來的影響?事務腳本是做不到這一點的(和領域模型層相比,其他層更容易受到架構變化的影響)

客戶是否的確能從這種持續設計和開發的方式中獲益,或者有現成的產品能滿足他們的需求?換句話說,我們是否應該一開始就考慮定製化開發?你最終取悅你的客戶,而不是技術開發者,所以得慎重的做成選擇。

使用 DDD 的戰術開發模式會比其他開發方式更加困難嗎,比如事物腳本?(這個問題很大程度上取決於團隊成員的技能水平和是否有領域專家)

如果團隊已經具備了實施 DDD 的條件,我們還會可以的選擇另一種開發方式嗎?有些開發者已經將模型的持久化變得很實用了,比如使用 ORM、全聚合序列化和持久化、事件存儲(Event Store)、或者戰術 DDD 框架等。但是我們也不能排除還有熱衷於其他開發方式的開發者。

第 2 章:領域、子域和限界上下文

1、什麼是領域

從廣義上講,領域即是一個組織所做的事情以及其中所包含的一切。商業機構通常會確定一個市場領域模型,然後在這個市場中銷售產品和服務。每個組織都有它自己的業務範圍和做事方式。這個業務範圍以及其中所進行的活動便是領域。當你爲某個組織開發軟件時,你面對的便是這個組織的領域。這個領域對於你來說應該是明晰的,因爲你在這個領域中工作。

2、核心域、支撐子域、通用子域

3、問題空間

問題空間是領域的一部分,對問題空間的開發將產生一個新的核心域。對問題空間的評估應該同時考慮已有子域和額外所需子域。因此,問題空間是核心域和其他子域的組合。問題空間中的子域通常隨着項目的不同而不同,他們各自關注於當前的業務問題,這使得子域對於問題空間的評估非常有用。子域允許我們快速的瀏覽領域中的各個方面,這些方面對於解決特定的問題是必要的。

4、解決方案空間

解決方案空間包括一個或多個限界上下文,即一組特定的軟件模型。這是因爲限界上下文即是一個特定的解決方案,它通過軟件的方式來實現解決方案。

5、實施解決方案之前,需要對問題空間和解決方案空間進行評估

這個戰略核心域的名字是什麼,它的目標是什麼?

這個戰略核心域中包含哪些概念?

這個核心域的支撐子域和通用子域是什麼?

如何安排項目人員?

你能組建出一支合適的團隊嗎?

6、解決方案空間在很大程度上受到現有系統和技術的影響,我們應該根據分離的限界上下文仔細的思考。考慮的問題:P51

7、限界上下文

限界上下文是一個顯式邊界,領域模型便存在於邊界之內。在邊界內,通用語言中的所有術語和詞組都有特定的含義,而模型需要準確地反映通用語言。

8、如何劃分限界上下文

限界上下文並不只侷限於容納模型,它通常標定了一個系統、一個應用程序或者一種業務服務。模塊、聚合、領域事件、領域服務等基礎部件都屬於限界上下文。限界上下文主要用來封裝通用語言和領域對象,但同時它也包含了那些爲領域模型提供交互手段和輔助功能的內容。

限界上下文的劃分不因因爲一些平臺、框架、基礎設施或者開發任務拆分等因素來創建,有時,我們可以使用模塊來避免創建一些微小的限界上下文。模塊可以將多個限界上下文減少到一個,因爲我們可以用戰術化的手段來管理團隊任務分配,而不是限界上下文。

實施 DDD 的底線是,採用語言驅動,而不是技術手段。

應該儘量保證一個團隊,一個限界上下文。

第 3 章:上下文映射圖

1、上下文映射圖的表達方式

一個項目的上下文映射圖可以用兩種方式來表示。比較容易的一種是畫一個簡單的框圖來表示兩個或多個限界上下文之間的映射關係。該框圖表示了不同的限界上下文在解決方案空間中是如何通過集成相互關聯的。另一種更詳細的方式是通過限界上下文集成的源代碼實現來表示。

2、多個限界上下文之間的關係,各個團隊之間的關係(P79)

合作關係

共享內核

客戶方 - 供應方開發

遵奉者

防腐層(ACL)

開放主機服務(OHS)

發佈語言(PL)

另謀他路

大泥球

3、對於一個非常詳細的上下文映射圖,我們很有可能無法對其進行實時更新。將映射圖貼在牆上是有好處的,這樣可以方便團隊成員之間的交流。保持簡單性和敏捷性,拒絕繁文縟節,這樣我們所創建的上下文映射圖將對項目起推動作用,而不是阻礙作用。

第 4 章:架構

1、DDD 的一大好處便是它並不需要使用特定的架構。

由於核心域位於限界上下文中,我們可以在整個系統中使用多種風格的架構。有些架構包圍着領域模型,能夠全局性的影響系統,而有些架構則滿足了某些特定的需求。

2、選擇合適的架構風格和架構模式

架構風格闡述如何實現某種架構,架構風格之於架構就像設計模式之於設計一樣,它將不同架構實現所共有的東西抽象出來,例如客戶端 - 服務器架構風格、分佈式對象風格;而架構模式則關注一種架構中的某個方面,架構模式比設計模式更加寬泛。

在選擇架構風格和架構模式時,我們應將軟件質量考慮在內,而同時,避免濫用架構風格和架構模式,採用的架構是用來減少風險的,而不是增加失敗風險。架構風格和模式的選擇受到功能需求的限制,比如用例和用戶故事,用例驅動架構在當今軟件開發中依然適用。

3、非常精彩的實際項目的架構演進流程(P100)

關鍵詞:DI、前後端分離、REST、六邊形架構(端口與適配器)、SOA、CQRS、事件驅動架構、long-running process、Sagas、Event Sourcing

4、分層架構

原則:每層只能與位於其下方的層發生耦合。(較低層也是可以與較高層發生耦合的,但只侷限於採用觀察者模式或者調停者模式)

嚴格分層架構:某層只能與直接位於其下方的層發生耦合;

鬆散分層架構:允許任意上方層與任意下方層發生耦合。

5、依賴倒置原則結合 DDD 的分層架構(P108)

結合後的現象:當我們在分層架構中採用依賴倒置原則時,可能會發現,事實上已經不存在分層的概念了。無論是高層還是低層,他們都只依賴於抽象,好像把整個分層架構給推平了一樣。

6、六邊形架構(端口與適配器)(P110)

核心是,系統的輸入、輸出均通過適配器實現,內部應用層的統一的公共 API 接受適配器的請求。應用層的功能是根據應用程序的功能需求來創建用例,而不是客戶數量或輸出機制。

六邊形架構可以用來支持系統中的其他架構。比如可能採用 SOA 架構、REST 或者事件驅動架構;也有可能採用 CQRS;或者數據網織或基於網格的分佈式緩存;還有可能採用 Map-Reduce 這種分佈式並行處理方式。

7、SOA 的精神

業務價值高於技術策略

戰略目標高於項目利益

8、CQRS 的指導原則

如果一個方法修改了對象的狀態,該方法便是一個命令,它不應該返回數據。

如果一個方法返回了數據,該方法便是一個查詢,此時它不應該通過直接的或間接的手段修改對象的狀態。

9、事件驅動架構:基於消息的管道和過濾器處理過程的基本特徵

管道是消息通道。過濾器通過輸入管道接收數據,通過輸出管道發送數據。實際上,管道即是一個消息通道

端口連接過濾器和管道。過濾器通過端口連接到輸入和輸出管道。端口使得六邊形架構成爲首選的架構。

過濾器即是處理器。過濾器可以對消息進行處理,而不見得一定對消息進行過濾。

分離處理器。每個過濾處理器都是一個分離的組件。

松耦合。每個過濾處理器都相對獨立的參與處理過濾,處理器組合可以通過配置完成。

可換性。根據用例需求,我們可以重新組織不同處理器的執行順序,這同樣是通過配置完成。

過濾器可以使用多個管道。在命令行例子中,過濾器只從一個管道中讀寫數據,但是消息過濾器可以從不同的管道中讀寫數據,這表示了一種並行的處理過程。

並行使用同種類型的過濾器。對於最繁忙的和最慢的過濾器來說,我們可以並行的採用多個相同類型的過濾器來增加處理量。

10、事件驅動架構:設計長時處理過程(Saga)的幾種方法(已支持 Saga 的消息機制:NServiceBus、MassTransit)

將處理過程設計成一個組合任務,使用一個執行組件對任務進行跟蹤,並對各個步驟和任務完成情況進行持久化。

將處理過程設計成一組聚合,這些聚合在一系列的活動中相互協作。一個或多個聚合實例充當執行組件並維護整個處理過程的狀態。這種方式被 Amazon 的 Pat Helland 所提倡。

設計一個無狀態的處理過程,其中每一個消息處理組件都將對所接收到的消息進行擴充——即向其中加入額外的數據信息——然後再將消息發送到下一個處理組件。在這種方法中,整個處理過程的狀態包含在每條消息中。

11、事件源技術上的優勢(附錄 A 詳盡的闡述瞭如何在聚合上實現事件源)

事件歷史可以用來消除系統中的 bug,對調試也有很大的益處。

事件源有助於獲得高吞吐量的領域模型,從而極大的提高事務處理效率。比如,向單張數據庫表中追加事件是非常快的。

另外,事件源還有助於提高 CQRS 查詢模型的伸縮性,因爲此時查詢模型的數據源可以在事件存儲更新之後得到靜默更新。

可以複製多個查詢模型的數據源實例以滿足更多的新增客戶。

12、事件源業務上的優勢

用新的或者修改後的事件向事件存儲打補丁可以修正許多問題。這對於業務來說可能不那麼顯而易見,但是這些補丁可以在很大程度上減少由模型中的 bug 所帶來的系統問題。

除了補丁之外,我們可以通過重放一組事件的方式來重做或撤銷對模型的修改。

有了所有事件的歷史消息,業務層便可以考慮很多諸如 “如果.... 會怎麼樣” 的問題。即通過重放一組發生在聚合上的事件,業務層可以得到很多問題的答案。通過模擬這些虛擬的業務場景,業務層可以從中獲得不少好處,而這也是實現業務智能化的一種方式。

13、數據網織和基於網格的分佈式計算(P143)

第 5 章:實體

1、爲什麼要使用實體

DDD 並不總能滿足我們的業務需求,有時,一個基於 CRUD 的系統會更加合適,節約了時間和金錢。但是,隨着軟件複雜性的增加,我們就越能體會到由錯誤的工具選擇所帶來的限制,這時維護一個基於 CUD 的系統可能是非常昂貴的。由於只從數據開發,CRUD 系統是不能創建出好的業務模型的。

2、唯一標識:創建實體身份標識的策略

用戶提供一個或多個初始唯一值作爲程序輸入,程序應該保證這些初始值是唯一的。

程序內部通過某種算法自動生成身份標識,此時可以使用一些類庫或框架,當然程序自身也可以完成這樣的功能。(例如 UUID、GUID、Apache Commons Id)

程序依賴於持久化存儲,比如數據庫,來生成唯一標識。

另一個限界上下文(系統或程序)已經決定出了唯一標識,這作爲程序的輸入,用戶可用在一組標識中進行選擇。

3、委託標識

有些 ORM 工具,比如 Hibernate,通過自己的方式來處理對象的身份標識。Hibernate 更傾向於使用數據庫提供的機制,比如使用一個數值序列來生成實體標識。如果我們自己的領域需要另外一種實體標識,此時這兩者將產生衝突。爲了解決這個問題,我們需要使用兩種標識,一種爲領域所用,一種爲 ORM 所使用,在 Hibernate 中,這被稱爲委派標識。

4、發現實體及其本質特徵(這方面書中的方法論還是少了些,從書中並不能很好的知道分析的方法,還是得從實操中獲得積累)

實體及其本質特徵

挖掘實體的關鍵行爲

基於角色和職責來建模實體的優缺點

如何創建實體

採用哪種驗證方式

跟蹤實體的狀態變化

第 6 章:值對象

1、值對象的優點

值對象用於度量和描述事物,我們應該儘量使用值對象來建模而不是實體對象。即便一個領域概念必須建模成實體,在設計時也應該更偏向於將其作爲值對象容器,而不是子實體容器。因爲我們可以非常容易的對值對象進行創建、測試、使用、優化和維護。

2、如何確定一個領域概念應該建模成一個值對象?

當你只關心某個對象的屬性時,該對象便可作爲一個值對象。爲其添加有意義的屬性,並賦予它相應的行爲。我們需要將值對象看成不變對象,不要給它任何身份標識,還應該儘量避免像實體對象一樣的複雜性。

3、值對象的特徵

它度量或者描述了領域中的一件東西。

它可以作爲不變量(不變性,當需要變更屬性時,應採取替換而不是更改 該值對象)。

它將不同的相關的屬性組合成一個概念整體(比如 {500 美元} 具有兩個屬性,一個是 500,一個是美元,單獨一個 500 可能表達另外的意思,單獨一個美元更不可能表達成值對象,只有這兩個者聯合起來纔是一個表達貨幣度量的概念整體)。

當度量和描述改變時,可以用另一個值對象予以替換。

它可以和其他值對象進行相等性比較。

它不會對協作對象造成副作用。(傳給值對象方法的參數依然應該是值對象,如果一個值對象方法將一個實體對象作爲參數時,最好的方法是,讓實體對象使用該方法的返回結果來修改其自身的狀態)

4、劃分值對象範圍的方法

如果你視圖將多個屬性加在一個實體上,但這樣卻弱化了各個屬性之間的關係,那麼此時你便應該考慮將這些相互關聯的屬性組合在一個值對象中了。每個值對象都是一個內聚的概念整體,它表達了通用語言中的一個概念。如果其中一個屬性表達了一種描述性概念,那麼我們應該把與該概念相關的所有屬性集中起來。如果其中一個或多個屬性發生了改變,那麼可以考慮對整體值對象進行替換。

5、最小化集成

可能的情況下儘量使用值對象來完成限界上下文之間的集成,這對於許多需要消費標準類型的上下文來說都是適用的。這樣的好處是可以達到最小化集成,即可以最小化下游模型中用於管理職責的屬性數目。使用不變的值對象使得我們做更少的職責假設。

6、根據領域模型來設計數據模型,而不是根據數據模型來設計領域模型。

無論你使用上面技術來完成數據建模,數據庫實體、主鍵、引用完整性和索引都不能用來驅動你對領域概念的建模。DDD 不是關於如何根據範式來組織數據的,而是在一個一致的限界上下文中建模一套通用語言。在這個過程中,應該儘量避免數據模型從領域模型中泄漏到客戶端中。

7、持久化值對象

文中只舉例了 Hibernate 的相關方法,其他 ORM 框架需要再進行查閱。

第 7 章:領域服務

1、領域服務(領域服務不是應用服務)

領域中的服務表示一個無狀態的操作,它用於實現特定於某個領域的任務。當某個操作不適合放在聚合和值對象上時,最好的方式便是使用領域服務了。

不要濫用領域服務,濫用領域服務將導致貧血領域模型這種反模式。

2、應用服務

應用服務只用於協調任務,調用單個業務操作(領域服務),而由該業務操作去處理所有的業務細節。我們絕不能將業務邏輯放到應用層,即使業務邏輯非常簡單,但它依然是業務邏輯。雖然我們不會講業務邏輯放在應用層,但是應用層卻可以作爲領域服務的客戶端。

3、領域服務命名?採用獨立接口?

P245(不一定要遵循 Java EE 規範)

第 8 章:領域事件

1、領域事件

領域專家所關心的發生在領域中的一些事件。將領域中所發生的活動建模成一系列的離散事件,每個事件都用領域對象來標識。領域事件是領域模型的組成部分,表示領域中所發生的事情。

2、領域專家和領域事件

雖然領域專家在起初可能意識不到所有類型的領域事件,但是通過討論之後,他們是應該能夠了解到其中的原因的。當團隊成員對領域事件達成一致之後,領域事件便是 “通用語言” 的正式組成部分了。

**3、應用服務控制着事物。**不要在事件通知過程中修改另外一個聚合試了,因爲這樣破壞了聚合的一大原則:在一個事務中,只對一個聚合進行修改。

4、相對分佈式事務來講,領域事件是一種更輕量級的最終一致性的實現方式。

5、如何保證領域模型存儲和事件存儲(消息設施所使用的持久化存儲)之間的一致性?

領域模型和消息設施共享持久化存儲(比如數據源)。在這種情況下,對模型的修改和對事件的提交發生在同一個本地事務中。這種方式的優點在於性能很高,而缺點在於消息系統的存儲區域(比如數據庫表)必須和領域模型位於同一個數據庫中。當然,如果你的領域模型和消息機制不能共享持久化存儲,這種方式便不合適了。

領域模型的持久化存儲和消息持久化存儲由全局的 XA 事務(兩階段提交)所控制。這種方式的優點在於模型和消息所使用的持久化存儲可以分開;缺點在於全局事務需要額外的支持,但不見得所有的存儲機制都支持全局事務。全局事務的成本是很高的,而性能卻很差。有可能出現的情況是,要麼領域模型存儲不支持 XA 事務,要麼消息存儲不支持 XA 事務,要麼兩者都不支持。

領域模型的持久化存儲中,創建一個特殊的存儲區域(比如一張數據庫表),該區域用於存儲領域事件,這便是一個事件存儲(Event Store)。這種方式和方式 1 相似,但是此時的事件存儲區域不再由消息機制所擁有和控制,而是你的限界上下文。同時,你需要創建一個消息外發組件將事件存儲中的所有消息機制發送出去。這種方式的優點在於:模型修改和事件提交可以同時位於單個本地事物中。另一個額外的優點是,我們可以發佈基於 Rest 的事件通知。使用這種方式時,消息機制所使用的消息存儲是完全私有的。因此,這種方式的缺點是,我們可能需要定製開發一個消息轉發組件來發送消息,同時客戶需要對消息進行消重處理。

6、時延與自治服務和系統

有些業務服務可能需要更高的吞吐量,此時我們需要好好的考慮最大容許時延,系統的架構應該滿足在事件時延上的需求。對於自治服務和支持它們的消息設施來說,我們應該在可用性和可伸縮性上下足功夫,以便更好地完成那些非功能性的需求。

7、事件存儲的方式

將事件存儲作爲一個消息隊列來使用,該消息隊列的作用是將所有的領域事件通過消息設施發佈出去。這種方法是本書中首要使用的方法,它允許在不同的限界上下文之間進行集成,此時遠程的訂閱方將對領域事件做出反應以滿足自身上下文的需求。

將相同的事件存儲用於基於 REST 的事件通知。

檢查由模型的命令方法所產生的所有結果的歷史記錄。這可以用於跟蹤 bug,不只是跟蹤自己模型中的 bug,還可以跟蹤客戶方中的 bug。因此,此時的事件存儲不再只是一個簡單的審計日誌。審計日誌對於調試來說是有用的,但是卻很少包含由聚合命令方法所產生的完整結果。

使用事件存儲中的數據來進行業務預測和分析。很多時候,業務人員只有在需要使用這些數據的時候才能意識到這些歷史數據的重要性,而在沒有事件存儲來維護這些數據的時候,他們便捉襟見肘了。

當從資源庫中獲取一個聚合實例時,使用事件來重建該聚合實例。對於事件源來說,這是一個必要的組成部分。重建聚合通過順序的應用發生在該聚合上的所有事件來完成。你可以將任意數量的事件用於聚合重建。

8、REST 風格事件通知的優缺點

如果多個客戶方都可以通過單個 URI 來請求相同的事件通知,那麼此時 REST 便是合適的。一個事件通知可以擁有任意多的消費方。雖然 REST 使用的是 “拉” 的方式,而不是 “推的” 的方式。

如果一個或多個消費方需要從多個發佈方中獲取資源以順序的完成一系列任務,那麼此時你便會感到 REST 所帶來的痛苦了。這實際上描述了一個消息隊列,許多發送方同時爲一個或多個消費方服務,此時事件的接收順序是重要的。對於實現消息隊列來說,“拉” 的方式並不是一個好的選擇。

9、通過消息中件發佈事件通知

第 9 章:模塊

1、模塊的作用

在 DDD 中,模型中的模塊表示了一個命名的容器,用於存放領域中內聚在一起的類,將類放在不同的模塊中的目的在於達到松耦合性。由於 DDD 中的模塊並不是一個通用的存儲區域,因此對其進行適當的命名是重要的。事實上,模塊名是通用語言的重要組成部分,應該反映出它們在領域中的概念。

2、設計模塊的簡單原則

模塊應該和領域概念保持一致。通常,對於一個或一組內聚的聚合來說,我們都相應的創建一個模塊。

根據通用語言來命名模塊。這也是 DDD 的一個基本目標。

不要機械式的根據通用的組件類型和模式來創造模塊。如果我們將所有的聚合放在一個模塊中,將所有的領域服務放在一個模塊中,又將所有的工廠放在另一個模塊中,那麼我們是得不到什麼好處的。這有悖於 DDD 設計原則,同時還會限制我們創建富含行爲的領域模型。此時,我們的關注點不是在領域上,而是在當前的組件和模式上。

設計松耦合的模塊。模塊間的松耦合性與類間的松耦合性具有相同的好處。這樣有利於我們維護和重構一些模塊層面上的概念。

當同層模塊間出現耦合時,我們應該杜絕循環依賴。要使不同的模塊完全獨立是不可能的。但是,如果我們消除同層模塊之間的雙向依賴,我們便可以減少它們之間的耦合度(比如產品依賴於開發團隊,但開發團隊卻不依賴於產品)

在父模塊和子模塊之間放鬆原則(父模塊即位於較高層次的模塊,子模塊即位於較低層次的模塊)。要消除父模塊和子模塊之間的依賴的確是困難的。但是在有可能的情況下,我們依然應該避免它們之間的循環依賴,只有在無法避免時才引入循環依賴(比如,父模塊中的對象創建一個子模塊中的對象,而子模塊對象又需要維護對父模塊對象的引用)

不要將模塊設計成一個靜態的概念,而是與模型中的對象一道進行建模。如果模型概念將隨時間而改變,這往往意味着對應的模塊也應該隨之而變。當你發現概念名和模塊名不再匹配時,你應該對模塊進行重構。誠然,這是痛苦的,但是和那些糟糕的模塊命名相比,這些痛苦是值得的。

3、先考慮模塊,再是限界上下文

有時,通用語言可以很好的幫助我們做出正確的選擇。但是另外的時候,其中的術語將變得非常含糊。在這種情況下,我們並不清楚如何劃分上下文邊界。此時,我們可以首先將它們放在一起,使用模塊來對模型進行劃分,而不是限界上下文。

但是,這並不意味着我們就應該限制對限界上下文的創建。我們應該通過通用語言的需求來劃分模型邊界。你應該知道,限界上下文不是用來代替模塊的。使用模塊的目的在於組織那些內聚在一起的領域對象,對於那些內聚性不強或者沒有內聚性的領域對象來說,我們應該將它們劃分在不同的模塊中。

第 10 章:聚合

1、設計聚合的原則

一個事務中只修改一個聚合實例。試圖保持多個聚合實例間的一致性通常意味着我們缺少了某些聚合不變條件。

設計聚合時主要關注的是聚合的一致性邊界,而不是創建一個對象樹。

設計小聚合,擅用根實體、值對象;設計時考慮下系統性能和可伸縮性。

新的用例可能引導我們重新對聚合進行建模,甚至產生了一個大的聚合(幾個聚合構成),但並不意味着要在單個事物中維持聚合的一致性,業務目標可以通過聚合間的最終一致性來實現。

通過唯一標識引用其他聚合

在聚合邊界之外使用最終一致性。任何跨聚合的業務規則都不能總是保持處於最新狀態。通過事件處理(消息隊列?)、批處理或者其他更新機制,我們可以在一定時間之內處理好他方依賴。

2、一致性處理的指導原則

對於一個用例,問問是否應該由執行該用例的用戶來保證數據的一致性。如果是,請使用事務一致性,當然此時依然需要遵循其他聚合原則。如果需要其他用戶或者系統來保證數據一致性,請使用最終一致性。

3、打破原則的理由

方便用戶界面

缺乏技術機制(沒有消息機制、分佈式事物、定時器、後臺線程等等技術組件)

全局事務

查詢性能

4、迪米特法則

強調了 “最小知識” 原則。考慮一個客戶端對象需要調用系統中其他對象的行爲方法的場景,此時我們可以將後者稱爲服務對象。在客戶端對象使用服務對象時,它應該儘量少的知道服務對象的內部結構。客戶端對象不應該知道任何關於服務對象屬性的信息。客戶端對象可以根據表層接口調用服務對象上的命令方法。然而,客戶端對象不應該滲入到服務對象的內部。如果客戶端所需服務位於服務對象的內部,那麼此時客戶端對象不應該訪問這樣的服務。對於服務對象來說,它只應該提供表層接口,在接口方法被調用時,它將操作委派給內部方法以完成功能。

對迪米特法則做一個簡單的總結:任何對象的任何方法只能調用以下對象中的方法:①該對象自身;②所傳入的參數對象;③它所創建的對象;④自身所包含的其他對象,並且對那些對象有直接訪問權。

5、“告訴而非詢問” 原則

一個對象不應該被告知如何執行操作。對於客戶端來說,這裏的 “非詢問” 表示:客戶端對象不應該首先詢問服務對象,然後根據詢問結果調用服務對象中的方法,而是應該通過調用服務對象的公共接口的方式來 “告訴” 服務對象所要執行的操作。該原則和迪米特原則存在相似之處,但是使用起來更加簡單。

6、實現原則

創建具有唯一標識的根實體

優先使用值對象

使用迪米特法則和 “告訴而非詢問” 原則

樂觀併發

不要在聚合中注入資源庫和領域服務,我們應該在聚合命令方法執行之前進行查找,然後將其傳入命令方法。(否則將會有很多額外的對象引用產生,系統內存喫緊、垃圾回收週期漫長的問題會更加嚴重)

第 11 章:工廠

1、使用工廠的主要動機:

將創建複雜對象和聚合的職責分配給一個單獨的對象,該對象本身並不承擔領域模型中的職責,但是依然是領域設計的一部分。工廠應該提供一個創建對象的接口,該接口封裝了所有創建對象的複雜操作過程,同時,它並不需要客戶去引用那個實際被創建的對象。對於聚合來說,我們應該一次性地創建整個聚合,並且確保它的不變條件得到滿足。

2、聚合根中的工廠方法的好處

有效的表達限界上下文中的通用語言

減輕客戶端在創建新聚合實例時的負擔

確保所創建的實例處於正確的狀態

第 12 章:資源庫

1、嚴格來講,只有聚合才擁有資源庫。存在兩種類型的資源庫設計:面向集合設計和麪向持久化設計。

2、面向集合資源庫

我們可以將面向集合的資源庫看成是一種傳統的方式,因爲它體現了原生 DDD 資源庫模式的基本思想。這種資源庫模擬了一個集合,或者至少模擬了集合上的標準接口。此時,從資源庫的接口來看,我們根本看不出其背後還存在着持久化機制,也感覺不到我們是在向存儲區域中保存數據。

3、面向集合資源庫精要

一個資源庫應該模擬一個 set 集合。無論採用什麼類型的持久化機制,我們都不應該允許多次添加同一個聚合實例。另外,當從資源庫中獲取到一個對象並對其進行修改時,我們並不需要 “重新保存” 該對象到資源庫中。考慮下集合的情形,要修改其中的一個對象,我們只需要先從集合中獲取到該對象的引用,然後在該對象上執行行爲方法即可。

4、持久化機制支持面向集合資源庫的方法:

隱式讀時複製(Implicit Copy-on-Read):在從數據存儲中讀取一個對象時,持久化機制隱式的對該對象進行復制,在提交時,再將該複製對象與客戶端中的對象進行比較。詳細過程如下:當客戶端請求持久化機制從數據存儲中讀取一個對象時,該持久化機制一方面將獲取到的對象返回給客戶端,一方面立即創建一份該對象的備份(除去延遲加載部分,這些部分可以在之後實際加載時再進行復制)。當客戶端提交事務時,持久化機制把該複製對象與客戶端中的對象進行比較。所有的對象修改都將更新到數據存儲中。

隱式寫時複製(Implicit Copy-on-Write):持久化機制通過委派來管理所有被加載的持久化對象。在加載每個對象時,持久化機制都會爲其創建一個微小的委派並將其交給客戶端。客戶端並不知道自己調用的是委派對象中行爲方法,委派對象會調用真實對象中的行爲方法。當委派對象首次接受到方法調用時,它將創建一份對真實對象的備份。委派對象將根據發生在真實對象上的改變,並將其標記爲 “骯髒的”。當事務提交時,該事務檢查所有的“骯髒” 對象並將對它們的修改更新到數據存儲中。

5、面向集合資源庫的高性能 ORM 工具:TopLink、EclipseLink

6、面向持久化資源庫精要

在向數據存儲中添加新建對象或修改既有對象時,我們都必須顯示的調用 put 方法,該方法將以新的值來替換先前關聯在某個鍵上的原值。這種類型的數據存儲可以極大的簡化對聚合的讀寫。正因如此,這種數據存儲也稱爲聚合存儲或面向聚合數據庫。

7、額外的行爲

一個資源庫應該儘可能的模擬一個集合,因此命名也應該類似。比如 count 可以換爲 size。

在數據存儲過程中,可能需要執行一些計算過程來滿足某些非功能性需求。但是這些功能最好放在領域服務中,因爲領域服務正是用於處理那些無狀態的、特定於領域的操作。

可以出於性能的考慮,只查找聚合根的一部分,但是謹慎使用這種方式。

獲取的數據如果來源於多個聚合,可以使用用例優化查詢的方法直接查詢所需要的數據,直接在持久化機制上執行查詢,然後將查詢結果放在一個 “值對象” 中予以返回。

在使用用例優化查詢時,如果發現必須創建多個查詢方法,那麼這可能是一種壞味道,一般意味着對聚合邊界的劃分是錯誤的。如果的確發生了這種情況,並且你確認對聚合邊界的設計是正確的,此時應該考慮使用 CQRS 了。

8、管理事務的位置

通常來說,我們將事務放在應用層中。然後爲每個主要的用例創建一個門面,門面中的業務方法通常都是粗粒度的,常見的情況是每一個用例流對應一個業務方法。業務方法對用例所需操作進行協調

對事務的管理絕對不應該放在領域模型和領域層中。通常來說與,與領域模型相關的操作都非常細粒度的,以至於無法用於管理事務,領域模型也不應該意識到事務的存在。

9、使用事務的警告

不要過度的在領域模型上使用事務。我們必須慎重的設計聚合以保證正確的一致性邊界。有時,在測試環境下,在單個事務中修改多個聚合可能工作得很好,但是在產品環境下,卻有可能出現由併發所導致的事務失敗。

10、資源庫 VS 數據訪問對象(DAO)

一個 DAO 主要從數據庫表的角度來看待問題,並且提供 CRUD 操作。表模塊、表數據網關、活動記錄這樣的模式應該用於事務腳本程序中,需要與領域模型分離開來對待,這些與 DAO 相關的模式通常只是對數據庫表的一層封裝。

資源庫和數據映射器則更加偏向於對象,因此通常被用於領域模型中。通常來說,你可以將資源庫當做 DAO 來看待。但是注意一點,在設計資源庫時,我們應該採用面向集合的方式,而不是面向數據訪問的方式。這有助於將領域當作模型來看待,而不是 CRUD 操作。

第 13 章:集成限界上下文

1、集成限界上下文的方式

RPC

消息機制,通過發佈 - 訂閱機制

RESTful HTTP。作者認爲這並不是一種形式的 RPC。

2、分佈式系統原則

網絡是不可靠的

總會存在時間延遲,有時甚至非常嚴重

帶寬是有限的

不要假設網絡是安全的

網絡拓撲結構將發生變化

知識和政策在多個管理員之間傳播

網絡傳輸是有成本的

網絡是異構的

3、通過消息集成限界上下文,長時處理過程的狀態機和超時跟蹤器。

4、防腐層

第 14 章:應用程序

1、應用程序與領域模型的關係

領域模型通常位於應用程序的中心位置。應用程序通過用戶界面向外展示領域模型的概念,並且允許用戶在模型上執行各種操作。用戶界面使用應用服務來協調用例任務,管理事務,並執行一些必要的安全授權。另外,用戶界面、應用服務和領域模型依賴於企業級的特定平臺設施的支持。這些基礎設施的實現細節通常包含組件容器、應用程序管理、消息系統和數據庫等。

2、什麼是應用程序

書中使用的 “應用程序” 標識那些支撐核心域模型的最近,通常包括領域模型本身、用戶界面、內部使用的應用服務和基礎設施組件等。至於這些組件中應該包含些什麼,這是根據應用程序的不同而不同的,並且有可能受到所有架構的影響。

3、用戶界面系統的類型(P469)

純粹請求 - 應答式 web 用戶界面,也稱爲 web1.0。典型框架有 Struts、SpringMvc 和 Web Flow、ASP.NET 等。

基於 web 的富互聯網應用(Rich Internet Application,RIA)用戶界面,包括那些使用 DHTML 和 Ajax 的系統,也稱爲 Web2.0。Google GWT、Yahoo YUI、Ext JS、Adobe Flex 和 Microsoft Silverlight 均屬於這個範疇。

本地客戶端 GUI(比如 Windows、Mac 和 Linux 的桌面用戶界面)

4、應用服務(P478)

將應用服務於領域服務等同起來是錯誤的,它們並不相同。我們應該將所有的業務領域邏輯放在領域模型中,不管是聚合、值對象或者領域服務;而將應用服務做成很薄的一層,並且只使用它們來協調對模型的任務操作。

5、基礎設施(P489)

基礎設施的職責是爲應用程序的其他部分提供技術支持。這裏,雖然我們避免對分層的討論,但是保持着依賴倒置原則的心態依然是有用的。因此,從架構上講,無論基礎設施位於什麼地方,只要它的組件依賴於用戶界面、應用服務和領域模型中的接口,而這些接口又需要特殊的技術支持,那麼它都能工作得很好。這樣,在應用服務獲取資源庫時,它只會依賴於領域模型中的接口,而實際使用的則是基礎設施中的實現類。

資源庫的實現被放在了基礎設施層中,因爲它們負責處理數據存儲,而這些不屬於模型的職責。你可以使基礎設施層實現那些與消息相關的接口,比如消息隊列和 E-mail 等。如果還有一些特殊的用戶界面組件來處理諸如圖表之類的展現,那麼它們也應該放在基礎設施層中。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/WrpV69KibaKaJrF92uasyA