3. 領域物件的生命週期
每個物件都有生命週期,如下圖所示。物件自建立後,可能會經歷各種不同的狀態,直至最終消亡——要麼存檔,要麼刪除。當然很多物件是簡單的臨時物件,僅通過呼叫建構函式來建立,用來做一些計算,然後由垃圾收集器回收。這類物件沒必要搞得那麼複雜。但有些物件具有更長的生命週期,其中一部分時間不是在活動記憶體中度過的。它們與其他物件具有複雜的相互依賴性。它們會經歷一些狀態變化,在變化時要遵守一些固定規則。管理這些物件時面臨諸多挑戰,稍有不慎就會偏離 Model-Driven Design 的軌道。
主要的挑戰有以下兩類。
(1)在整個生命週期中維護完整性。
(2)防止模型陷入管理生命週期複雜性造成的困境當中。
下面通過三種模式解決這些問題。首先是 Aggregate(聚合),它通過定義清晰的所屬關係和邊界,並避免混亂、錯綜複雜的物件關係網來實現模型的內聚。聚合模式對於維護生命週期各個階段的完整性具有至關重要的作用。
接下來,我們講注意力轉移到生命週期的開始階段,使用 Factory(工廠)來建立和重建複雜物件和 Aggregate(聚合),從而封裝它們的內部結構。最後,在生命週期的中間和末尾使用 Repository(儲存庫)來提供查詢和檢索持久化物件並封裝龐大基礎設施的手段。
儘管 Repository 和 Factory 本身並不是來源於領域,但它們在領域設計中扮演著重要的角色。這些結構提供了易於掌握的模型物件處理方式,使 Model- Driven Design 更完備。
使用 Aggregate 進行建模,並且在設計中結合使用 Factory 和 Repository ,這樣我們就能夠在模型物件的整個生命週期中,以有意義的單元、系統地操縱它們。Aggregate 可以劃分出一個範圍,這個範圍內的模型元素在生命週期各個階段都應該維護其固定規則。Factory 和 Repository 在 Aggregate 基礎上進行操作,將特定生命週期的複雜封裝起來。
3.1 模式:Aggregate
減少設計中的關聯有助於簡化物件之間的遍歷,並在某種程度上限制關係的急劇增多。但大多數業務領域中的物件都具有十分複雜的聯絡,以至於最終形成很長、很深的物件參照路徑,我們不得不在這個路徑上追蹤物件。在某種程度上,這種混亂狀態反映了現實世界,因為現實世界中就很少有清晰的邊界。但這卻是軟體設計中的一個重要問題。
假設我們從資料庫中刪除一個 Person 物件。這個人的姓名、出生日期和工作描述要一起被刪除,但要如何處理地址呢?可能還有其他人住在同一地址。如果刪除了地址,那些 Person 物件將會參照一個被刪除的物件。如果保留地址,那麼垃圾地址在資料庫中會積累起來。雖然自動垃圾收集機制可以清除垃圾地址,但這也是一種技術上的修復;就算資料庫系統存在這種處理機制,一個基本的建模問題依然被忽略。
即便是在考慮孤立的事務時,典型物件模型中的關係網也使我們難以斷定一個修改會產生哪些潛在的影響。僅僅因為存在依賴就更新系統的每個物件,這樣做是不現實的。
在多個客戶對相同物件進行並行存取的系統中,這個問題更加突出。當很多使用者對系統中的物件進行查詢和更新時,必須防止他們同時修改互相依賴的物件。範圍錯誤將導致嚴重的後果。
在具有複雜關聯的模型中,要保證物件更改的一致性是很困難的。不僅互不關聯的物件需要遵守一些固定規則,而且緊密關聯的各組物件也要遵守一些固定規則。然而,過於謹慎的鎖定機制會導致多個使用者之間毫無意義地相互干擾,從而使系統不可用。
換句話說,我們如何知道一個由其他物件組成的物件從哪裡開始,又到何處結束呢?在任何具有持久化資料儲存的系統中,對資料進行修改的事務必須要有範圍,而且要保持資料一致性的方式(也就是說,保持資料遵守固定規則)。資料庫支援各種鎖機制,而且可以編寫一些測試來驗證。但這些特殊的解決方案分散了人們對模型的注意力,很快人們就會回到 「走一步,看一步」 的老路上來。
實際上,要找到一種兼顧各種問題的解決方案,要求對領域有深刻的理解,例如,要了解特定類範例之間的更改頻率這樣的深層次因素。我們需要找到一個使物件間衝突較少而固定規則聯絡更緊密的模型。
儘管從表面上看這個問題是資料庫事務方面的一個技術難題,但它的根源卻在模型,歸根結底是由於模型中缺乏明確定義的邊界。從模型得到的解決方案將使模型更易於理解,並且使設計更易於溝通。當模型被修改時,它將引導我們對實現做出修改。
人們已經開發出很多模式(scheme)來定義模型中的所屬關係。
首先,我們需要用一個抽象來封裝模型中的參照。Aggregate 就是一組相關物件的集合,我們把它當作資料修改的單元。每個 Aggregate 都有一個根(root)和一個邊界(boundary)。邊界定義了 Aggregate 的內部都有什麼。根則是 Aggreg ate 所包含的一個特定 Entity。對 Aggregate 而言,外部物件只可以參照根,而邊界內部的物件之間則可以相互參照。除根以外的其他 Entity 都有本地標識,但這些標識只在 Aggregate 內部才需要加以區別,因為外部物件除了根 Entity 之外看不到其他物件。
固定規則(invariant)是指在資料變化時必須保持的一致性規則,其涉及 Aggregate 成員之間的內部關係。而任何跨越 Aggregate 的規則將不要求每時每刻都保持最新狀態。通過事件處理、批次處理或其他更新機制,這些依賴會在一定時間內得以解決。但在每個事務完成時,Aggregate 內部所應用的固定規則必須得到滿足,
現在,為了實現這個概念上的 Aggregate ,需要對所有事務應用一組規則。
(1)根 Entity 具有全域性標識,它最終負責檢查固定規則。
(2)根 Entity 具有全域性標識。邊界內的 Entity 具有本地標識,這些標識只在 Aggregate 內部才是唯一的。
(3)Aggregate 外部的物件不能參照除根 Entity 之外的任何內部物件。根 Entity 可以把內部 Entity 的參照傳遞給它們,但這些物件只能臨時使用這些參照,而不能保持參照。根可以把一個 Value Object 的副本傳遞給另一個物件,而不關心它發生了什麼變化,因為它只是一個 Value,不再與 Aggregate 有任何關聯。
(4)作為上一條規則的推論,只有 Aggregate 的根才能直接通過資料庫查詢獲取。所有其他物件必須通過遍歷關聯來發現。
(5)Aggregate 內部的物件可以保持對其他 Aggregate 根的參照。
(6)刪除操作必須一次刪除 Aggregate 邊界內的所有物件。(利用垃圾收集機制,這很容易做到。由於除根以外的其他物件都沒有外部參照,因此刪除了根以後,其他物件均會被回收)
(7)當提交對 Aggregate 邊界內部的任何物件的修改時,整個 Aggregate 的所有固定規則必須被滿足。
我們應該將 Entity 和 Value Object 分門別類地聚集到 Aggregate 中,並定義每個 Aggregate 的邊界。在每個 Aggregate 中,選擇一個 Entity 作為根,並通過根來控制對邊界內其他物件的所有存取。只允許外部物件保持對根的參照。對內部成員的臨時參照可以被傳遞出去,但僅在一次操作中有效。由於根控制存取,因此不能繞過它來修改內部物件。這種設計有利於確保 Aggregate 中的物件滿足所有固定規則,也可以確保在任何狀態變化時 Aggregate 作為一個整體滿足固定規則。
有一個能夠宣告 Aggregate 的技術框架是很有幫助的,這樣就可以自動實施鎖機制和其他一些功能。如果沒有這樣的技術框架,團隊就必須靠自我約束來使用時限事先商定的 Aggregate,並按照這些 Aggregate 來編寫程式碼。
Aggregate 劃分出一個範圍,在這個範圍內,生命週期的每個階段都必須滿足一些固定規則。接下來要討論的兩種模式 Factory 和 Repository 都是在 Aggregate 上執行操作,它們將特定生命週期轉換的複雜性封裝起來。
3.2 模式:Factory
當建立一個物件或建立整個 Aggregate 時,如果建立工作很複雜,或者暴露了過多的內部結構,則可以使用 Factory 進行封裝。
物件的功能主要體現在其複雜的內部設定以及關聯方面。我們應該一直對物件進行提煉,直到所有與其意義或在互動中的角色無關的內容被完全剔除為止。一個物件在它的生命週期中要承擔大量職責。如果再讓複雜物件負責自身的建立,那麼職責過載將會導致問題。
但將職責轉交給另一相關方——應用程式中的客戶(client)物件——會產生更嚴重的問題。客戶知道需要完成什麼工作,並依靠領域物件來執行必要的計算。如果指望客戶來裝配它需要的領域物件,那麼它必須瞭解一些物件的內部結構。為了確保所有應用於領域物件各部分關係的固定規則得到滿足。客戶必須知道物件的一些規則。甚至呼叫建構函式也會使客戶與所要構建的物件的具體類產生耦合。結果是,對領域物件實現所做的任何修改都要求客戶做出相應修改,這使得重構變得更加困難。
當客戶建立物件時,它會牽涉不必要的複雜性,並將其職責搞得模糊不清。這違背了領域物件及建立的 Aggregate 的封裝要求。更嚴重的是,如果客戶是應用層的一部分,那麼職責就會從領域層洩漏到應用層。應用層與實現細節之間的這種耦合使得領域層抽象的大部分優勢蕩然無存,而且導致後續更改的代價變得更加高昂。
物件的建立本身可以是一個主要操作,但被建立的物件並不適合承擔複雜的裝配操作。將這些職責混在一起可能產生難以理解的拙劣設計。讓客戶直接負責建立物件又會使客戶的設計陷入混亂,並且破壞被裝配物件或 Aggregate 的封裝,而且導致客戶與被建立物件的實現之間產生過於緊密的耦合。
複雜的物件建立是領域層的職責,然而這項任務並不屬於那些用於表示模型的物件。在這些情況下,物件的建立和裝配對應於領域中的重要事件。如 「開立銀行賬號」。但一般情況下,物件的建立和裝配在領域中並沒有什麼意義,它們只不過是實現的一種需要。為了解決這一問題,我們必須在領域設計中增加一種新的構造,它不是 Entity、Value Object ,也不是 Service。這與前面的論述違背,因此把它解釋清楚很重要。我們正在向設計中新增一些元素,但它們不對應於模型中的任何事物,而確實又承擔了領域層的部分職責。
每種物件導向的語言都提供了一種建立物件的機制(例如,建構函式),但我們仍然需要一種更加抽象且不與其他物件發生耦合的構造機制。這就是 Factory,它是一種負責建立其他物件的程式元素。如圖:
正如物件的介面應該封裝物件的實現一樣(從而使客戶無需知道物件的工作機理就可以使用物件的功能),Factory 封裝了建立物件或 Aggregate 所需的知識。它提供了反映客戶目標的介面,以及被建立物件的抽象檢視。
因此:
應該將建立複雜物件的範例和 Aggregate 的職責轉移給單獨的物件,這個物件本身可能沒有承擔領域模型中的職責,但它仍是領域設計的一部分。提供一個封裝所有複雜裝配操作的介面,而且這個介面不需要客戶參照要被範例化的物件的具體類。在建立 Aggregate 時要把它作為一個整體,並確保它滿足固定規則。
Factory 有很多設計方式。包括 Factory Method(工廠方法)、Abstract Factory(抽象工廠)和 Builder(構建器)。
任何好的工廠都需要滿足以下兩個基本需求。
(1)每個建立方法都是原子的,而且要保證被建立物件或 Aggregate 的所有固定規則。Factory 生成的物件要處於一致的狀態。在生成 Entity 時,這意味著建立滿足所有固定規則的整個 Aggregate,但在建立完成後可以向聚合新增可選元素。在建立不變的 Value Object 時,這意味著所有屬性必須被初始化為正確的最終狀態。如果 Factory 通過其介面接收到了一個建立物件的請求,而它又無法正確地建立出這個物件,那麼它應該丟擲一個異常,或者採用其他機制,以確保不會返回錯誤的值。
(2)Factory 應該被抽象為所需的型別,而不是所要建立的具體類。
3.2.1 選擇 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 外部只能對被構建物件進行臨時參照。
3.2.2 有些情況下只需使用建構函式
Factory 的引入提供了巨大的優勢,而這種優勢往往並未得到充分利用。但是,在有些情況下直接使用建構函式確實是最佳選擇。Factory 實際上會使那些不具有多型性的簡單物件複雜化。
在以下情況下最好使用簡單的、公共的建構函式。
(1)類(class)是一種型別(type)。它不是任何相關層次結構的一部分,而且也沒有通過介面實現多型。
(2)客戶關心的是實現,可能是將其作為選擇 Strategy 的一種方式。
(3)客戶可以存取物件的所有屬性,因此向客戶公開的建構函式中沒有巢狀的物件建立。
(4)構造並不複雜。
(5)公共建構函式必須遵守與 Factory 相同的規則:它必須是原子操作,而且要滿足被建立物件的所有固定規則。
不要在建構函式中呼叫其他類別建構函式。建構函式應該保持絕對簡單。複雜的裝配,特別是 Aggregate ,需要使用 Factory。使用 Factory Method 的門檻並不高。
3.2.3 介面的設計
當設計 Factory 的方法簽名時,無論是獨立的 Factory 還是 Factory Method,都要記住以下兩點:
(1)每個操作都必須是原子的。我們必須在與 Factory 的一次互動中把建立物件所需的所有資訊傳遞給 Factory 。同時必須確定當建立失敗時將執行什麼操作,比如某些固定規則沒有被滿足。可以丟擲一個異常或僅僅返回 null 。為了保持一致,可以考慮採用編碼標準來處理所有 Factory 的失敗。
(2)Factory 將與其引數發生耦合。如果在選擇輸入引數時不小心,可能會產生錯綜複雜的依賴關係。耦合程度取決於對引數(argument)的處理。如果只是簡單地將引數插入到要構建的物件中,則依賴程度是適中的。如果從引數中選出一部分在構造物件時使用,耦合將更緊密。
最安全的引數是那些來自較低設計層的引數。即使在同一層中,也有一種自然的分層傾向,其中更基本的物件被更高層的物件使用。
另一個好的引數選擇是模型中與被構建物件密切相關的物件,這樣不會增加新的依賴。
3.2.4 固定規則的相關邏輯應放在哪裡
Factory 負責確保它所建立的物件或 Aggregate 滿足所有固定規則,然而把應用於一個物件的規則移到該物件外部之前應三思。Factory 可以將固定規則的檢查工作委派給被建立的物件,而且通常是最佳選擇。
但 Factory 與被建立物件之間存在一種特殊關係。Factory 已經知道被建立物件的內部結構,而且建立 Factory 的目的與被建立物件的實現有著密切的聯絡。在某些情況下,把固定規則的相關邏輯放到 Factory 中是有好處的,這樣可以讓被建立物件的職責更明晰。對於 Aggregate 規則來說尤其如此(這些規則會約束很多物件)。但固定規則的相關邏輯卻不適合放到那些與其他領域物件關聯的 Factory Method 中。
雖然原則上在每個操作結束時都應該應用固定規則,但通常物件所允許的轉換可能永遠也不會用到這些規則。可能 Entity 標識屬性的賦值需要滿足一條固定規則。但該標識在建立後可能一直保持不變。Value Object 則是完全不變的。如果邏輯在物件的有效生命週期內永遠也不被用到,那麼物件就沒必要攜帶這個邏輯。在這種情況下,Factory 是放置固定規則的合適地方,這樣可以使 Factory 建立出的物件更簡單。
3.2.5 Entity Factory 與 Value Object Factory
Entity Factory 與 Value Object Factory 有兩個方面的不同。由於 Value Object 是不可變的,因此,Factory 所生成的物件就是最終形式。因此 Factory 操作必須得到被建立物件的完整描述。而 Entity Factory 則只需具有構造有效 Aggregate 所需的那些屬性。對於固定規則不關心的細節,可以之後再新增。
3.2.6 重建已儲存的物件
到目前為止,Factory 只是發揮了它在物件生命週期開始時的作用。到了某一時刻,大部分物件都要儲存在資料庫中或通過網路傳輸,而在當前的資料庫技術中,幾乎沒有哪種技術能夠保持物件的內容特徵。大多數傳輸方法都要將物件轉換為平面資料才能傳輸,這使得物件只能以非常有限的形式出現。因此,檢索操作潛在地需要一個複雜的過程將各個部分中信裝配成一個可用的物件。
用於重建物件的 Factory 與用於建立物件的 Factory 很類似,主要有以下兩點不同。
(1)用於重建物件的 Entity Factory 不分配新的跟蹤ID。如果重新分配ID,將丟失與先前物件的連續性。因此,在重建物件的 Factory 中,標識屬性必須是輸入引數的一部分。
(2)當固定規則未被滿足時,重建物件的 Factory 採用不同的方式進行處理。當建立新物件時,如果未滿足固定規則,Factory 應該簡單地拒絕建立物件,但在重建物件時則需要更靈活的響應。如果物件已經在系統的某個地方存在(如在資料庫中),那麼不能忽略這個事實。但是,同樣也不能任憑規則被破壞。必須通過某種策略來修復這種不一致的情況,這使得重建物件比建立物件更困難。
總之,必須把建立範例的存取點標識出來,並顯示地定義它們的範圍。它們可能只是建構函式,但通常需要一種更抽象或更復雜的範例建立機制。為了滿足這種需求,需要在設計中引入新的構造——Factory。Factory 通常不表示模型的任何部分,但它們是領域設計的一部分,能夠使物件更明確地表示出模型。
Factory 封裝了物件建立和重建時的生命週期轉換。還有一種轉換大大增加了領域設計的技術複雜性,這是物件與儲存之間的互相轉換。這種轉換由另一種領域設計構造來處理,它就是 Repository。
3.3 模式:Repository
我們可以通過物件之間的關聯來找到物件。但當它處於生命週期的中間時,必須要有一個起點,以便從這個起點遍歷到一個 Entity 或 Value。
資料庫搜尋是全域性可存取的,它使我們可以直接存取任何物件。由此,所有物件不需要相互聯結起來,整個物件關係網就能夠保持在可控的範圍內。是提供遍歷還是依靠搜尋,這成為一個設計決策,需要在搜尋的解耦與關聯的內聚之間做出權衡。Customer 物件應該保持該客戶所有已訂的 Order 嗎?應該通過 Customer ID 欄位在資料庫中查詢 Order 嗎?恰當地結合搜尋與關聯將會得到易於理解的設計。
遺憾的是,開發人員一般不會過多地考慮這種精細的設計,因為他們滿腦子都是需要用到的機制,以便很有技巧地利用它們來實現物件的儲存,取回和最終刪除。
從概念上講,物件檢索發生在 Entity 生命週期的中間。不能只是因為我們將 Customer 物件儲存在資料庫中,而後把它們檢索出來,這個 Customer 就代表一個新的客戶。為了記住這個區別,把使用已儲存的資料建立範例的過程稱為重建。
領域驅動設計的目標是通過關注領域模型(而不是技術)來建立更好的軟體。假設開發人員構造了一個 SQL 查詢,並將這些資訊傳遞給建構函式或 Factory 。開發人員執行這一連串操作的時候,早已不再把模型當作重點了。我們很自然地會把物件當作容器來放置查詢出來的資料,這樣整個設計就轉向了資料處理風格。雖然具體的技術細節有所不同,但問題仍然存在——客戶處理的是技術,而不是模型概念。諸如 Metadata Mapping Layer 這樣的基礎設施可以提供很大幫助,利用它很容易將查詢結果轉換為物件,但開發人員試圖會繞過模型的功能(如 Aggregate,甚至是物件封裝),而直接獲取和操作他們所需的資料。這將導致越來越多的領域規則嵌入到查詢程式碼中,或者乾脆丟失了。雖然物件資料庫清楚了轉換問題,但搜尋機制還是很機械的,開發人員仍傾向於要什麼就去拿什麼。
客戶需要一種有效的方式來獲取對已存在的領域物件的參照。如果基礎設施提供了這方面的便利,那麼開發人員可能會增加很多可遍歷的關聯,這會使模型變得非常混亂。另一方面,開發人員可能使用查詢從資料庫提取他們所需的資料,或是直接提取具體的物件,而不是通過 Aggregate 的根來得到這些物件。這樣就導致領域邏輯進入查詢和客戶程式碼中,而 Entity 和 Value Object 則變成單純的資料容器。採用大多數處理資料庫的技術複雜性很快就會使客戶程式碼變得混亂,這將導致開發人員簡化領域層,最終使模型變得無關緊要。
在所有持久化的物件中,有一小部分必須通過基於物件屬性的搜尋來全域性存取。當很難通過遍歷方式來存取某些 Aggregate 根的時候,就需要使用這種存取方式。它們通常是 Entity,有時候是具有複雜內部結構的 Value Object,還可能是列舉 Value。而其他物件則不宜使用這樣存取方式,因為這會混淆它們之間的重要區別。隨意的資料庫查詢會破壞領域物件的封裝和 Aggregate。技術基礎設施和資料庫存取機制的暴露會增加客戶的複雜度,並妨礙模型驅動設計。
有大量的技術可以用來解決資料庫存取的技術難題,例如,將 SQL 封裝到 Query Object 中,或利用 Metadata Mapping Layer 進行物件和表之間的轉換。Factory 可以幫助重建那些已儲存的物件。這些技術和很多其他技術有助於控制資料庫存取的複雜度。
有得必有失,我們應該注意失去了什麼。我們已經不再考慮領域模型中的概念。程式碼也不再表達業務,而是對資料庫檢索技術進行操縱。Repository 是一個簡單的概念框架,它可用來封裝這些解決方案,並將我們的注意力重新拉回到模型上。
Repository 將某種型別的所有物件表示為一個概念集合(通常是模擬的)。它的行為類似於集合,只是具有更復雜的查詢功能。在新增或刪除相應型別的物件時,Repository 的後臺機制負責將物件新增到資料庫中,或從資料庫中刪除物件。這個定義將一組緊密相關的職責集中在一起,這些職責提供了對 Aggregate 根的整個生命週期的全程存取。
客戶使用查詢方法向 Repository 請求物件,這些查詢方法根據客戶所指定的條件(通常是特定屬性的值)來挑選物件。Repository 檢索被請求的物件,並封裝資料庫查詢和後設資料對映機制。Repository 可以根據客戶所要求的各種條件來挑選物件。它們也可以返回彙總資訊,如有多少個範例滿足查詢條件。Repository 甚至能返回彙總計算,如所有匹配物件的某個數值屬性的總和。
Repository 解除了客戶的巨大負擔,使客戶只需與一個簡單的、易於理解的介面進行對話,並根據模型向這個介面提出它的請求。要實現所有這些功能需要大量複雜的技術基礎設施,但介面很簡單,而且在概念層次上與領域模型緊密聯絡起來。
因此:
為每種需要全域性存取的物件型別建立一個物件,這個物件相當於該型別的所有物件中在記憶體中的一個集合的「替身」。通過一個眾所周知的全域性介面來提供存取。提供新增和刪除物件的方法,用這些方法來封裝在資料儲存中實際插入或刪除資料的操作。提供根據具體條件來挑選物件的方法,並返回屬性值滿足查詢條件的物件或物件集合(所返回的物件是完全範例化),從而將實際的儲存和查詢技術封裝起來。只為那些確實需要直接存取的 Aggregate 根提供 Repository 。讓客戶聚焦於模型,而將所有物件的儲存和存取操作交給 Repository 來完成。
Repository 有很多優點,包括:
(1)它們為客戶提供了一個簡單的模型,可用來獲取持久化物件並管理它們的生命週期;
(2)它們使應用程式和領域設計與持久化技術(多種資料庫策略甚至是多個資料來源)解耦;
(3)它們體現了有關物件存取的設計決策;
(4)可以很容易將它們替換為「啞實現」,以便在測試中使用(通常使用記憶體中的集合)。
3.3.1 Repository 查詢
所有 Repository 都為客戶提供了根據某種條件來查詢物件的方法,但如何設計這個介面卻有很多選擇。
最容易構建的 Repository 用寫死的方式來實現一些具有特定引數的查詢。這些查詢可以形式各異,例如,通過標識來檢索 Entity(幾乎所有 Repository 都提供了這種查詢)、通過某個特定屬性值或複雜的引數組合來請求一個物件集合、根據值域(如日期範圍)來選擇物件,甚至可以執行某些屬於 Repository 一般職責範圍內的計算(特別是利用那些底層資料庫所支援的操作)。
在任何基礎設施上,都可以構建寫死式的查詢,也不需要很大的投入,因為即使它們不做這些事,有些客戶也必須要做。
在一些需要執行大量查詢的專案上,可以構建一個支援更靈活查詢的 Repository 框架,如圖。這要求開發人員熟悉必要的技術,而且一個支援性的基礎設施會提供巨大的幫助。
基於 Specification(規格)的查詢是將 Repository 通用化的好辦法。客戶可以使用規格來描述(也就是指定)它需要什麼,而不必關心如何獲取結果。在這個過程中,可以建立一個物件來實際執行篩選操作。
即使一個 Repository 的設計採取了靈活的查詢方式,也應該允許新增專門的寫死查詢。這些查詢作為便捷的方法,可以封裝常用查詢或不返回物件(如返回的是選中物件的我彙總計算)的查詢。不支援這些特殊查詢方式的框架可能會扭曲領域設計,或是乾脆被開發人員棄之不用。
持久化技術的封裝可以使得客戶變得十分簡單,並且使客戶與 Repository 的實現之間完全解耦。但像一般的封裝一樣,開發人員必須知道在封裝背後都發生了什麼事情。在使用 Repository 時,不同的使用方式或工作方式可能會對效能產生極大的影響。
3.3.2 Repository 的實現
根據所使用的持久化技術和基礎設施不同,Repository 的實現也將有很大的變化。理想的實現是向客戶隱藏所有內部的工作細節(儘管不向客戶的開發人員隱藏這些細節),這樣不管資料是儲存在物件資料庫中,還是儲存在關聯式資料庫中,或是簡單地保持在記憶體中,客戶程式碼都相同。Repository 將會委託相應的基礎設施服務來完成工作。將儲存、檢索和查詢機制封裝起來是 Repository 實現的最基本特性。
Repository 概念在很多情況下都適用。可能的實現方法很多,下面是一些需要謹記的注意事項:
(1)對型別進行抽象。Repository 「含有」特定型別的所有範例,但這並不意味著每個類都需要有一個 Repository。類可以是一個層次結構中的抽象超類。型別可以是一個介面——介面的實現者並沒有層次結構上的關聯,也可以是一個具體的類。由於資料庫技術缺乏這樣的多型性質,因此我們將面臨很多約束。
(2)充分利用與客戶解耦的優點。我們可以很容易地更改 Repository 的實現,但如果客戶直接呼叫底層機制,我們就很難修改其實現。也可以利用解耦來優化效能,因為這樣就可以使用不同的查詢技術,或在記憶體中快取物件,可以隨時自由地切換持久化策略。
(3)將事務的控制權留給客戶。儘管 Repository 會執行資料庫的插入和刪除操作,但它通常不會提交事務。例如,儲存資料後緊接著就提交似乎是自然的事情,但想必只有客戶才有上下文,從而能夠正確地初始化和提交工作單元。如果 Repository 不插手事務控制,那麼事務管理就會簡單得多。
通常,專案團隊會在基礎設施中新增框架,用來支援 Repository 的實現。Repository 超類除了與較低層的基礎設施元件進行共同作業以外,還可以實現一些基本查詢,特別是要實現的靈活查詢時。
3.3.3 Repository 與 Factory 的關係
Factory 負責處理物件生命週期的開始,而 Repository 幫助管理生命週期的中間和結束。當物件駐留在記憶體中或儲存在物件資料庫時,這是很好理解的。但通常至少有一部分物件儲存在關聯式資料庫、檔案或其他非物件導向的系統中。在這些情況下,檢索出來的資料必須被重建為物件形式。
由於在這種情況下 Repository 基於資料來建立物件,因此很多人認為 Repository 就是 Factory,而從技術角度來看的確如此。但是我們最好還是從模型的角度來看待這一問題,前面講過,重建一個已儲存的物件並不是建立一個新的概念物件。從領域驅動設計的角度來看,Factory 和 Repository 具有完全不同的職責。Factory 負責製造新的物件,而 Repository 負責查詢新的物件。Repository 應該讓客戶感覺到那些物件就好像駐留在記憶體中一樣。物件可能必須被重建,但它是同一個概念物件,仍舊處於生命週期的中間。
Repository 也可以委託 Factory 來建立一個物件,這種方法(雖然實際很少這樣做,但在理論上是可行的)可用於從頭開始建立物件。
這種職責上的明確區分還有助於 Factory 擺脫所有持久化職責。Factory 的工作是用資料來範例化一個可能很複雜的物件。如果產品是一個新物件,那麼客戶將知道建立完成之後應該把它新增到 Repository 中,由 Repository 來封裝物件在資料庫中的儲存。
另一種情況促使人們將 Factory 和 Repository 結合起來使用,這就是想要實現一種「查詢或建立」功能,即客戶描述它所需的物件,如果找不到這樣的物件,則為客戶新建立一個。我們最好不要追求這種功能,它不會帶來多少方便。當將 Entity 和 Value Object 區分開時,很多看上去有用的功能就不復存在了。需要 Value Object 的客戶可以直接請求 Factory 來建立一個。通常,在領域中將新物件和原有物件區分開是很重要的,而將它們組合在一起的框架實際上只會使局面變得混亂。
3.4 為關係型資料庫設計物件
在以物件導向為主的軟體系統中,最常用的非物件元件就是關聯式資料庫。這種現狀產生了混合使用正規化的常見問題。(待續)