模式:REPOSITORY

目錄

 

模式:REPOSITORY

REPOSITORY的查詢

客戶代碼可以忽略REPOSITORY的實現,但開發人員不能忽略

REPOSITORY的實現

在框架內工作

REPOSITORY與FACTORY的關係

爲關係數據庫設計對象


模式:REPOSITORY

我們可以通過對象之間的關聯來找到對象。但當它處於生命週期的中間時,必須要有一個起點,以便從這個起點遍歷到一個ENTITY或VALUE。

無論要用對象執行什麼操作,都需要保持一個對它的引用。那麼如何獲得這個引用呢?一種方法是創建對象,因爲創建操作將返回對新對象的引用。第二種方法是遍歷關聯。我們以一個已知對象作爲起點,並向它請求一個關聯的對象。這樣的操作在任何面向對象的程序中都會大量用到,而且對象之間的這些鏈接使對象模型具有更強的表達能力。但我們必須首先獲得作爲起點的那個對象。

想到這種方法的人並不多(實現ENTITY和應用AGGREGATE),嘗試它的人就更少了,因爲人們將大部分對象存儲在關係數據庫中。這種存儲技術使人們自然而然地使用第三種獲取引用的方式——基於對象的屬性,執行查詢來找到對象;或者是找到對象的組成部分,然後重建它。

數據庫搜索是全局可訪問的,它使我們可以直接訪問任何對象。由此,所有對象不需要相互聯接起來,整個對象關係網就能夠保持在可控的範圍內。是提供遍歷還是依靠搜索,這成爲一個設計決策,需要在搜索的解耦與關聯的內聚之間做出權衡。Customer對象應該保持該客戶所有已訂的Order嗎?應該通過Customer ID字段在數據庫中查找Order嗎?恰當地結合搜索與關聯將會得到易於理解的設計。

遺憾的是,開發人員一般不會過多地考慮這種精細的設計,因爲他們滿腦子都是需要用到的機制,以便很有技巧地利用它們來實現對象的存儲、取回和最終刪除。

現在,從技術的觀點來看,檢索已存儲對象實際上屬於創建對象的範疇,因爲從數據庫中檢索出來的數據要被用來組裝新的對象。實際上,由於需要經常編寫這樣的代碼,我們對此形成了根深蒂固的觀念。但從概念上講,對象檢索發生在ENTITY生命週期的中間。不能只是因爲我們將Customer對象保存在數據庫中,而後把它檢索出來,這個Customer就代表了一個新客戶。爲了記住這個區別,我把使用已存儲的數據創建實例的過程稱爲重建。

領域驅動設計的目標是通過關注領域模型(而不是技術)來創建更好的軟件。假設開發人員構造了一個SQL查詢,並將它傳遞給基礎設施層中的某個查詢服務,然後再根據得到的錶行數據的結果集提取出所需信息,最後將這些信息傳遞給構造函數或FACTORY。開發人員執行這一連串操作的時候,早已不再把模型當作重點了。我們很自然地會把對象看作容器來放臵查詢出來的數據,這樣整個設計就轉向了數據處理風格。雖然具體的技術細節有所不同,但問題仍然存在——客戶處理的是技術,而不是模型概念。諸如METADATA MAPPING LAYER這樣的基礎設施可以提供很大幫助,利用它很容易將查詢結果轉換爲對象,但開發人員考慮的仍然是技術機制,而不是領域。更糟的是,當客戶代碼直接使用數據庫時,開發人員會試圖繞過模型的功能(如AGGREGATE,甚至是對象封裝),而直接獲取和操作他們所需的數據。這將導致越來越多的領域規則被嵌入到查詢代碼中,或者乾脆丟失了。雖然對象數據庫消除了轉換問題,但搜索機制還是很機械的,開發人員仍傾向於要什麼就去拿什麼。

客戶需要一種有效的方式來獲取對已存在的領域對象的引用。如果基礎設施提供了這方面的便利,那麼開發人員可能會增加很多可遍歷的關聯,這會使模型變得非常混亂。另一方面,開發人員可能使用查詢從數據庫中提取他們所需的數據,或是直接提取具體的對象,而不是通過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。技術基礎設施和數據庫訪問機制的暴露會增加客戶的複雜度,並妨礙模型驅動的設計。

有大量的技術可以用來解決數據庫訪問的技術難題,例如,將SQL封裝到QUERY OBJECT中,或利用METADATA MAPPING LAYER進行對象和表之間的轉換。FACTORY可以幫助重建那些已存儲的對象(本章後面將會討論)。這些技術和很多其他技術有助於控制數據庫訪問的複雜度。

有得必有失,我們應該注意失去了什麼。我們已經不再考慮領域模型中的概念。代碼也不再表達業務,而是對數據庫檢索技術進行操縱。REPOSITORY是一個簡單的概念框架,它可用來封裝這些解決方案,並將我們的注意力重新拉回到模型上。

REPOSITORY將某種類型的所有對象表示爲一個概念集合(通常是模擬的)。它的行爲類似於集合(collection),只是具有更復雜的查詢功能。在添加或刪除相應類型的對象時,REPOSITORY的後臺機制負責將對象添加到數據庫中,或從數據庫中刪除對象。這個定義將一組緊密相關的職責集中在一起,這些職責提供了對AGGREGATE根的整個生命週期的全程訪問。

客戶使用查詢方法向REPOSITORY請求對象,這些查詢方法根據客戶所指定的條件(通常是特定屬性的值)來挑選對象。REPOSITORY檢索被請求的對象,並封裝數據庫查詢和元數據映射機制。REPOSITORY可以根據客戶所要求的各種條件來挑選對象。它們也可以返回彙總信息,如有多少個實例滿足查詢條件。REPOSITORY甚至能返回彙總計算,如所有匹配對象的某個數值屬性的總和,如下圖所示。

REPOSITORY解除了客戶的巨大負擔,使客戶只需與一個簡單的、易於理解的接口進行對話,並根據模型向這個接口提出它的請求。要實現所有這些功能需要大量複雜的技術基礎設施,但接口很簡單,而且在概念層次上與領域模型緊密聯繫在一起。

因此:

爲每種需要全局訪問的對象類型創建一個對象,這個對象相當於該類型的所有對象在內存中的一個集合的“替身”。通過一個衆所周知的全局接口來提供訪問。提供添加和刪除對象的方法,用這些方法來封裝在數據存儲中實際插入或刪除數據的操作。提供根據具體條件來挑選對象的方法,並返回屬性值滿足查詢條件的對象或對象集合(所返回的對象是完全實例化的),從而將實際的存儲和查詢技術封裝起來。只爲那些確實需要直接訪問的AGGREGATE根提供REPOSITORY。讓客戶始終聚焦於模型,而將所有對象的存儲和訪問操作交給REPOSITORY來完成。

REPOSITORY有很多優點,包括:

  • 它們爲客戶提供了一個簡單的模型,可用來獲取持久化對象並管理它們的生命週期;
  • 它們使應用程序和領域設計與持久化技術(多種數據庫策略甚至是多個數據源)解耦;
  • 它們體現了有關對象訪問的設計決策;
  • 可以很容易將它們替換爲“啞實現”(dummy implementation),以便在測試中使用(通常使用內存中的集合)。

REPOSITORY的查詢

所有REPOSITORY都爲客戶提供了根據某種條件來查詢對象的方法,但如何設計這個接口卻有很多選擇。

最容易構建的REPOSITORY用硬編碼的方式來實現一些具有特定參數的查詢。這些查詢可以形式各異,例如,通過標識來檢索ENTITY(幾乎所有REPOSITORY都提供了這種查詢)、通過某個特定屬性值或複雜的參數組合來請求一個對象集合、根據值域(如日期範圍)來選擇對象,甚至可以執行某些屬於REPOSITORY一般職責範圍內的計算(特別是利用那些底層數據庫所支持的操作)。

儘管大多數查詢都返回一個對象或對象集合,但返回某些類型的彙總計算也符合REPOSITORY的概念,如對象數目,或模型需要對某個數值屬性進行求和統計。

在任何基礎設施上,都可以構建硬編碼式的查詢,也不需要很大的投入,因爲即使它們不這些事,有些客戶也必須要做。

在一些需要執行大量查詢的項目上,可以構建一個支持更靈活查詢的REPOSITORY框架。如下圖所示。這要求開發人員熟悉必要的技術,而且一個支持性的基礎設施會提供巨大的幫助。

基於SPECIFICATION(規格)的查詢是將REPOSITORY通用化的好辦法。客戶可以使用規格來描
述(也就是指定)它需要什麼,而不必關心如何獲得結果。在這個過程中,可以創建一個對象來
實際執行篩選操作。第9章將深入討論這種模式。

基於SPECIFICATION的查詢是一種優雅且靈活的查詢方法。根據所用的基礎設施的不同,它可能易於實現,也可能極爲複雜。Rob Mee和Edward Hieatt在[Fowler 2002]一書中探討了設計這樣的REPOSITORY時所涉及的更多技術問題。 

即使一個REPOSITORY的設計採取了靈活的查詢方式,也應該允許添加專門的硬編碼查詢。這些查詢作爲便捷的方法,可以封裝常用查詢或不返回對象(如返回的是選中對象的彙總計算)的查詢。不支持這些特殊查詢方式的框架有可能會扭曲領域設計,或是乾脆被開發人員棄之不用。

客戶代碼可以忽略REPOSITORY的實現,但開發人員不能忽略

持久化技術的封裝可以使得客戶變得十分簡單,並且使客戶與REPOSITORY的實現之間完全解耦。但像一般的封裝一樣,開發人員必須知道在封裝背後都發生了什麼事情。在使用REPOSITORY時,不同的使用方式或工作方式可能會對性能產生極大的影響。

Kyle Brown曾告訴過我他的一段經歷,有一次他被請去解決一個基於WebSphere的製造業應用程序的問題,當時這個程序正向生產環境部署。系統在運行幾小時後會莫名其妙地耗盡內存。Kyle在檢查代碼後發現了原因:在某一時刻,系統需要將工廠中每件產品的信息彙總到一起。開發人員使用了一個名爲all objects(所有對象)的查詢來進行彙總,這個操作對每個對象進行實例化,然後選擇他們所需的數據。這段代碼的結果是一次性將整個數據庫裝入內存中!這個問題在測試中並未發現,原因是測試數據較少。

這是一個明顯的禁忌,而一些更不容易注意到的疏忽可能會產生同樣嚴重的問題。開發人員需要理解使用封裝行爲的隱含問題,但這並不意味着要熟悉實現的每個細節。設計良好的組件是有顯著特徵的(這是第10章的重點之一)。

正如第5章所討論的那樣,底層技術可能會限制我們的建模選擇。例如,關係數據庫可能對複合對象結構的深度有實際的限制。同樣,開發人員要獲得REPOSITORY的使用及其查詢實現之間的雙向反饋。

REPOSITORY的實現

根據所使用的持久化技術和基礎設施不同,REPOSITORY的實現也將有很大的變化。理想的實現是向客戶隱藏所有內部工作細節(儘管不向客戶的開發人員隱藏這些細節),這樣不管數據是存儲在對象數據庫中,還是存儲在關係數據庫中,或是簡單地保持在內存中,客戶代碼都相同。REPOSITORY將會委託相應的基礎設施服務來完成工作。將存儲、檢索和查詢機制封裝起來是REPOSITORY實現的最基本的特性,如下圖所示。

REPOSITORY概念在很多情況下都適用。可能的實現方法有很多,這裏只能列出如下一些需要謹記的注意事項。

  • 對類型進行抽象。REPOSITORY“含有”特定類型的所有實例,但這並不意味着每個類都需要有一個REPOSITORY。類型可以是一個層次結構中的抽象超類(例如,TradeOrder可以是BuyOrder或SellOrder)。類型可以是一個接口——接口的實現者並沒有層次結構上的關聯,也可以是一個具體類。記住,由於數據庫技術缺乏這樣的多態性質,因此我們將面臨很多約束。
  • 充分利用與客戶解耦的優點。我們可以很容易地更改REPOSITORY的實現,但如果客戶直接調用底層機制,我們就很難修改其實現。也可以利用解耦來優化性能,因爲這樣就可以使用不同的查詢技術,或在內存中緩存對象,可以隨時自由地切換持久化策略。通過提供一個易於操縱的、內存中的(in-memory)啞實現,還能夠方便客戶代碼和領域對象的測試。
  • 將事務的控制權留給客戶。儘管REPOSITORY會執行數據庫的插入和刪除操作,但它通常不會提交事務。例如,保存數據後緊接着就提交似乎是很自然的事情,但想必只有客戶纔有上下文,從而能夠正確地初始化和提交工作單元。如果REPOSITORY不插手事務控制,那麼事務管理就會簡單得多。

通常,項目團隊會在基礎設施層中添加框架,用來支持REPOSITORY的實現。REPOSITORY超類除了與較低層的基礎設施組件進行協作以外,還可以實現一些基本查詢,特別是要實現的靈活查詢時。遺憾的是,對於類似Java這樣的類型系統,這種方法會使返回的對象只能是Object類型,而讓客戶將它們轉換爲REPOSITORY含有的類型。當然,如果在Java中查詢所返回的對象是集合時,客戶不管怎樣都要執行這樣的轉換。有關實現REPOSITORY的更多指導和一些支持性技術模式(如QUERY OBJECT)可以在[Fowler 2002]一書中找到。

在框架內工作

在實現REPOSITORY這樣的構造之前,需要認真思考所使用的基礎設施,特別是架構框架。這些框架可能提供了一些可用來輕鬆創建REPOSITORY的服務,但也可能會妨礙創建REPOSITORY的工作。我們可能會發現架構框架已經定義了一種用來獲取持久化對象的等效模式,也有可能定義了一種與REPOSITORY完全不同的模式。

例如,你的項目可能會使用J2EE。看看這個框架與MODEL-DRIVEN DESIGN的模式之間有哪些概念上近似的地方(記住,實體bean與ENTITY不是一回事),你可能會把實體bean和AGGREGATE根當作一對類似的概念。在J2EE框架中,負責對這些對象進行訪問的構造是EJB Home。但如果把EJB Home裝飾成REPOSITORY的樣子可能會導致其他問題。

一般來講,在使用框架時要順其自然。當框架無法切合時,要想辦法在大方向上保持領域驅動設計的基本原理,而一些不符的細節則不必過分苛求。尋求領域驅動設計的概念與框架中的概念之間的相似性。這裏的假設是除了使用指定框架之外沒有別的選擇。很多J2EE項目根本不使用實體bean。如果可以自由選擇,那麼應該選擇與你所使用的設計風格相協調的框架或框架中的一些部分。

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來創建一個。通常,在領域中將新對象和原有對象區分開是很重要的,而將它們組合在一起的框架實際上只會使局面變得混亂。

爲關係數據庫設計對象

在以面向對象技術爲主的軟件系統中,最常用的非對象組件就是關係數據庫。這種現狀產生了混合使用範式的常見問題(參見第5章)。但與大部分其他組件相比,數據庫與對象模型的關係要緊密得多。數據庫不僅僅與對象進行交互,而且它還把構成對象的數據存儲爲持久化形式。已經有大量的文獻對於如何將對象映射到關係表以及如何有效存儲和檢索它們這樣的技術挑戰進行了討論。最近的一篇討論可參見[Fowler 2002]一書。有一些相當完善的工具可用來創建和管理它們之間的映射。除了技術上的難點以外,這種不匹配可能對對象模型產生很大的影響。

有3種常見情況:

  1. 數據庫是對象的主要存儲庫;
  2. 數據庫是爲另一個系統設計的;
  3. 數據庫是爲這個系統設計的,但它的任務不是用於存儲對象。

如果數據庫模式(database schema)是專門爲對象存儲而設計的,那麼接受模型的一些限制是值得的,這樣可以讓映射變得簡單一點。如果在數據庫模式設計上沒有其他的要求,那麼可以精心設計數據庫結構,以便使得在更新數據時能更安全地保證聚合的完整性,並使數據更新變得更加高效。從技術上來看,關係表的設計不必反映出領域模型。映射工具已經非常完善了,足以消除二者之間的巨大差別。問題在於多個重疊的模型過於複雜了。MODEL-DRIVEN DESIGN的很多關於避免將分析和設計模型分開的觀點,也同樣適用於這種不匹配問題。這確實會犧牲一些對象模型的豐富性,而且有時必須在數據庫設計中做出一些折中(如有些地方不能規範化)。但如果不做這些犧牲就會冒另一種風險,那就是模型與實現之間失去了緊密的耦合。這種方法並不要必須使用一種簡單的、一個對象/一個表的映射。依靠映射工具的功能,可以實現一些聚合或對象的組合。但至關重要的是:映射要保持透明,並易於理解——能夠通過審查代碼或閱讀映射工具中的條目就搞明白。

  • 當數據庫被視作對象存儲時,數據模型與對象模型的差別不應太大(不管映射工具有多麼強大的功能)。可以犧牲一些對象關係的豐富性,以保證它與關係模型的緊密關聯。如果有助於簡化對象映射的話,不妨犧牲某些正式的關係標準(如規範化)。
  • 對象系統外部的過程不應該訪問這樣的對象存儲。它們可能會破壞對象必須滿足的固定規則。此外,它們的訪問將會鎖定數據模型,這樣使得在重構對象時很難修改模型。

另一方面,很多情況下數據是來自遺留系統或外部系統的,而這些系統從來沒打算被用作對象的存儲。在這種情況下,同一個系統中就會有兩個領域模型共存。第14章將深入討論這個問題。或許與另一個系統中隱含的模型保持一致有一定的道理,也可能更好的方法是使這兩個模型完全不同。

允許例外情況的另一個原因是性能。爲了解決執行速度的問題,有時可能需要對設計做出一些非常規的修改。

但大多數情況下關係數據庫是面向對象領域中的持久化存儲形式,因此簡單的對應關係纔是最好的。表中的一行應該包含一個對象,也可能還包含AGGREGATE中的一些附屬項。表中的外鍵應該轉換爲對另一個ENTITY對象的引用。有時我們不得不違背這種簡單的對應關係,但不應該由此就全盤放棄簡單映射的原則。

UBIQUITOUS LANGUAGE可能有助於將對象和關係組件聯繫起來,使之成爲單一的模型。對象中的元素的名稱和關聯應該嚴格地對應於關係表中相應的項。儘管有些功能強大的映射工具使這看上去有些多此一舉,但關係中的微小差別可能引發很多混亂。

對象世界中越來越盛行的重構實際上並沒有對關係數據庫設計造成多大的影響。此外,一些嚴重的數據遷移問題也使人們不願意對數據庫進行頻繁的修改。這可能會阻礙對象模型的重構,但如果對象模型和數據庫模型開始背離,那麼很快就會失去透明性。

最後,有些原因使我們不得不使用與對象模型完全不同的數據庫模式,即使數據庫是專門爲我們的系統創建的。數據庫也有可能被其他一些不對對象進行實例化的軟件使用。即使當對象的行爲快速變化或演變的時候,數據庫可能並不需要修改。讓模型與數據庫之間保持鬆散的關聯是很有吸引力的。但這種結果往往是無意爲之,原因是團隊沒有保持數據庫與模型之間的同步。如果有意將兩個模型分開,那麼它可能會產生更整潔的數據庫模式,而不是一個爲了與早前的對象模型保持一致而到處都是折中處理的拙劣的數據庫模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章