上文中已經提到了管理領域模型對象生命周期的兩大角色,即工廠與倉儲,並對工廠的EntityFramework實踐作了詳細的描述。本節主要介紹倉儲的概念,由於倉儲的內容比較多,我將在接下來的兩節中具體講解倉儲的架構設計與實踐經驗。
倉儲(Repository),顧名思義,就是一個倉庫,這個倉庫保存著領域模型的實體對象。在業務處理的過程中,我們有可能需要把正在參與處理過程的對象保存到倉儲中,也有可能會從倉儲中讀取需要的實體對象,抑或將對象直接從倉儲中刪除。上文也用一張簡要的狀態圖描述了倉儲在管理領域模型對象生命周期中所處的位置。
與工廠相同,倉儲的關注對象也應該是聚合根,而不是聚合中的某個實體,更不應該是值對象。或許你會說,我當然可以針對銷售訂單行(Order Line)進行增刪改查等操作,而無需跟銷售訂單(Sales Order)打交道。當然,你的確可以這樣做,但如果你一定要堅持自己的觀點,那麼你就是把銷售訂單行(Order Line)當成是聚合根了,也就是說,你默許Order Line在你的領域模型中,是一種具有獨立概念的實體。關於這個問題,在領域驅動設計的社區中,有人發表了更為“強勢”的觀點:
One interesting DDD rule is: you should create repositories only for aggregate roots.
When I read about it the first time I interpreted it this way: create repositories at least for all aggregate roots, but when you need a little repository for something else go ahead and implement it (and nobody will know what you did).
So I was thinking that the rule is somehow flexible. It turns out that it's not, and this is good: it keeps the domain stable and coherent. If entity A is an aggregate root, entity B is part of that aggregate, and you need to load B separated from the concept of A, this is a sign that the implementation does not reflect the business needs (anymore). In this case, B should probably become the root of its own aggregate
意思是說,如果實體A是聚合根,而B是該聚合中的一個實體,而你的設計希望繞過A而直接從倉儲中獲得B,那麼,這就是一個信號,預示著你的設計可能存在問題,也就是說,B很有可能被當成是另一個聚合的根,而這個聚合只有一個對象,就是B本身。由此看來,聚合的劃分與倉儲的設計,在領域驅動設計的實踐中是非常重要的內容。
工廠是從無到有地創建對象,從代碼上看,工廠裡充斥著new關鍵字,用以創建對象,當然,工廠的職責並不完全是new出一個對象那麼簡單。而倉儲則更偏向於對象的保存和獲得,在獲得的時候,同樣也會有新的對象產生,這個新的對象與保存進去的對象相比,引用不同了,但數據和業務ID值(也就是我們常說的實體鍵)是不變的,因此,在領域層看來,從倉儲中讀取得到的對象與當時保存進去的對象並沒有什麼兩樣。
你可能已經體會到,倉儲就是一個數據庫,它與數據庫一樣,有讀取、保存、查詢、刪除的操作。我只能說,你已經了解到倉儲的職能,並沒有了解到它的角色。倉儲是領域層與基礎結構層的一個銜接組件,領域層通過倉儲訪問外部存儲機制,這樣就使得領域層無需關心任何技術架構上的實現細節。因此,倉儲這個角色的職責不僅僅是讀取、保存、查詢、刪除,它還解耦了領域層與基礎結構層。在實踐中,可以使用依賴注入的方式,將倉儲實例注入到領域層,從而獲得靈活的體系結構。
下面是我們案例中,倉儲接口的代碼:
倉儲接口
public interface IRepository<TEntity>
where TEntity : EntityObject, IAggregateRoot
{
void Add(TEntity entity);
TEntity GetByKey(int id);
IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec);
void Remove(TEntity entity);
void Update(TEntity entity);
}
IRepository是一個泛型接口,泛型類型被where子句限定為EntityFramework中的EntityObject,與此同時,where子句還限定了泛型類型必須實現IAggregateRoot接口。換句話講,IRepository接口的泛型類型必須是繼承於 EntityObject類,並實現了IAggregateRoot接口的引用類型。根據我們在 “聚合”一文中的表述,我們可以實現針對Customer、Order以及Category實體類的倉儲類。
這裡只給出了倉儲實現的一個引子,至少到目前為止我們已經簡單地定義了倉儲實現的一個框架,也就是上面這個IRepository泛型接口。接口中具體要包括哪些方法,不是本系列文章要討論的關鍵問題。為了描述與演示,我們只為IRepository接口設計如上四個方法,即Add、 GetByKey、Remove和Update。接下來,我將詳細描述在基於實體框架(EntityFramework)的倉儲設計中所遇到的困難,以及如何在實踐中解決這些困難。