《領域驅動設計:軟體核心複雜性應對之道》 隨筆(三)
每個對象都有它的生命週期。一個對象在建立以後,可能要經曆各種不同的狀態,並最終消亡。當然許多個物件都是簡單的臨時對象,我們只是調用其建構函式把它們建立出來,在某種計算中使用它們,然後就把它們扔到垃圾收集器中去了。
如何管理這些對象給我們提出了挑戰,處理不好就很容易使我們的工作偏離模型驅動設計的方向。
挑戰可以分為兩類:
在生命週期中維護對象的完整性;
避免模型由於管理生命週期的複雜性而陷入困境。
通過3個模式來處理這些問題。
彙總(Aggregate)通過定義清晰的所有權和邊界來使得模型變得更緊湊,避免出現盤根錯節的對象(關係)網。這個模式對於在生命週期的各個階段中維護完整性是非常關鍵的。
用工廠(Factory)來建立和重組複雜的對象和彙總,並保持對其內部結構良好的封裝。
倉儲(Repository)用於處理生命週期的中間和結束部分,為我們提供尋找和提取持久對象的方法,同時把與生命週期管理有關的複雜基礎設施封裝起來。
雖然工廠和倉儲本身本身並不是從領域中產生的,但它們在領域設計中作用不容忽視。這些構造為我們提供了訪問和控制模型對象的方法,完善了模型驅動設計。
建立彙總的模型,並把工廠和倉儲加入設計中來,可以使我們系統地對模型對象進行操縱,同時使得這些對象的生命週期成為一個個意義明確的單元。彙總圈出一個範圍,在這個範圍中無論對象處於生命週期的哪個階段,都應保持不變性。工廠和倉儲對象合進行操作,將特定生命週期變遷的複雜性封裝起來。
在包含著複雜關聯的模型中,要保證對象修改的一致性是很困難的。我們必須保證緊密關聯的對象組也能保持不變性,而不僅僅只保證各個離散的對象。如果鎖定策略過于謹慎,就會導致多個使用者毫無必要地互相干擾,使系統變得無法使用。
一個彙總是一族相關聯的對象,出於資料變化的目的,我們將這些對象視為一個單元。每個彙總都有一個根(root)和一個邊界。邊界定義了彙總中包含什麼;根是包含在彙總中的單個特定的實體。根是彙總中唯一允許被外部對象引用的元素,但在彙總的邊界內,對象之間可以相互引用。根之外的實體具有本地標識,但是它們僅僅在彙總內部才需要區分其標識,因為根實體上下文以外的外部對象不會看到它們。
將實體和值對象聚集到彙總中。每個彙總定義了一個邊界。為每個彙總選擇一個實體作為其根,並通過根來控制所有對邊界內對象的訪問。外部對象只能持有根的引用;對內部元素的臨時引用只能在單個操作中使用。由於根控制了訪問,因此我們無法繞過它去修改內部元素。這種安排使得我們可以保證在任何狀態變化中,彙總本身(作為一個整體)的不變數,以及彙總中對象的不變數都可以被滿足。
彙總圈定出一個範圍,在這個範圍中無論出於生命週期的哪個階段,都應該滿足不變數。接下來的工廠和倉儲模式用來對彙總進行操作,將特定生命週期變遷的複雜性封裝起來。
當建立一個對象或整個彙總的邏輯變得非常複雜,或者過多地暴露了內部結構時,工廠提供了封裝。
“建立”可以作為對象自身一個主要的操作,但是,把對象的建立職責與複雜的組裝操作組合起來是不恰當的。這樣會使得設計非常笨拙而且難以理解。讓客戶直接構造對象又讓客戶的設計非常混亂,破壞了對被組裝對象或彙總的封裝,使得客戶和被建立的對象的實現完全關聯到了一起。
複雜的對象建立工作是一種領域層的職責,然而建立工作並不屬於表達模型的對象。
對象的建立和組裝在領域中通常沒有含義;它們只是實現上的需要。
每種物件導向語言都提供了一種建立對象的機制,但是我們還是需要一種更加抽象的,與其他對象解耦的構造。擔負了建立其他對象的職責的程式元素被稱為工廠。
將建立複雜物件或彙總的執行個體的職責分離到一個單獨的對象中來,這個對象本身可能在領域模型中沒有職責,但仍然是領域設計的一部分。它提供了一個將所有複雜的組裝封裝起來的介面,這樣客戶就無需引用它要執行個體化的對象的具體類了。用工廠把彙總作為一個整體建立出來,並保證其不變數得到滿足。
工廠封裝了對象在建立和重建時的生命週期變遷。還有另一種變遷,它在技術上的複雜性也會導致領域設計陷入混亂,那就是對象和儲存之間的雙向轉換。這種轉換是另一種領域設計構造——倉儲的職責。
客戶需要一種可行的方法來獲得已有的領域對象的引用。如果基礎結構允許客戶的開發人員很容易地加入關聯,那麼他們就會加入更多的導航關聯,把模型弄得一團糟。另一種可能是,開發人員會使用查詢提取他們需要的額外資料,或者提取一些特殊的對象,而它們本來是應該通過彙總根來訪問。領域邏輯跑到查詢代碼和客戶代碼中去了,而實體和值對象變成了純粹的資料容器。大部分資料庫訪問基礎結構的技術複雜性,很快使得客戶代碼陷入混亂,最終開發人員只好拋開領域層,把模型變成了一個擺設。
部分持久化對象必須通過按對象屬性進行查詢的方式來實現全域訪問。對不便於通過導航來訪問的彙總根來說,這種訪問方式是必需的。這些對象通常是實體,有時包含複雜內部結構的值對象,有時是枚舉值。為其他對象提供這種訪問會使一些重要的區別變得模糊。不受限制的資料庫查詢實際上會破壞領域對象和彙總的封裝。把技術基礎結構和資料庫訪問機制暴露出來,會使客戶變得複雜,同時掩蓋了模型驅動設計。
但即便有這些技術,我們還是要注意失去了些什麼。我們不再考慮領域模型中的概念了。我們的代碼也不再描述與業務相關的事情;它只是在使用資料檢索技術。倉儲模式是一個簡單的概念架構,用來把上面那些解決方案封裝起來,並找回模型的焦點。
一個倉儲將某種類型的所有對象描述為一個概念性的集合。它的行為與集合類似,但是包含更精細的查詢能力。
倉儲可以加入和刪除具有合適類型的對象,並通過倉儲背後的機制將它們插入資料庫或從資料庫刪除。
倉儲具有一系列緊密相關的職責,為我們提供了對彙總根從產生之初直到其生命週期結束期間的訪問能力。
為每種需要全域訪問的物件類型建立一個對象,該對象為該類型所有對象在記憶體中的集合提供影像。用一個眾所周知的全域介面來設立訪問入口。提供增刪對象的方法,把對資料存放區的實際的插入和刪除封裝起來。提供根據某種標準篩選對象的方法,返回完整執行個體化了的屬性值符合標準的對象或對象集合,把實際的儲存和查詢技術封裝起來。僅為確實需要直接存取的彙總根提供倉儲。讓客戶聚焦於模型,把所有Object Storage Service和訪問的工作委託給倉儲來完成。
倉儲具有許多優點,包括:
它們為客戶提供了一個簡單的模型,來擷取持久對象並管理其生命週期;
它們把應用和領域設計從持久技術、多資料庫策略或甚至多種資料來源解耦出來;
它們傳達了對象訪問的設計決策
它們可以很容易被替換為啞實現,以便測試中使用(通常使用一個記憶體中的集合)
工廠處理的對象生命週期的開始,而倉儲則協助管理生命週期的中間和結束部分。
在這種領域驅動的設計視角中,工廠和倉儲具有完全不同的職責。工廠建立新的對象;而倉儲尋找舊的對象。
倉儲必須讓客戶覺得那些對象好像就在記憶體中。那些對象也許不得不重建(是的,必須要建立一個新的執行個體),但是它是同一個概念對象,仍然處於它的生命週期之中。
清晰地區分建立和重建有助於把所有與持久化相關的職責從工廠中分離出來。工廠的工作是根據資料來執行個體化一個可能非常複雜的對象。
如果產品是一個新的對象,那麼客戶會知道這一點,並把它添加到倉儲中去,而對象儲存到資料庫中的工作則由倉儲來封裝。