DDD读书笔记
DDD筆記
- 總體導覽圖
- 分層模式
- 軟件中的模型
- 關聯
- Entity
- Value Object
- Service
- Module
- 領域對象的生命周期
- Aggregate
- FACTORY
- 選擇FACTORY及其應用位置
- 有些情況下只需使用構造函數
- 接口的設計
- 固定規則的相關邏輯應放置在哪里
- ENTITY FACTORY與VALUE OBJECT FACTORY
- 重建已存儲的對象
- REPOSITORY
- REPOSITORY與FACTORY的關系
總體導覽圖
分層模式
| 用戶界面層(或表示層) | 負責向用戶顯示信息和解釋用戶指令。這里指的用戶可以是另一個計算機系統, 不一定是使用用戶界面的人 |
| 應用層 | 定義軟件要完成的任務,并且指揮表達領域概念的對象來解決問題。這一層所負 責的工作對業務來說意義重大,也是與其他系統的應用層進行交互的必要渠道 。應用層要盡量簡單,不包含業務規則或者知識,而只為下一層中的領域對象協調 任務,分配工作,使它們互相協作。它沒有反映業務情況的狀態,但是卻可以具有 另外一種狀態,為用戶或程序顯示某個任務的進度 |
| 領域層(或模型層) | 負責表達業務概念,業務狀態信息以及業務規則。盡管保存業務狀態的技術細節 是由基礎設施層實現的,但是反映業務情況的狀態是由本層控制并且使用的。領域 層是業務軟件的核心 |
| 基礎設施層 | 為上面各層提供通用的技術能力:為應用層傳遞消息,為領域層提供持久化機制, 為用戶界面層繪制屏幕組件,等等。基礎設施層還能夠通過架構框架來支持4個層次 間的交互模式 |
軟件中的模型
關聯
對象之間的關聯使得建模與實現之間的交互更為復雜。
模型中每個可遍歷的關聯,軟件中都要有同樣屬性的機制。
至少有3種方法可以使得關聯更易于控制。
(1) 規定一個遍歷方向。
(2) 添加一個限定符,以便有效地減少多重關聯。
(3) 消除不必要的關聯。
例子:
像很多國家一樣,美國有過很多位總統。這是一種雙向的、一對多的關系。然而,在提到?喬 治〃華盛頓?這個名字時,我們很少會問?他是哪個國家的總統??。從實用的角度講,我們可 83 以將這種關系簡化為從國家到總統的單向關聯。如圖5-1所示。這種精化實際上反映了對領域的 深入理解,而且也是一個更實用的設計。它表明一個方向的關聯比另一個方向的關聯更有意義且
更重要。也使得Person類不受非基本概念President的束縛。
通常,通過更深入的理解可以得到一個?限定的?關系。進一步研究總統的例子就可以知道, 一個國家在一段時期內只能有一位總統(內戰期間或許有例外)。這個限定條件把多重關系簡化 為一對一關系,并且在模型中植入了一條明確的規則。如圖5-2所示。1790年誰是美國總統?喬 治〃華盛頓。
Entity
定義 : 很多對象不是通過它們的屬性定義的,而是通過連續性和標識定義的。
一些對象主要不是由它們的屬性定義的。它們實際上表示了一條“標識線”(A Thread of Identity),這條線跨越時間,而且常常經歷多種不同的表示。有時,這樣的對象必須與另一個具 有不同屬性的對象相匹配。而有時一個對象必須與具有相同屬性的另一個對象區分開。錯誤的標 識可能會破壞數據。
當一個對象由其標識(而不是屬性)區分時,那么在模型中應該主要通過標識來確定該對象 的定義。使類定義變得簡單,并集中關注生命周期的連續性和標識。定義一種區分每個對象的方 式,這種方式應該與其形式和歷史無關。要格外注意那些需要通過屬性來匹配對象的需求。在定 義標識操作時,要確保這種操作為每個對象生成唯一的結果,這可以通過附加一個保證唯一性的 符號來實現。這種定義標識的方法可能來自外部,也可能是由系統創建的任意標識符,但它在模 型中必須是唯一的標識。模型必須定義出“符合什么條件才算是相同的事物”。
比較
體育場座位預訂程序可能會將座位和觀眾當作ENTITY來處理。在分配座位時,每張票都有一 個座位號,座位是ENTITY。其標識符就是座位號,它在體育場中是唯一的。座位可能還有很多其 他屬性,如位臵、視野是否開闊、價格等,但只有座位號(或者說某一排的一個位臵)才用于識 別和區分座位。
另一方面,如果活動采用入場卷的方式,那么觀眾可以尋找任意的空座位來坐,這樣就不需 要對座位加以區分。在這種情況下,只有座位總數才是重要的。盡管座位上仍然印有座位號,但 軟件已經不需要跟蹤它們。事實上,這時如果模型仍然將座位號與門票關聯起來,那么它就是錯 誤的,因為采用入場卷的活動并沒有這樣的約束。在這種情況下,座位不是ENTITY,因此不需要 標識符。
需要關注設計標識操作
Value Object
定義: 很多對象沒有概念上的標識,它們描述了一個事務的某種特征。
跟蹤ENTITY的標識是非常重要的,但為其他對象也加上標識會影響系統性能并增加分析工 作,而且會使模型變得混亂,因為所有對象看起來都是相同的。
軟件設計要時刻與復雜性做斗爭。我們必須區別對待問題,僅在真正需要的地方進行特殊 處理。
然而,如果僅僅把這類對象當作沒有標識的對象,那么就忽略了它們的工具價值或術語價 值。事實上,這些對象有其自己的特征,對模型也有著自己的重要意義。這些是用來描述事物 的對象。
用于描述領域的某個方面而本身沒有概念標識的對象稱為VALUE OBJECT(值對象)。VALUE OBJECT被實例化之后用來表示一些設計元素,對于這些設計元素,我們只關心它們是什么,而不 關心它們是誰。
當我們只關心一個模型元素的屬性時,應把它歸類為VALUE OBJECT。我們應該使這個模型 元素能夠表示出其屬性的意義,并為它提供相關功能。VALUE OBJECT應該是不可變的。不要為 它分配任何標識,而且不要把它設計成像ENTITY那么復雜。
關于關聯:
如果說ENTITY之間的雙向關聯很難維護,那么兩個VALUE OBJECT之間的雙向關聯則完 全沒有意義。當一個VALUE OBJECT指向另一個VALUE OBJECT時,由于沒有標識,說一個對象指向的 3 對象正是那個指向它的對象并沒有任何意義的。我們充其量只能說,一個對象指向的對象與那個指 向它的對象是等同的,但這可能要求我們必須在某個地方實施這個固定規則。
Value Object 可以通過多種方式優化,比如 共享 或者 復制, 一般 Value Object 設計為不可變的
Service
定義: 有時,對象不是一個事物。
在某些情況下,最清楚、最實用的設計會包含一些特殊的操作,這些操作從概念上講不屬于 任何對象。與其把它們強制地歸于哪一類,不如順其自然地在模型中引入一種新的元素,這就是 SERVICE(服務)。
-
一些領域概念不適合被建模為對象。如果勉強把這些重要的領域功能歸為ENTITY或VALUE OBJECT的職責,那么不是歪曲了基于模型的對象的定義,就是人為地增加了一些無意義的對象
-
好的SERVICE有以下3個特征。
(1) 與領域概念相關的操作不是ENTITY或VALUE OBJECT的一個自然組成部分。
(2) 接口是根據領域模型的其他元素定義的。
(3) 操作是無狀態的。 -
當領域中的某個重要的過程或轉換操作不是ENTITY或VALUE OBJECT的自然職責時,應該 在模型中添加一個作為獨立接口的操作,并將其聲明為SERVICE。定義接口時要使用模型語言, 并確保操作名稱是UBIQUITOUS LANGUAGE中的術語。此外,應該使SERVICE成為無狀態的。
很多領域或應用層SERVICE是在ENTITY和VALUE OBJECT的基礎上建立起來的,它們的行為類 似于將領域的一些潛在功能組織起來以執行某種任務的腳本。ENTITY和VALUE OBJECT往往由于粒度過細而無法提供對領域層功能的便捷訪問。我們在這里會遇到領域層與應用層之間很微妙的分 界線。例如,如果銀行應用程序可以把我們的交易進行轉換并導出到一個電子表格文件中,以便 進行分析,那么這個導出操作就是應用層SERVICE。?文件格式?在銀行領域中是沒有意義的,它 也不涉及業務規則。
另一方面,賬戶之間的轉賬功能屬于領域層SERVICE,因為它包含重要的業務規則(如處理 相應的借方賬戶和貸方賬戶),而且?資金轉賬?是一個有意義的銀行術語。在這種情況下,SERVICE 自己并不會做太多的事情,而只是要求兩個Account對象完成大部分工作。但如果將轉賬操作強加在Account對象上會很別扭,因為這個操作涉及兩個賬戶和一些全局規則。
粒度 :
由于應用層負責對領域對象的行為進行協調,因此細粒度的領域對象可能會把領 域層的知識泄漏到應用層中。這產生的結果是應用層不得不處理復雜的、細致的交互,從而使得 領域知識蔓延到應用層或用戶界面代碼當中,而領域層會丟失這些知識。明智地引入領域層服務 有助于在應用層和領域層之間保持一條明確的界限。
Module
MODULE為人們提供了兩種觀察模型的方式,一是可以在MODULE中查看 細節,而不會被整個模型淹沒,二是觀察MODULE之間的關系,而不考慮其內部細節
MODULE之間應該是低耦合的,而在MODULE的內部則是高內聚的。耦合和內聚的 解釋使得MODULE聽上去像是一種技術指標,仿佛是根據關聯和交互的分布情況來機械地判斷它 們。然而,MODULE并不僅僅是代碼的劃分,而且也是概念的劃分。一個人一次考慮的事情是有限 的(因此才要低耦合)。不連貫的思想和“一鍋粥”似的思想同樣難于理解(因此才要高內聚)。
領域對象的生命周期
領域對象生命周期主要挑戰:
(1) 在整個生命周期中維護完整性。
(2) 防止模型陷入管理生命周期復雜性造成的困境當中。
AGGREGATE(聚合),它通過定義清晰的所屬關系和邊界,并避免混亂、錯綜復雜的對象關系網來實現模型的內聚。聚合模式對于維護生命周期各 個階段的完整性具有至關重要的作用。
FACTORY(工廠)來創建和重建復 雜對象和AGGREGATE(聚合),從而封裝它們的內部結構。最后,在生命周期的中間和末尾使用 REPOSITORY(存儲庫)來提供查找和檢索持久化對象并封裝龐大基礎設施的手段。
使用AGGREGATE進行建模,并且在設計中結合使用FACTORY和REPOSITORY,這樣我們就能夠 在模型對象的整個生命周期中,以有意義的單元、系統地操縱它們。AGGREGATE可以劃分出一個 范圍,這個范圍內的模型元素在生命周期各個階段都應該維護其固定規則。FACTORY和 REPOSITORY在AGGREGATE基礎上進行操作,將特定生命周期轉換的復雜性封裝起來。
Aggregate
減少設計中的關聯有助于簡化對象之間的遍歷,并在某種程度上限制關系的急劇增多。但大 多數業務領域中的對象都具有十分復雜的聯系,以至于最終會形成很長、很深的對象引用路徑, 我們不得不在這個路徑上追蹤對象。在某種程度上,這種混亂狀態反映了現實世界,因為現實世 界中就很少有清晰的邊界。但這卻是軟件設計中的一個重要問題。
定義:
AGGREGATE就是一組相關對象的集合,我們把它作為數據修改的單元。每個AGGREGATE都有一個根(root)和一個邊界(boundary)。邊界 定義了AGGREGATE的內部都有什么。根則是AGGREGATE所包含的一個特定ENTITY。對AGGREGATE 而言,外部對象只可以引用根,而邊界內部的對象之間則可以互相引用。除根以外的其他ENTITY 都有本地標識,但這些標識只在AGGREGATE內部才需要加以區別,因為外部對象除了根ENTITY之 外看不到其他對象。
例子:
汽車修配廠的軟件可能會使用汽車模型。汽車是一個具有全局標識的ENTITY: 我們需要將這部汽車與世界上所有其他汽車區分開(即使是一些非常相似的汽車)。我們可以使 用車輛識別號來進行區分,車輛識別號是為每輛新汽車分配的唯一標識符。我們可能想通過4個 輪子的位臵跟蹤輪胎的轉動歷史。我們可能想知道每個輪胎的里程數和磨損度。要想知道哪個輪 胎在哪兒,必須將輪胎標識為ENTITY。當脫離這輛車的上下文后,我們很可能就不再關心這些輪胎的標識了。如果更換了輪胎并將舊輪胎送到回收廠,那么軟件將不再需要跟蹤它們,它們會成 為一堆廢舊輪胎中的一部分。沒有人會關心它們的轉動歷史。更重要的是,即使輪胎被安在汽車 上,也不會有人通過系統查詢特定的輪胎,然后看看這個輪胎在哪輛汽車上。人們只會在數據庫 中查找汽車,然后臨時查看一下這部汽車的輪胎情況。因此,汽車是AGGREGATE的根ENTITY,而 輪胎處于這個AGGREGATE的邊界之內。另一方面,發動機組上面都刻有序列號,而且有時是獨立 于汽車被跟蹤的。在一些應用程序中,發動機可以是自己的AGGREGATE的根。
固定規則:
固定規則(invariant)是指在數據變化時必須保持的一致性規則,其涉及AGGREGATE成員之 間的內部關系。而任何跨越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作為一個 整體滿足固定規則。
有一個能夠聲明AGGREGATE的技術框架是很有幫助的,這樣就可以自動實施鎖機制和其他一 些功能。如果沒有這樣的技術框架,團隊就必須靠自我約束來使用事先商定的AGGREGATE,并按 照這些AGGREGATE來編寫代碼。
FACTORY
當創建一個對象或創建整個AGGREGATE時,如果創建工作很復雜,或者暴露了過多的內部結 構,則可以使用FACTORY進行封裝。
對象的功能主要體現在其復雜的內部配臵以及關聯方面。我們應該一直對對象進行提煉,直 到所有與其意義或在交互中的角色無關的內容被完全剔除為止。一個對象在它的生命周期中要承 擔大量職責。如果再讓復雜對象負責自身的創建,那么職責過載將會導致問題。
比如:
汽車發動機是一種復雜的機械裝臵,它由數十個零件共同協作來履行發動機的職責——使軸 轉動。我們可以試著設計一種發動機組,讓它自己抓取一組活塞并塞到汽缸中,火花塞也可以自 己找到插孔并把自己擰進去。但這樣組裝的復雜機器可能沒有我們常見的發動機那樣可靠或高 效。相反,我們用其他東西來裝配發動機。或許是機械師,或者是工業機器人。無論是機器人還 是人,實際上都比二者要裝配的發動機復雜。裝配零件的工作與使軸旋轉的工作完全無關。只是 在生產汽車時才需要裝配工,我們駕駛時并不需要機器人或機械師。由于汽車的裝配和駕駛永遠不會同時發生,因此將這兩種功能合并到同一個機制中是毫無價值的。同理,裝配復雜的復合對 象的工作也最好與對象要執行的工作分開。
但將職責轉交給另一個相關方——應用程序中的客戶(client)對象——會產生更嚴重的問 題。客戶知道需要完成什么工作,并依靠領域對象來執行必要的計算。如果指望客戶來裝配它需 要的領域對象,那么它必須要了解一些對象的內部結構。為了確保所有應用于領域對象各部分關 系的固定規則得到滿足,客戶必須知道對象的一些規則。甚至調用構造函數也會使客戶與所要構 建的對象的具體類產生耦合。結果是,對領域對象實現所做的任何修改都要求客戶做出相應修改, 這使得重構變得更加困難。
當客戶負責創建對象時,它會牽涉不必要的復雜性,并將其職責搞得模糊不清。這違背了領 域對象及所創建的AGGREGATE的封裝要求。更嚴重的是,如果客戶是應用層的一部分,那么職責 就會從領域層泄漏到應用層中。應用層與實現細節之間的這種耦合使得領域層抽象的大部分優勢 蕩然無存,而且導致后續更改的代價變得更加高昂。
對象的創建本身可以是一個主要操作,但被創建的對象并不適合承擔復雜的裝配操作。將這 些職責混在一起可能產生難以理解的拙劣設計。讓客戶直接負責創建對象又會使客戶的設計陷入 混亂,并且破壞被裝配對象或AGGREGATE的封裝,而且導致客戶與被創建對象的實現之間產生 過于緊密的耦合。
因此:
應該將創建復雜對象的實例和AGGREGATE的職責轉移給單獨的對象,這個對象本身可能沒有 承擔領域模型中的職責,但它仍是領域設計的一部分。提供一個封裝所有復雜裝配操作的接口, 而且這個接口不需要客戶引用要被實例化的對象的具體類。在創建AGGREGATE時要把它作為一 個整體,并確保它滿足固定規則。
*任何好的工廠都需滿足以下兩個基本需求。
(1) 每個創建方法都是原子的,而且要保證被創建對象或AGGREGATE的所有固定規則。 FACTORY生成的對象要處于一致的狀態。在生成ENTITY時,這意味著創建滿足所有固定規則的整 個AGGREGATE,但在創建完成后可以向聚合添加可選元素。在創建不變的VALUE OBJECT時,這意 味著所有屬性必須被初始化為正確的最終狀態。如果FACTORY通過其接口收到了一個創建對象的 請求,而它又無法正確地創建出這個對象,那么它應該拋出一個異常,或者采用其他機制,以確 保不會返回錯誤的值。
(2) FACTORY應該被抽象為所需的類型,而不是所要創建的具體類。[Gamma et al. 1995]中的 高級FACTORY模式介紹了這一話題。*
選擇FACTORY及其應用位置
一般來說,FACTORY的作用是隱藏創建對象的細節,而且我們把FACTORY用在那些需要隱藏 細節的地方。這些決定通常與AGGREGATE有關。
- 如果需要向一個已存在的AGGREGATE添加元素,可以在AGGREGATE的根上創建一個 FACTORY METHOD。這樣就可以把AGGREGATE的內部實現細節隱藏起來,使任何外部客戶看不到 這些細節,同時使根負責確保AGGREGATE在添加元素時的完整性
- 另一種情況是:在一個對象上使用FACTORY METHOD,這個對象與生成另一個對象密切相關, 但它并不擁有所生成的對象。當一個對象的創建主要使用另一個對象的數據(或許還有規則)時, 則可以在后者的對象上創建一個FACTORY METHOD,這樣就不必將后者的信息提取到其他地方來創建前者。這樣做還有利于表達前者與后者之間的關系。
- FACTORY與被構建對象之間是緊密耦合的,因此FACTORY應該只被關聯到與被構建對象有著密 切聯系的對象上。當有些細節需要隱藏(無論要隱藏的是具體實現還是構造的復雜性)而又找不 到合適的地方來隱藏它們時,必須創建一個專用的FACTORY對象或SERVICE。整個AGGREGATE通常 由一個獨立的FACTORY來創建,FACTORY負責把對根的引用傳遞出去,并確保創建出的AGGREGATE 滿足固定規則。如果AGGREGATE內部的某個對象需要一個FACTORY,而這個FACTORY又不適合在 AGGREGATE根上創建,那么應該構建一個獨立的FACTORY。但仍應遵守規則——把訪問限制在 AGGREGATE內部,并確保從AGGREGATE外部只能對被構建對象進行臨時引用
有些情況下只需使用構造函數
在有些情況下直接使用構造函數確實是最佳選擇。FACTORY實際上會使那些不具 有多態性的簡單對象復雜化。
在以下情況下最好使用簡單的、公共的構造函數:
- 類(class)是一種類型(type)。它不是任何相關層次結構的一部分,而且也沒有通過接口實現多態性。
- 客戶關心的是實現,可能是將其作為選擇STRATEGY的一種方式。
- 客戶可以訪問對象的所有屬性,因此向客戶公開的構造函數中沒有嵌套的對象創建。
- 構造并不復雜。
- 公共構造函數必須遵守與FACTORY相同的規則:它必須是原子操作,而且要滿足被創建對
象的所有固定規則。
不要在構造函數中調用其他類的構造函數。構造函數應該保持絕對簡單。復雜的裝配,特別是AGGREGATE,需要使用FACTORY。使用FACTORY METHOD的門檻并不高。
Java類庫提供了一些有趣的例子。所有集合都實現了接口,接口使得客戶與具體實現之間不 產生耦合。然而,它們都是通過直接調用構造函數創建的。但是,集合類本來是可以使用FACTORY來封裝集合的層次結構的。而且,客戶也可以使用FACTORY的方法來請求所需的特性,然后由 FACTORY來選擇適當的類來實例化。這樣一來,創建集合的代碼就會有更強的表達力,而且新增 集合類時不會破壞現有的Java程序。
但在某些場合下使用具體的構造函數更為合適。首先,在很多應用程序中,實現方式的選擇 對性能的影響是非常敏感的,因此應用程序需要控制選擇哪種實現(盡管如此,真正智能的 FACTORY仍然可以滿足這些因素的要求)。不管怎樣,集合類的數量并不多,因此選擇并不復雜。
雖然沒有使用FACTORY,但抽象集合類型仍然具有一定價值,原因就在于它們的使用模式。 集合通常都是在一個地方創建,而在其他地方使用。這意味著最終使用集合(添加、刪除和檢索 其內容)的客戶仍可以與接口進行對話,從而不與實現發生耦合。集合類的選擇通常由擁有該集
142 合的對象來決定,或是由該對象的FACTORY來決定。
接口的設計
當設計FACTORY的方法簽名時,無論是獨立的FACTORY還是FACTORY METHOD,都要記住以下 兩點:
- 每個操作都必須是原子的
- Factory將與其參數發生耦合
- 最安全的參數是那些來自較低設計層的參數。即使在同一層中,也有一種自然的分層傾向, 其中更基本的對象被更高層的對象使用
- 另一個好的參數選擇是模型中與被構建對象密切相關的對象,這樣不會增加新的依賴。
- 使用抽象類型的參數,而不是它們的具體類。FACTORY與被構建對象的具體類發生耦合,而無需與具體的參數發生耦合。
固定規則的相關邏輯應放置在哪里
FACTORY負責確保它所創建的對象或AGGREGATE滿足所有固定規則,然而在把應用于一個對象的規則移到該對象外部之前應三思。FACTORY可以將固定規則的檢查工作委派給被創建對象, 而且這通常是最佳選擇。
雖然原則上在每個操作結束時都應該應用固定規則,但通常對象所允許的轉換可能永遠也不 會用到這些規則。可能ENTITY標識屬性的賦值需要滿足一條固定規則。但該標識在創建后可能一 直保持不變。VALUE OBJECT則是完全不變的。如果邏輯在對象的有效生命周期內永遠也不被用到, 那么對象就沒有必要攜帶這個邏輯。 在這種情況下,FACTORY是放臵固定規則的合適地方,這樣 可以使FACTORY創建出的對象更簡單。
ENTITY FACTORY與VALUE OBJECT FACTORY
由于VALUE OBJECT是不可變 的,因此,FACTORY所生成的對象就是最終形式。因此FACTORY操作必須得到被創建對象的完整 描述。而ENTITY FACTORY則只需具有構造有效AGGREGATE所需的那些屬性。對于固定規則不關心 的細節,可以之后再添加。
我們來看一下為ENTITY分配標識時將涉及的問題(VALUE OBJECT不會涉及這些問題)。正如 第5章所指出的那樣,既可以由程序自動分配一個標識符,也可以通過外部(通常是用戶)提供 一個標識符。如果客戶的標識是通過電話號碼跟蹤的,那么該電話號碼必須作為參數被顯式地傳 遞給FACTORY。當由程序分配標識符時,FACTORY是控制它的理想場所。盡管唯一跟蹤ID實際上 144 是由數據庫“序列”或其他基礎設施機制生成的,但FACTORY知道需要什么樣的標識,以及將標 識放到何處。
重建已存儲的對象
用于重建對象的FACTORY與用于創建對象的FACTORY很類似,主要有以下兩點不同:
當創建新對象時,如果未滿足固定規則,FACTORY應該簡單地拒絕創建對象,但在重建對象時則需要更靈活的響應。如果對象已經在系統的某個地方存在(如在數據庫中),那么不能忽略這個事實。但是, 同樣也不能任憑規則被破壞。必須通過某種策略來修復這種不一致的情況,這使得重建對象比創 建新對象更困難。
REPOSITORY
背景: 我們可以通過對象之間的關聯來找到對象。但當它處于生命周期的中間時,必須要有一個起 點,以便從這個起點遍歷到一個ENTITY或VALUE。
無論要用對象執行什么操作,都需要保持一個對它的引用。那么如何獲得這個引用呢?一種 方法是創建對象,因為創建操作將返回對新對象的引用。第二種方法是遍歷關聯。我們以一個已 知對象作為起點,并向它請求一個關聯的對象。這樣的操作在任何面向對象的程序中都會大量用 到,而且對象之間的這些鏈接使對象模型具有更強的表達能力。但我們必須首先獲得作為起點的 那個對象。
客戶需要一種有效的方式來獲取對已存在的領域對象的引用。如果基礎設施提供了這方面的 便利,那么開發人員可能會增加很多可遍歷的關聯,這會使模型變得非常混亂。另一方面,開發 人員可能使用查詢從數據庫中提取他們所需的數據,或是直接提取具體的對象,而不是通過 AGGREGATE的根來得到這些對象。這樣就導致領域邏輯進入查詢和客戶代碼中,而ENTITY和 VALUE OBJECT則變成單純的數據容器。采用大多數處理數據庫訪問的技術復雜性很快就會使客 戶代碼變得混亂,這將導致開發人員簡化領域層,最終使模型變得無關緊要。**
目標: 根據到目前為止所討論的設計原則,如果我們找到一種訪問方法,它能夠明確地將模型作為 焦點,從而應用這些原則,那么我們就可以在某種程度上縮小對象訪問問題的范圍
初學者可 以不必關心臨時對象。臨時對象(通常是VALUE OBJECT)只存在很短的時間,在客戶操作中用到 它們時才創建它們,用完就刪除了。我們也不需要對那些很容易通過遍歷來找到的持久對象進行 查詢訪問。例如,地址可以通過Person對象獲取。而且最重要的是,除了通過根來遍歷查找對象 這種方法以外,禁止用其他方法對AGGREGATE內部的任何對象進行訪問。
持久化的VALUE OBJECT一般可以通過遍歷某個ENTITY來找到,在這里ENTITY就是把對象封
裝在一起的AGGREGATE的根。事實上,對VALUE的全局搜索訪問常常是沒有意義的,因為通過屬 性找到VALUE OBJECT相當于用這些屬性創建一個新實例。但也有例外情況。例如,當我在線規劃 旅行線路時,有時會先保存幾個中意的行程,過后再回頭從中選擇一個來預訂。這些行程就是 VALUE(如果兩個行程由相同的航班構成,那么我不會關心哪個是哪個),但它們已經與我的用 戶名關聯到一起了,而且可以原封不動地將它們檢索出來。另一個例子是“枚舉”,在枚舉中一 個類型有一組嚴格限定的、預定義的可能值。但是,對VALUE OBJECT的全局訪問比對ENTITY的全 局訪問更少見,如果確實需要在數據庫中搜索一個已存在的VALUE,那么值得考慮一下,搜索結 果可能實際上是一個ENTITY,只是尚未識別它的標識。
現在可以更精確地將問題重新表述如下:
_在所有持久化對象中,有一小部分必須通過基于對象屬性的搜索來全局訪問。當很難通過遍 歷方式來訪問某些AGGREGATE根的時候,就需要使用這種訪問方式。它們通常是ENTITY,有時 是具有復雜內部結構的VALUE OBJECT,還可能是枚舉VALUE。而其他對象則不宜使用這種訪問方 式,因為這會混淆它們之間的重要區別。隨意的數據庫查詢會破壞領域對象的封裝和 AGGREGATE。技術基礎設施和數據庫訪問機制的暴露會增加客戶的復雜度,并妨礙模型驅動的 設計。
有得必有失,我們應該注意失去了什么。我們已經不再考慮領域模型中的概念。代碼也不再 表達業務,而是對數據庫檢索技術進行操縱。REPOSITORY是一個簡單的概念框架,它可用來封裝 這些解決方案,并將我們的注意力重新拉回到模型上。_
因此:
為每種需要全局訪問的對象類型創建一個對象,這個對象相當于該類型的所有對象在內存中 的一個集合的“替身”。通過一個眾所周知的全局接口來提供訪問。提供添加和刪除對象的方法,
151 用這些方法來封裝在數據存儲中實際插入或刪除數據的操作。提供根據具體條件來挑選對象的方 法,并返回屬性值滿足查詢條件的對象或對象集合(所返回的對象是完全實例化的),從而將實 際的存儲和查詢技術封裝起來。只為那些確實需要直接訪問的AGGREGATE根提供REPOSITORY。 讓客戶始終聚焦于模型,而將所有對象的存儲和訪問操作交給REPOSITORY來完成。
REPOSITORY有很多優點,包括:
- 它們為客戶提供了一個簡單的模型,可用來獲取持久化對象并管理它們的生命周期;
- 它們使應用程序和領域設計與持久化技術(多種數據庫策略甚至是多個數據源)解耦;
- 它們體現了有關對象訪問的設計決策;
- 可以很容易將它們替換為“啞實現”(dummy implementation),以便在測試中使用(通常
使用內存中的集合)。
實現:
- 對類型進行抽象。REPOSITORY“含有”特定類型的所有實例,但這并不意味著每個類都 需要有一個REPOSITORY。類型可以是一個層次結構中的抽象超類(例如,TradeOrder可以 是BuyOrder或SellOrder)。類型可以是一個接口——接口的實現者并沒有層次結構上的關 聯,也可以是一個具體類。記住,由于數據庫技術缺乏這樣的多態性質,因此我們將面 臨很多約束。
- 充分利用與客戶解耦的優點。 我們可以很容易地更改REPOSITORY的實現,但如果客戶直接調用底層機制,我們就很難修改其實現。也可以利用解耦來優化性能,因為這樣就可以使用不同的查詢技術,或在內存中緩存對象,可以隨時自由地切換持久化策略。通過 提供一個易于操縱的、內存中的(in-memory)啞實現,還能夠方便客戶代碼和領域對象
的測試。 - 將事務的控制權留給客戶。 盡管REPOSITORY會執行數據庫的插入和刪除操作,但它通常不會提交事務。例如,保存數據后緊接著就提交似乎是很自然的事情,但想必只有客戶 才有上下文,從而能夠正確地初始化和提交工作單元。如果REPOSITORY不插手事務控制, 那么事務管理就會簡單得多。
REPOSITORY與FACTORY的關系
從領域驅動設計的角度來看,FACTORY 和REPOSITORY具有完全不同的職責。FACTORY負責制造新對象,而REPOSITORY負責查找已有對象。 REPOSITORY應該讓客戶感覺到那些對象就好像駐留在內存中一樣。對象可能必須被重建(的確, 可能會創建一個新實例),但它是同一個概念對象,仍舊處于生命周期的中間。
REPOSITORY也可以委托FACTORY來創建一個對象,這種方法(雖然實際很少這樣做,但在理 論上是可行的)可用于從頭開始創建對象,此時就沒有必要區分這兩種看問題的角度了
這種職責上的明確區分還有助于FACTORY擺脫所有持久化職責。FACTORY的工作是用數據來 實例化一個可能很復雜的對象。如果產品是一個新對象,那么客戶將知道在創建完成之后應該把 它添加到REPOSITORY中,由REPOSITORY來封裝對象在數據庫中的存儲:
總結
- 上一篇: html5网页制作电脑版,页未央HTML
- 下一篇: 小红书接口加密参数X-sign