领域驱动设计理论基础
運用領域模型
消化知識
做好一塊業務的第一步是如何消化一個知識,消化一個知識其實就是建立有效模型,將知識透明化,變成可視化、可交流的文檔或者圖形。有效建模的要素包括以下幾點:
- 模型和現實的綁定。最初的原型可能比較簡陋,但它會在模型與現實之間建立早期鏈接,所有后續的迭代是維護完善該鏈接。
- 建立了一種基于模型的語言。隨著項目的進展,參與者都能夠使用模型中的術語,并將它們組織成符合模型結構的語句,而且無需翻譯即可理解互相要表達的意思。
- 開發一個蘊含豐富知識的模型。 對象具有行為和強制性規則。模型不僅僅是一種數據模式,它還是解決復雜問題不可或缺的部分。我們需要挖掘提取業務知識背后隱藏的概念。將復雜的知識進行分解,分解成單一變化的對象。
- 提煉模型。 在模型日趨完整的過程中,重要的概念不斷被添加到模型中,但同樣重要的是,不再使用的或不重要的概念則從模型中被移除。當一個不需要的概念與一個重要的概念關聯時,則把重要的概念提取到一個新模型中,其他那些不要的概念就可以丟棄。
- 頭腦風暴和實驗。語言和草圖,再加上頭腦風暴活動,將我們的討論變成“模型實驗室”,在這些討論中可以演示、嘗試和判斷上百種變化。當團隊走查場景時,口頭表達本身就可以作為所提議的模型的可行性測試。
在傳統的瀑布方法中,業務專家與分析員進行討論,分析員消化理解這些知識后,對其進行抽象并將結果結果傳遞給程序員,再由程序員編寫軟件代碼。由于這種方法沒有反饋,因此總是失敗。分析員全權負責創建模型,但他們創建的模型只是基于業務專家的意見。他們既沒有向程序員學習的機會,也得不到早期軟件版本的經驗。知識只是朝一個方向在流動,但是不會積累。
好的程序員會自然而然地抽象并開發出一個可以完成更多工作的模型。但是這個模型只是局部的,只能滿足特定時刻特定部分的需求。只有團隊所有成員一起消化理解模型,通過頻繁的交互才能形成全局有效一致的認知。領域模型的不斷精化迫使開發人員學習重要的業務原理,而不是機械地進行功能開發。領域專家被迫提煉自己已知的重要知識過程往往也是完善其自身理解的過程,而且他們會漸漸理解軟件項目所必須的概念嚴謹性。模型聚焦于需求分析,同時與編程和設計緊密交互。它通過良性循環加深團隊對領域的理解,使他們更透徹地理解模型,并對其進一步精化。
統一團隊的領域交流語言
要想創建一種靈活的、蘊含豐富知識的設計,需要一種通用的、共享的團隊語言,以及對語言的不斷的試驗。
如果語言支離破碎,項目必將遭遇嚴重問題。日常討論所使用的術語和代碼(軟件項目的最重要產品)中使用的術語不一致。甚至同一個人在講話和寫東西時使用的語言也不一致,這導致的后果是,對領域的深刻表述常常稍縱即逝,根本無法記錄到代碼或文檔中。
翻譯使得溝通不暢,并削弱了知識消化,帶來誤解的風險。口頭溝通會產生信息不可逆的丟失。
團隊統一的領域語言的詞匯應該包括但不限于類及主要操作的名稱。語言中的術語,有些可以是模型討論中已經明確的規則,有些可以是來自模型上的高級組織規則,團隊常常用于領域模型的設計模式也使領域語言更加豐富。
通過大量使用基于模型的語言,并且不達流暢絕不罷休,我們可以逐步得到一個完整的、易于理解的模型,它由簡單元素組成,并通過組合這些簡單元素表達復雜概念。
將模型作為語言的支柱。確保團隊在內部所有交流中以及代碼中堅持使用這種語言。在畫圖、寫東西,特別是講話時也要使用這種語言。
通過嘗試不同的表示方法(它們反映了備選模型)來消除難點。然后重構代碼,重新命名類、方法和模塊,以便與新模型保持一致。解決交談中的屬于混淆問題。領域專家應該抵制不合適或無法充分表達領域理解的術語或結構,開發人員應該密切關注那些將會妨礙設計的有歧義和不一致的地方。
討論系統時要結合模型。使用模型元素及其交互來大聲描述場景,并且按照模型允許的方式將各種概念結合到一起。找到更簡單的表達方式講出你要講的話,然后將這些新的想法應用到圖和代碼中。
綁定模型和實現
如果整個程序設計或者其核心部分沒有與領域模型相對應,那么這個模型就是沒有價值的,軟件的正確性也值得懷疑。同時,模型和設計功能之間過于復雜的對應關系也是難于理解的,在實際項目中,當設計改變時也無法維護這種關系。若分析與設計之間產生嚴重分歧,那么在分析和設計活動中所獲得的知識就無法彼此共享。
模型驅動設計(Model-Driven Design)不再將分析模型和程序設計分離開,而是尋求一種能夠滿足這兩方面需求的單一模型。不考慮純粹的技術問題,程序設計中的每個對象都反映了模型中所描述的響應概念。這就要求我們以更高的標準來選擇模型,因為它必須同時滿足兩種完全不同的目標。
軟件系統各個部分的設計應該忠實地反映領域模型,以便體現出這兩者之間的明確對應關系。我們應該反復檢查并修改模型,以便軟件可以更加自然地實現模型,即使想讓模型反映出更深層次的領域概念也該如此。我們需要的模型不但應該滿足這兩種需求,還應該能夠支持健壯的通用語言。
從模型中獲取用于程序設計和基本職責分配的術語。讓程序代碼成為模型的表達。代碼的改變可能會是模型的改變,而其影響勢必要波及接下來響應的需求項目。
面向對象編程之所以功能強大,是因為它基于建模范式,并且為模型構造提供了實現方式。從程序員的角度來看,對象真實存在于內存中,它們與其他對象互相聯系,它們被組織成類,并且通過消息傳遞來完成相應的行為。
模型驅動設計的核心點是編程思想的轉變,從過程設計轉變成面向對象設計。如果編寫代碼的人員認為自己沒必要對模型負責,或者不知道如何讓模型為應用程序服務,那么這個模型就和程序沒有任何關聯。如果開發人員沒有意識到改變代碼就意味著改變模型,那么他們對程序的重構不但不會增強模型的作用,反而還會削弱它的效果。同樣,如果建模人員不參與到程序實現的過程中,那么對程序實現的約束就沒有切身的感受,即使有,也會很快忘記。如果分工阻斷了設計人員與開發人員之間的協作,使他們無法轉達實現模型驅動設計(Model-Driven Design)的種種細節,那么經驗豐富的設計人員則不能將自己的知識和技術傳遞給開發人員。
任何參與建模的技術人員,不管在項目中的主要職責是什么,都必須花時間了解代碼。任何負責修改代碼的人員則必須學會用代碼來表達模型。每一個開發人員都必須不同程度地參與模型討論。參與不同工作的人都必須有意識地通過團隊統一語言與接觸代碼的人及時交換關于模型的想法。
分離領域
分層的價值在于每一層都只代表程序中的某一特定方面。這種限制使每個方面的設計都更具內聚性,更容易解釋。當然,要分離出內聚設計中最重要的方面,選擇恰當的分層方式是至關重要的。盡管Layered Architecture的種類繁多,但是大多數成功的架構使用的都是下面4個概念層的某種變體。
| 用戶界面層(或表示層) | 負責向用戶顯示信息和解釋用戶指令。這里指的用戶可以是另一個計算機系統,不一定是使用用戶界面的人 |
| 應用層 | 定義軟件要完成的任務,并且指揮表達領域概念的對象來解決問題。這一層所負責的工作對業務來說意義重大,也是與其他系統的應用層進行交互的必要渠道 |
| 領域層(或模型層) | 負責表達業務概念,業務狀態信息以及業務規則。盡管保存業務狀態的技術細節是由基礎設施層實現的,但是反映業務情況的狀態是由領域層控制并且使用的領域層是業務軟件的核心。 |
| 基礎設施層 | 為上面各層提供通用的技術能力;為應用層傳遞消息,為領域層提供持久化機制,為用戶界面層繪制屏幕組件等等。基礎設施層還能夠通過架構框架來支持4個層次間的交互模式。 |
如果與領域有關的代碼分散在大量的其他層(用戶界面層、應用層、基礎設施層等)之中,那么查看與分析領域代碼就會變得異常困難。對用戶界面的簡單修改實際上很可能會改變業務邏輯。而要想調整業務規則也可能需要對用戶界面代碼、數據庫操作代碼或者其他的程序元素進行仔細的篩查。這樣就不太可能實現一致的、模型驅動的對象了,同時也會給自動化測試帶來困難。
給復雜的應用程序劃分層次。在每一層內分別進行設計,使其具有內聚性并且只依賴于它的下層。采用標準的架構模式,只與上層進行松散的耦合。將所有與領域模型相關的代碼放在一個層中,并把它與用戶界面層、應用層以及基礎設施層的代碼分開。領域對象應該將重點放在如何表達領域模型上,而不需要考慮自己的顯示和存儲問題,也無需管理應用任務等內容。這使得模型的含義足夠豐富,結構足夠清晰,可以捕捉到基本的業務知識,并有效的使用這些知識。
各層之間是松散連接的,層與層的依賴關系只能是單向的。上層可以直接使用或操作下層元素,方法是通過調用下層元素的公共接口,保持對下層元素的引用,以及采用常規的交互手段。而如果下層元素需要與上層元素進行通信,則需要采用另一種通信機制,使用架構模式來連接上下層,如回調模式或觀察者模式等。
一個復雜的項目應該采用模型驅動設計(Model-Driven Design),不應該使用一些類似Smart UI的“反模式”,這類模式雖然效率高,可以順利地使用關系數據庫,能夠提供數據級的整合,但是違反了層次架構,在復雜業務場景中沒有對業務問題進行抽象,導致很多代碼無法復用,同時會形成概念模糊,無中心的現象。
模型表示的元素
用于表示模型的3種模型元素模式:Entity、Value Object和Service。一個對象是用來表示某種具有連續性和標識事物,還是用于描述某種狀態的屬性,這是Entity和Value Objecct之間的根本區別。明確的選擇這兩種模式中的一個來定義對象,有利于減少歧義,并幫助我們做出特定的選擇,這樣才能得到健壯的設計。
領域中還有一些方面適合用動作或操作來表示,這比用對象表示更加清楚。這些方面最好用Service來表示,而不應把操作的責任強加到Entity或Value Object上,盡管這樣做只是稍微違背了面向對象的建模傳統。Service是接受客戶端請求來完成某事。在軟件技術層中有很多Service(這些不全是領域層所說的Service)。領域層的也可以使用Service,當對軟件要做的某項無狀態的活動進行建模時,就可以將該活動作為一項Service。
關聯
現實生活中有大量“多對多”關聯,其中有很多關聯天生就是雙向的,我們在模型開發的早期進行頭腦風暴活動并探索領域時,也會得到很多這樣的關聯。但這些普遍的關聯會使實現和維護變得很復雜。
盡可能地對關系進行約束是非常重要的。我們可以規定一個方向去梳理關聯;通過添加一個限定符,以便有效的將“多對多”關聯限制為“一對多”關聯;從業務角度去消除不必要的關聯。
從仔細地簡化和約束模型的關聯到真正的模型驅動設計還有一段很漫長的探索過程。仔細區分對象可以使得模型更加清晰,并得到更實用的實現。
Entity
一些對象主要不是由它們的屬性定義的,它們實際上表示了一條“標識線”,這些主要由標識定義的對象被稱作Entity。Entity(實體)有特殊的建模和設計思路。它們具有生命周期,這期間它們的形式和內容可能發生根本改變,但必須保持一種內在的連續性。
標識有時只有在系統上下文中才重要。比如體育場預定程序可能將座位和觀眾當作Entity來處理,但如果活動采用入場券方式,那么就不需要對座位加以區分,這種情況下,只有座位總數才是重要的。
當一個對象由其標識(而不是屬性)區分時,那么在模型中應該主要通過標識來確定該對象的定義。使類定義變得簡單,并集中關注生命周期的連續性和標識。定義一種區分每個對象的方式,這種方式應該與形式和歷史無關。要格外注意那些需要通過屬性來匹配對象的需求。在定義標識操作時,要確保這種操作作為每個對象生成唯一的結果,這可以通過附加一個保證唯一性的符號來實現。這種定義標識的方法可能來自外部,也可能是由系統創建的任意標識符,但它在模型中必須是唯一的標識。模型必須定義出“符合什么條件才算是相同的事物”(可以理解為數據庫表的業務唯一健)。比如掛號場景下,用戶訂單就是一個Entity,而這個訂單所對應的科室疾病就不是一個Entity。
每個Entity都必須有一種建立標識的操作方式,以便與其他對象區分開。不管系統是如何定義的,都必須確保標識屬性在系統中是唯一的,即使是分布式系統中,或者對象已經被歸檔,也必須確保標識的唯一性。
Value Object
很多對象沒有概念上的標識,但它們描述了一個事物的某種特征。用于描述領域的某個方面而本身沒有概念標識的對象稱為Value Object。Value Object被實例化之后用來表示一些設計元素,對于這些設計元素,我們只關心它們是什么,而不關心它們是誰。Value Object是對Entity的補充。
跟蹤Entity的標識是非常重要的,但為其他對象也加上標識會影響系統性能并增加分析工作,而且會使模型變得混亂,因為所有對象看起來都是相同的。
軟件設計要時刻與復雜性做斗爭。我們必須區別對待問題,僅在真正需要的地方進行特殊處理。
當我們只關心一個模型元素的屬性時,應把它歸類為Value Object。我們應該使用這個模型元素能夠表示出其屬性的意義,并為它提供相關功能。Value Object應該是不可變的(不是絕對的不可變)。不要為它分配任何標識,而且不要把它設計成像Entity那么復雜。
Entity之間的雙向關聯很難維護,兩個Value Object之間的雙向關聯則完全沒有任何意義。
Service
有時候對象不是一個事物,在設計中會包含一些特殊的操作,這些操作從概念上講不屬于任何對象。與其把它們強制地歸于哪一個類,不如順其自然地在模型中引入一種新的元素,這就是Service(服務)。一些領域改變不適合被建模為對象。如果勉強把這些重要的領域功能歸為Entity或Value Object的職責,那么不是歪曲基于建模的對象定義,就是人為增加一些無意義的對象。比如賬戶之間的轉賬,用轉賬服務來描述比將轉賬行為分解到賬戶中更加自然。
使用Service時應謹慎,它們不應該替代Entity和Value Object的所有行為。但是,一個操作實際上是一個重要的領域概念時,Service很自然就會稱為Model-Driven Design中的一部分。將模型中的獨立操作聲明為一個Service,而不是聲明為一個不代表任何事情的虛擬對象,可以避免對任何人產生的誤導。好的Service應該有以下3個特征:
- 與領域概念相關的不是Entity或Value Object 的一個自然組成部分。
- 接口是根據領域模型的其他元素定義的。
- 操作是無狀態,這里說得無狀態是指任何客戶都可以使用某個Service的任何實例,而不必關心該實例的歷史狀態。
當領域中的某個重要的過程或轉換操作不是Entity或Value Object的自然職責時,應該在模型中添加一個作為獨立接口的操作,并將其聲明為Service。同時應該使Service成為無狀態的。這里說的Service是那些在領域中具有重要意義的Service,但Service這個概念并不只是在領域層中使用。我們需要注意區分屬于領域層的Service和那些屬于其他層的Service(比如發送短信的service等),并劃分責任,以便將它們明確區分開。
Service可以控制領域層中的接口的粒度,并且避免客戶端與Entity和Value Object的耦合。在大型系統中,中等粒度的、無狀態的Service更容易被復用,因為它們在簡單的接口背后封裝了重要的功能。此外,細粒度的對象可能導致分布式系統的消息傳遞的效率低下。
Module(也成為Package)
Module為人們提供了兩種觀察模型的方式,一是可以在Module中查看細節,而不會被整個模型淹沒;二是觀察Module之間的關系,而不考慮其內部細節。領域層中的Module應該成為模型中有意義的部分,Module從更大的角度描述了領域。
眾所周知,Module之間應該是低耦合的,而在Module的內部則是高內聚的。Module不僅僅是代碼的劃分,也是概念的劃分。一個人一次考慮的事情是有限的(因此菜肴低耦合)。不連貫的思想和“一鍋粥”似的思想同樣難于理解(因此才要高內聚)。
在一個好的模型中,元素之間是要協同工作的,而仔細選擇的Module可以將那些具有緊密概念關系的模型元素集中到一起。將這些具有相同職責的對象元素聚合在一起,可以把建模和設計集中到單一的Module中,這極大地降級建模和設計的復雜性,使人們可以從容應對這些工作。
選擇能夠描述系統的Module,并使之包含一個內聚的概念集合。這通常會實現Module之間的低耦合,但如果效果不理想,則應尋找一種更改模型的方式來消除概念之間的耦合,或者找到一個新的Module概念(可能之前被忽略了),基于這個概念組織的Module可以以一種有意義的方式將元素集中到一起。對模型進行精化,直到可以根據高層領域概念對模型進行劃分,同時對應的代碼也不會產生耦合。Module及其名稱應該能反映出領域模型深層知識。
領域模型中的每個概念都應該在實現元素中反映出來。Entity、Value Object、它們之間的關聯、領域Service以及用于組織元素的Module都是實現與模型直接對應的地方。實現中的對象、指針和檢索機制必須直接、清楚地映射到模型元素。如果沒有做到這一點,就要重寫代碼,或者回頭修改模型,或者同時修改代碼和模型。
建模范式
目前主流的范式是面向對象設計,而且現在的大部分復雜項目都開始使用對象。對象建模在簡單性和復雜性之間實現了一個很好的平衡。
雖然對象建模的概念很簡單,但它的豐富功能足以捕獲重要的領域知識。而且它從一開始就獲得了開發工具的支持,使得模型可以在軟件中表達出來。大多數開發人員、項目經理和從事項目工作的其他專家都已經很好的接受了面向對象設計。
領域建模不一定是對象模型。當領域中只有個別元素適合用其他范式時,開發人員可以接受一些蹩腳的對象,以使整個模型保持一致。可以將業務規則引擎或者工作流引擎等類似的非對象組件集成到對象系統中,以使開發人員能夠用最適當的風格對待特殊概念進行建模。
雖然Model-Driven Design 不一定是面向對象的,但它確實需要一種富有表達力的模型結構實現,無論是對象、規則還是工作流,都是如此。當將非對象元素混合到以面向對象為主的系統中時,需要遵循以下4條經驗規則:
- 不要和實現范式對抗。我們總是可以用別的方式來考慮領域,從而找到適合于范式的模型概念。
- 把通用語言作為依靠的基礎。即使工具之間沒有嚴格聯系時,語言使用上的高度一致性也能防止各個設計部分分裂。
- 不要一味依賴UML。有時固定使用某種工具(如UML繪圖工具)將導致人們通過歪曲模型來得到UML圖。有時使用其他風格的圖形或簡單的語言描述比牽強附會地視圖更好。
- 保持懷疑態度。工具是否真正有用武之地?不能因為存在一些規則,就必須使用規則引擎。規則也可以表示為對象,雖然可能不是特別優雅。
領域對象的生命周期
領域對象的生命周期管理這些對象時面臨諸多挑戰,稍有不慎就會偏離Model-Driven Design的軌道。主要的挑戰有以下兩類:
- 在整個生命周期中維護完整性和事務。
- 防止模型陷入管理生命周期復雜性造成的困境當中。
通過3種模式解決這些問題。首先是Aggregate(聚合),它通過定義清晰的所屬關系和邊界,并避免錯綜復雜的對象關系網來實現模型的內聚。聚合模式對于維護生命周期各個階段的完整性具有至關重要的作用。接著是在生命周期的開始階段,使用Factory(工廠)來創建和重建復雜對象,從而封裝它們的內部結構。最后,在生命周期的中間和末尾使用Repository(存儲庫)來提供查找和檢索持久化對象并封裝龐大基礎設施的手段。盡管Repository和Factory本身并不是來源于領域,但它們在領域設計中扮演者重要的角色。這些結構提供了易于掌握的模型對象處理方式,使Model-Driven Desogn更完備。
Aggregate
典型對象模型中的關系網使我們難以斷定一個修改會產生哪些潛在的影響。僅僅因為存在依賴就更新系統中的每個對象,這樣做是不現實的。在多個客戶對相同對象進行并發訪問的系統中,這個問題更加突出。當很對用戶對系統中的對象進行查詢和更新時,必須防止他們同時修改互相依賴的對象,范圍錯誤將導致嚴重的后果。
在具有復雜關系的模型中,要保證對象更改的一致性是很難的。不僅互不關聯的對象需要遵守一定固定規則,而且緊密關聯的各組對象也要遵守一些固定規則。然而,過于謹慎的鎖定機制又會導致多個用戶之間毫無意義地互相干擾,從而使系統不可用。
實際上,要想找到一種兼顧各種問題的解決方案,要求對領域有深刻的理解,要了解特定類實例之間的更改頻率這樣的深層次等因素。我們需要找到一個使對象間沖突較少而固定規則聯系更緊密的模型。
盡管從表面上看這個問題是數據庫事務方面的一個技術難題,但它的根源在模型,歸根結底是由于模型中缺乏明確定義的邊界。從模型得到的解決方案將使模型更易于理解,并且使設計更易于溝通。當模型被修改時,它將引導我們對實現作出修改。
我們需要用一個抽象來封裝模型中的引用。Aggregate就是一組相關對象的集合,我們把它作為數據修改的單元。每個Aggregate都有一個根(root)和一個邊界(boundary)。根是Aggregate所包含的一個特定Entity。對Aggregate而言,外部對象只可以引用根,而邊界內部的對象之間則可以互相引用。
為了實現這個概念上的Aggregate,需要對所有事務應用一組規則。
- 根Entity具有全局標識,它最終負責檢查固定規則。
- 根Entity具有全局標識。邊界內的Entity具有本地標識,這些標識只在Aggregate內部才是唯一的。
- Aggregate外部的對象不能引用除Entity之外的任何內部對象。根Entity可以把內部Entity的引用傳給它們,但這些對象只能臨時使用這些引用,而不能保持引用。根可以把一個Value Object的副本傳給另一個對象,而不必關系它發生什么變化,因為它只是一個Value,不再與Aggregate有任何關聯。
- 作為上一條規則的推論,只有Aggregate的根才能直接通過數據庫查詢獲取。所有其他對象必須通過關聯來發現。
- Aggregate內部的對象可以保持對其他Aggregate根的引用。
- 刪除操作必須一次刪除Aggregate邊界之內的所有對象。
- 當提交對Aggregate邊界內部的任何對象的修改時,整個Aggregate的所有固定規則都必須被滿足。
我們應該將Entity和Value Object分門別類地聚集到Aggregate中,并定義每一個Aggregate的邊界。在每個Aggregate中,選擇一個Entity作為根,并通過根來控制對邊界內其他對象的所有訪問。由于根控制訪問,因此不能繞過它來修改內部對象。這種設計有利于確保Aggregate中的對象滿足所有固定規則,也可以確保在任何狀態變化時Aggregate作為一個整體滿足固定規則。
Factory
當創建一個對象或創建整個Aggregate時,如果創建工作很復雜,或者暴露了過多的內部結構,則可以使用Factory進行封裝。
對象的功能主要體現在其復雜的內部配置以及關聯方面。我們應該一直對對象進行提煉,直到所有與其意義或在交互中的角色無關的內容被完全剔除為止。一個對象在它的生命周期里面要承擔大量的職責。如果再讓復雜對象負責自身的創建,那么職責過載將會導致很多問題。
復雜的對象創建是領域層的職責,然而這項任務并不屬于那些用于表示模型的對象。當客戶負責創建復雜對象時,它會牽涉不必要的復雜性,并將其職責搞的模糊不清。這違背了領域對象及所創建的Aggregate的封裝要求。更嚴重的是,如果客戶是應用的一部分,那么職責就會從領域層泄漏到應用層中。應用層與實現細節之間的這種耦合使得領域層抽象大部分優勢蕩然無存,而且導致后續更改的代價變得更加高昂。
對象的創建本身可以是一個主要操作,但被創建的對象并不適合承擔復雜的裝配操作。將這些職責混在一起可能產生難以理解的拙劣設計。讓客戶直接創建對象又會使客戶的設計陷入混亂,并且破壞被裝配對象或Aggregate的封裝,而且導致客戶與被創建對象的實現之間產生過于緊密的耦合。
每種面向對象的語言都提供了一種創建對象的機制(例如C++和java中的構造函數,Smalltalk中創建實例的類方法),但我們仍然需要一種更加抽象且不與其他對象發生耦合的構造機制。Factory就是一種負責創建其他對象的構造機制,封裝了創建復雜對象或Aggregate所需的知識。Factory提供了反映客戶目標的接口,以及被創建對象的抽象視圖,從而使客戶無需知道對象的工作機理就可以使用對象的功能。
Factory有很多設計方式,包括但不限于工廠方法模式、抽象工廠模式和構造器模式。任何好的工廠都需要滿足以下兩個基本需求:
- 每個創建方法都是原子的,而且要保證被創建對象或Aggregate的所有固定規則。Factory生成的對象要處于一致的狀態。在生成Entity時,意味著創建滿足所有固定規則的整個Aggregate,但在創建完成后可以向Aggregate添加可選元素。
- Factory應該被抽象為所需的類型,而不是所要創建的具體類。
在向Aggregate添加元素時可以通過在Aggregate的根上創建一個Factory Method,從而把Aggregate的內部實現細節隱藏起來,使任何外部客戶看不到這些細節,同時使根負責確保Aggregate在添加元素的完整性。
當創建一個對象時,這個對象與另一個對象的生成密切相關,但它并不擁有所生成的對象。也就是當一個對象的創建主要使用另一個對象的數據(或許還有規則)時,則可以在后者的對象上創建一個Factory Method,這樣就不必將后者的信息提取到其他地方來創建前者。這樣做還有利于表達前者和后者之間的關系。
Factory與被構造對象之間是緊密耦合的,因此Factory應該只被關聯到與被構造對象有著密切聯系的對象上。當有些細節需要隱藏而又找不到合適的地方來隱藏它們時,必須創建一個專用的Factory對象或Service。整個Aggregate通常由一個獨立的Factory來創建,Factory負責把對根的引用傳遞出去,并確保創建出的Aggregate滿足固定規則。如果Aggregate內部的某個對象需要一個Factory,而這個Factory又不適合在Aggregate根上創建,那么應該構建一個獨立的Factory。
Factory的引入提供了巨大的優勢,而這種優勢往往并未得到充分利用。但是,在有些情況下直接使用構造函數確實是最佳選擇。Factory實際上會使那些不具有多態性的簡單對象復雜化。以下情況下最好使用簡單的、公共的構造函數。
- 類是一種類型。它不是任何相關層次結構的一部分,而且也沒有通過接口實現多態性。
- 客戶關心的是實現,可能是將其作為選擇Strategy的一種方式。比如java中的集合類。
- 客戶可以訪問對象的所有屬性,因此向客戶公開的構造函數中沒有嵌套的對象創建。
- 構造并不復雜。
- 公共構造函數必須遵守與Factory相同的規則:它必須是原子操作,而且要滿足被創建對象的所有固定規則。
不要在構造函數中調用其他類的構造函數。構造函數應該保持絕對簡單。復雜的裝配,特別是Aggregate,需要使用Factory。
無論是獨立的Factory還是Factory Method,都要記住以下兩點:
- 每個操作都必須是原子的。我們必須在與Factory的一次交互中把創建對象所需的所有信息傳遞給Factory。同時必須確定當創建失敗時將執行什么操作。可以考慮采用編碼標準來處理所有Factory的失敗。
- Factory將與其參數發生耦合。如果在選擇輸入參數時不小心,可能會產生錯綜復雜的依賴關系。耦合程度取決于對參數的處理。如果只是簡單地將參數插入到要構建的對象中,則依賴程度適中;如果從參數選出一部分在構造對象時使用,耦合將更緊密。
在某些情況下,把固定規則的相關邏輯放到Factory中是有好處的,這樣可以讓被創建對象的職責更明晰。對于Aggregate規則來說尤其如此(這些規則會約束很多對象)。固定規則的相關邏輯應該集中在Entity的構造Factory中,特別不適合放到那些與其他領域對象關聯的Factory Method中。
檢索操作需要一個復雜的操作將各個部分重新裝配成一個可用的對象。用于重建對象的Factory與用于創建對象的Factory很類似,主要有以下兩點不同。
- 用于重建對象的Entity Factory不分配新的跟蹤ID。如果分配新ID,將丟失與先前對象的連續性。因此,在重建對象的Factory中,標識屬性必須是輸入參數的一部分。
- 當固定規則未被滿足時,重建對象的Factory采用不同的方式進行處理。當創建新對象時,如果未滿足固定規則,Factory應該簡單的拒絕創建對象,但在重建對象時則需要更靈活的響應。如果對象已經在系統某個地方存在(如數據庫中),那么不能忽略這個事實。
Repository
Factory封裝了對象創建和重建時的生命周期轉換。還有一種轉換大大增加了領域設計的技術復雜性,就是對象與存儲之間的互相轉換。這種轉換由另一種領域設計構造來處理,它就是Repository。
我們可以通過對象之間的關聯找到對象。但當它處于生命周期的中間時,必須要有一個起點,以便從這個起點遍歷到一個Entity或Value。
領域驅動設計的目標是通過關注領域模型(而不是技術)來創建更好的軟件。假設開發人員構造了一個SQL查詢,并將它傳遞給基礎設施層中的某個查詢服務,然后再根據得到的表行數據的結果集提取出所需信息,最后將這些信息傳遞給構造函數或者Factory。開發人員執行這一連串操作的時候,早已不再把模型當作重點了。當客戶代碼直接使用數據庫時,開發人員會試圖繞過模型的功能(如Aggregate,甚至是對象封裝),而直接獲取和操作他們所需的數據。這將導致越來越多的領域規則被嵌入查詢代碼中,或干脆丟失了。如果基礎設施提供了這方面的便利,那么開發人員可能會增加很多遍歷的關聯,這會使模型變得非常混亂。另一方面,開發人員可能使用查詢從數據庫中提取他們所需的數據,或是直接提取具體的對象,而不是通過Aggregate的根來得到這些對象。這樣就導致領域邏輯進入查詢和客戶代碼中,而Entity和Value Object則變成單純的數據容器。采用大多數處理數據庫訪問的技術復雜性很快就會使客戶代碼變得混亂,這將導致開發人員簡化領域層,最終使模型變得無關緊要。
除了通過根來遍歷查找對象這種方法以外,禁止用其他方法對Aggregate內部的任何對象進行訪問。持久化的Value Object一般可以通過遍歷某個Entity來找到,在這里Entity就是把對象封裝在一起的Aggregate的根。
在所有持久化對象中,有一小部分(通常是Entity,有時是具有復雜內部結構的Value Object)必須通過基于對象屬性的搜索來全局訪問。當很難通過遍歷方式來訪問某些Aggregate根的時候,就需要使用這種訪問方式。 其他對象不宜使用這種訪問方式,因為這會混淆它們之間的重要區別。隨意的數據庫查詢會破壞領域對象的封裝和Aggregate。技術基礎設施和數據庫訪問機制的暴露會增加客戶的復雜度,并妨礙模型驅動的設計。
Repository將某種類型的所有對象表示為一個概念集合(通常是模擬的)。它的行為類似于集合,只是具有更復雜的查詢功能。在添加或刪除相應類型的對象時,Repository的后臺機制負責將對象添加到數據庫中,或從數據庫中刪除對象。這個定義將一組緊密相關的職責集中在一起,這些職責提供了對Aggregate根的整個生命周期的全程訪問。
客戶使用查詢方法向Repository請求對象,這些查詢方法根據客戶所指定的條件(通常是特定屬性的值)來挑選對象。Repository檢索被請求的對象,并封裝數據庫查詢和元數據映射機制。Repository可以根據客戶所要求的各種條件來挑選對象。它們也可以返回匯總信息,如有多少個實例滿足查詢條件。
Repository解除了客戶的巨大負擔,使客戶只需與一個簡單的、易于理解的接口進行對話,并根據模型向這個接口提出它的請求。要實現所有這些功能需要大量復雜的技術基礎設施,但接口很簡單,而且在概念層次上與領域模型緊密聯系在一起。只為那些確實需要直接訪問的Aggregate根提供Repository,讓客戶始終聚焦于模型,而將所有對象的存儲和訪問操作封裝起來,在Repository里來完成。Respository有很多優點,包括:
- 它們為客戶提供了一個簡單的模型,可用來獲取持久化對象并管理它們的生命周期。
- 它們使應用程序和領域設計與持久化技術(多種數據庫策略甚至是多個數據源)解構。
- 它們體現了有關對象訪問的設計決策
- 可以很容易將它們替換為“啞實現”,以便在測試中使用(通常使用內存中的集合)
在一些需要執行大量查詢的項目上,可以構建一個支持更靈活查詢的Repository框架。這要求開發人員熟悉必要的技術,而且一個支持性的基礎設施會提供巨大的幫助。即使一個Repository的設計采取了靈活的查詢方式,也應該允許添加專門的硬編碼查詢。不支持這些特殊查詢方式的框架有可能會扭曲領域設計,或干脆被開發人員棄之不用。客戶代碼可以忽略Repository的實現,但開發人員不能忽略,必須知道在封裝背后都發生了什么事情。
Repository概念在很多情況下都適用。可能的實現方法有很多,這里只能列出一些需要謹記的注意事項。
- 對類型進行抽象。Repository“含有”特定類型的所有實例,但這并不意味著每個類都需要一個Repository。類型可以是一個層次結構中的抽象超類。類型可以是一個接口,也可以是一個具體的類。由于數據庫缺乏這樣的多態性質,因此我們將面臨很多約束。
- 充分利用與客戶解耦的優點。我們可以很容易地更改Repository的實現,但如果客戶直接調用底層機制,我們就很難修改其實現。也可以利用解耦來優化性能,因為這樣就可以使用不同的查詢技術,或在內存中緩存對象。
當數據庫被視作對象存儲時,數據模型與對象模型的差別不應太大。可以犧牲一些對象關系的豐富性,以保證它與關系模型的緊密關聯。簡單的對應關系才是最好的。表中的一行應該包含一個對象,也可能包含Aggregate中的一些附屬項。表中的外鍵應該轉換為對另一個Entity對象的引用。有時我們不得不違背這種簡單的對應關系,但不應該由此就全盤放棄簡單映射的原則。
總結
以上是生活随笔為你收集整理的领域驱动设计理论基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Parsing Netflow usin
- 下一篇: CentOS 7设置KVM硬盘模式为SC