第二章:模型驅動設計MDA的構建模組
這些模式根據領域驅動的設計投射了廣泛的面向物件設計的最佳實踐。他們指導決策以澄清模型,並使模型和實施保持一致,每一個都強化了對方的有效性。精心設計各個模型元素的細節為開發人員提供了一個穩定的平臺,可以從中探索模型並使其與實現密切相關。
分層架構
在面向物件的程式中,UI,資料庫和其他支援程式碼通常直接寫入業務物件(DTO資料傳輸物件)。其他業務邏輯嵌入在UI小部件和資料庫指令碼的行為中。發生這種情況是因為從短期來看,這是使事情順利進行的最簡單方法。(資料表生成大量臨時資料物件DTO的CRUD增刪改查是最簡單開發方法)
當與領域相關的程式碼通過如此大量的其他程式碼(DTO或VO)進行傳播時,很難看到和邏輯推理。UI的表面更改實際上導致改變業務邏輯;要更改業務規則,可能需要對UI程式碼、資料庫程式碼或其他程式元素進行細緻的跟蹤。實現連貫的,模型驅動的物件變得不切實際;自動化測試很尷尬,由於每項活動都涉及所有技術和邏輯,因此必須保持程式非常簡單或無法理解。
因此:
隔離領域模型和業務邏輯的表示式,消除對基礎架構、使用者介面甚至非業務邏輯的應用程式邏輯的依賴性。將複雜程式劃分為多個層,在每個層內開發一個具有凝聚力的設計,並且僅影響下面的層。遵循標準的架構模式,為上面的層提供鬆散的耦合。在一個層中集中與領域模型相關的所有程式碼,並將其與使用者介面、應用程式和基礎結構程式碼隔離開來。領域物件不受顯示自身,儲存自身,管理應用程式任務等的責任,可以專注於表達領域模型。這使得模型能夠發展到足夠豐富和清晰,以捕獲必要的業務知識並使其發揮作用。
這裡的關鍵目標是隔離,相關模式,例如“六邊形體系結構”(banq注:參考鮑勃大叔的清晰架構),可以在允許我們的領域模型表達中避免依賴和引用其他系統問題的程度上提供或改善。
實體
許多物件代表一個系列事物:有連續性、有標識的、經歷一個生命週期,儘管它的屬性可能會改變。(banq注:可以用資料表中的實體概念類比理解,兩者相差不多)
某些物件主要不是由其屬性定義的,它們代表了一個貫穿時間並經常跨越不同表示的有標識的系列事務。有時,即使屬性不同,這樣的物件也必須與另一個物件匹配,必須將物件與其他物件區分開來,即使它們可能具有相同的屬性,錯誤的標識可能導致資料損壞。
因此:
當一個物件通過其標識而不是其屬性進行區分時,將這種初心一直保持在模型的定義中。保持類的定義簡單,重點關注生命週期的連續性和標識。
定義區分每個物件的方法,無論其形式或歷史如何,警惕需要按屬性匹配物件的需求。定義一個能保證為每個物件生成唯一結果的操作方法,也可以通過附加保證唯一的符號如(主健ID)。這種識別方法可能來自外部,也可能是由系統建立的任意識別符號,但它必須與模型中的標識區別相對應。
模型必須定義那些意味著相同的東西。(banq注:參考類Class的定義,參考用汽車比喻理解OOP:)
DDD實體
值物件
一些物件用於描述或計算事物的某些特徵。
許多物體無需進行概念上的認同和區分,無此必要,實體的缺點是帶來設計的複雜性。
跟蹤實體的標識至關重要,但將標識ID附加到其他物件可能會損害系統性能,增加分析工作量。因此使所有物件看起來相同來混同模型的區別,有時這樣反而簡單,軟體設計與複雜性是一場持續的戰鬥。我們必須做出實體和值物件區分,不能將所有物件都看成實體,預設情況為值物件,僅在必要時才使用特殊處理。
但是,如果我們認為這類物件只是缺乏標識,那麼我們的工具箱或詞彙量就沒有增加太多,實際上,這些物件具有自己的特徵,以及它們對模型的重要意義,這些是描述事物特徵的物件。
因此:
如果只關心模型元素的屬性和邏輯(而不是區分它們),則將其歸類為值物件。使它表達它傳達的屬性的含義並賦予它相關的功能。將值物件視為不可變;使所有操作無副作用 - 不依賴於任何可變狀態的函式。不要為值物件賦予任何標識,並避免維護實體帶來的設計複雜性。
DDD值物件
領域事件
領域專家關心發生了的事情。
實體負責跟蹤其狀態和規範其生命週期的業務規則。但是如果你需要知道狀態變化的實際原因,只記錄狀態是無法明確原因的,並且可能很難解釋系統如何得到這個狀態結果邏輯推理過程,審計跟蹤可以實現跟蹤,但通常不適合用於程式本身的邏輯,實體的更改歷史記錄可以允許訪問先前的狀態,但如果忽略保留這些更改的含義,任何對資訊的任何操作都是程式性的、連續的,這些含義會被推出領域層。
分散式系統中也出現了一系列獨特但相關的問題,分散式系統的狀態不能始終保持完全一致(只能最終一致),但是我們必須始終保持聚合內部狀態一致,同時非同步通知其他聚合進行更改,當這種更改訊息在網路的節點之間傳播時,可能難以解決無序到達或來自不同源的多個更新(事件訊息的順序問題)。
因此:
將有關領域中發生的事情資訊建模為一系列離散事件,將每個事件表示為領域物件,這些與反映軟體本身內活動的系統事件不同,儘管系統事件通常與領域事件相關聯,或者作為對領域事件的響應的一部分,或者作為將領域事件的資訊傳遞到系統中 。
領域事件是領域模型的完整部分,表示領域中發生的事情。忽略不相關的領域活動,同時明確表示領域專家想要跟蹤或被通知的事件,或者與其他模型物件中的狀態更改相關聯的事件。
在分散式系統中,可以從特定節點當前已知的領域事件推斷出實體的狀態,從而在缺乏關於整個系統的完整資訊的情況下實現相干模型。
領域事件通常是不可變的,因為它們是過去某些事物的記錄,除了對事件的描述之外,領域事件通常還包含事件發生時間的時間戳以及事件中涉及的實體的標識。此外,領域事件通常具有單獨的時間戳,指示事件何時進入系統以及輸入事件的人員的身份。領域事件的標識可以基於這些屬性的某些集合。因此,例如,如果同一事件的兩個例項到達節點,則可以將它們識別為相同。
(banq注:參考 https://www.jdon.com/event.html ,下圖是Jdon框架通過領域事件實現乾淨的架構)
服務
有時,它不是一件事。
來自領域的一些概念不能自然地建模為物件,如果強制所需的領域功能成為實體或值的責任,要麼扭曲基於模型的物件的定義,要麼新增無意義的人造物件。
因此:
當領域中的重要流程或轉換不是實體或值物件的自然責任時,將操作作為宣告為服務的獨立介面新增到模型,定義服務契約,一組關於與服務互動的斷言。在特定有界上下文的普遍存在的語言中陳述這些斷言。為服務命名,這也成為普遍存在的語言的一部分。
DDD服務
模組
每個人都使用模組,但很少有人將它們視為模型的完整部分,程式碼被分解為各種類別,從技術架構的各個方面到開發人員的工作分配,即使是很多重構的開發人員也傾向於滿足於專案早期構想的模組。
耦合和內聚的解釋傾向於使它們聽起來像技術指標,根據關聯和相互作用的分佈進行機械判斷,然而,不僅僅是程式碼被分為模組,還有概念。一個人一次可以考慮多少事情是有限的(因此需要低耦合),不連貫的思想碎片就像一種無差別的心靈雞湯一樣難以理解。
因此:
選擇講述系統故事的模組,幷包含一組緊密結合的概念,為模組命名,使其成為普遍存在的語言的一部分,模組是模型的一部分,它們的名稱應該反映對領域的洞察力。
模組之間必須是低耦合,如果無法找到一種模型改變方法以實現概念上鬆耦合,那麼久選擇整體概念,可以通過有意義的方式將元素組合在一起,基於獨立理解和邏輯推理的概念意義上尋求低耦合(而不是強制),優化模型,直到根據高階域概念進行分割槽,並且相應的程式碼也被解耦。
(又名打包)
聚合
很難保證具有複雜關聯的模型中物件更改的一致性,物件應該保持自己的內部一致狀態,但是它們可能會被概念上構成部分的其他物件的變化所遮蔽;謹慎的資料庫鎖定方案會導致多個使用者無意義地相互干擾,並使系統無法使用,在多個伺服器之間分配物件或設計非同步事務時會出現類似問題。(banq注:聚合是為高一致性的強事務而設計的,不可能所有實體之間操作都是強事務,所以使用通用的事務元件中介軟體比如JTA等其實是一種試圖用技術解決業務的緣木求魚辦法,大概只適合那些不願意、或者無法進行業務詳細分析設計的場合。)
因此:
將實體和值物件聚類為聚合並定義每個大物件的邊界。選擇一個實體作為每個聚合的根,並允許外部物件僅保留對根的引用(對內部成員的引用僅在單個操作中使用)。定義整個聚合的屬性和不變數,並對根或某些指定的框架機制賦予執行責任。
使用相同的聚合邊界來管理事務和分發。
在聚合邊界內,同步應用一致性規則;跨越邊界,非同步處理更新。
將聚合儲存在一臺伺服器上;允許在節點之間分配不同的聚合。
當設計決策沒有根據聚合邊界進行引導設計時,重新考慮模型。領域情景是否暗示了一個重要的新見解?這些變化通常會提高模型的表現力和靈活性,以及解決事務和分發問題。
DDD聚合
儲存庫
以無處不在的語言查詢訪問聚合的表達。
可遍歷關聯的擴散(根據外來鍵一個接一個的查詢)僅用於發現混亂模型;在成熟模型中,查詢通常表達領域概念,然而查詢可能會導致問題。
使用大多數資料庫等基礎架構技術會導致複雜性,這種複雜性會瀰漫在資料庫的呼叫程式碼中,從而導致開發人員對領域層進行愚蠢的處理,這使得模型變得無關緊要,沒有模型好像也沒用事情。
查詢框架可以封裝大部分技術複雜性,使開發人員能夠以更自動化或宣告性的方式從資料庫中提取他們需要的確切資料,但這隻能解決部分問題。
無約束的查詢可能會從物件中提取特定欄位,破壞封裝,或者從聚合內部例項化一些特定物件,使聚合根結構不明顯,並使這些物件無法強制執行域模型的規則。領域邏輯轉移到查詢和應用程式層程式碼中,實體和值物件變成僅僅是資料容器(banq注:變成資料傳輸物件DTO)。
因此:
對於需要全域性訪問的每種型別的聚合,建立一個服務(倉儲服務),該服務可以提供該聚合根型別的所有物件的記憶體中集合的錯覺(好像是物件的倉庫)。通過眾所周知的全域性介面設定訪問許可權,提供新增和刪除物件的方法,這些方法將封裝資料儲存中的實際資料插入或刪除。提供基於對領域專家有意義的標準選擇物件的方法,返回完全例項化的物件或屬性值滿足條件的物件集合,從而封裝實際的儲存和查詢技術,或返回以惰性方式給出完全例項化聚合的假象的代理(例如JPA的惰性載入)。這些僅為實際需要直接訪問的聚合根提供儲存庫(banq注:聚合根類似一個物品,物品儲存在倉庫中,這是儲存庫的意思,儲存庫=儲存倉庫),保持應用程式邏輯專注於模型,
DDD倉儲Repository模式
工廠
當建立整個內部一致聚合或大值物件變得複雜或顯示太多內部結構時,工廠提供封裝。
建立物件本身可能是一項主要操作,但複雜的裝配操作不適合所建立物件的責任,將這些責任結合起來可能會產生難以理解的設計。如果使用客戶端直接構建會混淆了客戶端本身的設計,破壞了組裝物件或聚合的封裝,並且過度地將客戶端耦合到所建立物件的實現。(banq注:參考Gof工廠模式)
因此:
將建立複雜物件和聚合例項的責任轉移到單獨的工廠物件,該物件本身在領域模型中不承擔任何其他責任,但仍然是領域設計的一部分。提供一個封裝所有複雜程式集的介面,並且不需要客戶端引用要例項化的物件的具體類。建立整個聚合作為一個原子單位(banq注:類似ACID中的A原子性),強制執行其不變數。將一個複雜的值物件的建立也作為一個原子單位建立,然後使用Builder模式進行組裝。
(banq注:工廠和倉儲可見:JiveJdon案例,在Spring-data-jdbc或JPA中,由Spring-data框架提供工廠和框架:)