模型對象的生命週期 - 倉儲
上文中已經提到了管理領域模型對象生命週期的兩大角色,即工廠與倉儲,並對工廠的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。接下來,我將詳細描述在基於Entity Framework(EntityFramework)的倉儲設計中所遇到的困難,以及如何在實踐中解決這些困難。
| -----【以下為原文網友評論及回複資訊】----- |
| |
Re:Entity Framework之領域驅動實踐(七) [ 2010-2-2 14:00:00 | By: Webcopy(遊客) ] 這裡的 IRepository介面被where子句限定為EF中的EntityObject,這就與EF架構緊耦合了. 個人覺得IResository介面定義應該是在領域模型中,不應該與具體的架構或類庫關聯.試想,如果我用NHibernate對 IResository介面進行實現,就與EF無關了. 也許我理解的不正確..... 以下為blog主人的回複: 我個人的意見還是具體問題具體分析。NHibernate是不需要實體類去繼承任何類或者實現任何介面的,因此,在這裡將IRepository的泛型型別約束為EntityObject也無妨。換句話說,即使你的類繼承了EntityObject,NHibernate照樣可以幫你完成持久化等操作。但如果你所挑選的ORM不是NHibernate,那就視情況而定了。你完全可以定義一個IEntity的介面並將其用作泛型約束。由於C#的單類多介面繼承的特性,多出一個IEntity的介面並不會對你的設計造成任何影響。 你考慮問題的思路是正確的,至少你考慮到了架構的擴充性。當然,在實際中,這些基礎結構的技術架構是很少變動的,也就是說,一旦選用了EF,就不太可能在今後的系統生命週期中再去換其它的ORM,除非是有特定的需求。在接下來的兩篇博文中,我將介紹基於EF的倉儲實現方式,以及一種與EF無關的通用倉儲架構,在這兩篇文章中,你還將體會到依賴注入帶給我們的機遇。 (順便BS下自己,最近太忙了,一直沒有機會更新部落格,但我一定會將本系列文章寫完,敬請期待!) |
Re:Entity Framework之領域驅動實踐(七) [ 2010-2-11 11:07:00 | By: haojie77 ] Concrete Repository一般都定義在Infrastructure層, 而EF為我們產生的Entity Model又在Domain層, Domain依賴於Infrastucture, 這樣一來Concrete Repository是不是也要依賴於Domain中的Entity Model 來產生entity? 這樣的雙向依賴在.Net中貌似不行. 當我們需要得到一個符合某些特定條件的彙總根集合的時候(比如要顯示所有在中國並且姓"鄭"的Customer), 那麼CustomerRepository.FindBySpec(spec)中如何得到在Domain中定義的Customer? 如果在FindBySpec中只用根據spec拼接出來的sql那就沒有這個問題. 換種說法就是 CustomerRepository.FindBySpec的傳回型別如果是List<Customer>而不是 List<EntityObject>, Customer又由EF產生在Domain層中, 那麼如何解決這個問題? 還是說把CustomerRepository也定義在Domain層中?(我看到您的StoreDDD貌似就是這個意思 Domain.Repositories) 因為那時你用的是ActiveRecord實現的, "有違反DDD經驗的嫌疑"是否就是指把Repository定義在了Domain中? 現在如果用EF是否有辦法能解決這個問題? 我甚至想過把EF自動產生的EntityObject當作DTO放在Infrastructure中, 而自己在Domain層中去定義真正的Entity去繼承(或者是內聚更合理)EntityObject, 再給與它行為. 但是DDD中說Repository是用來喚醒Entity的, 這麼做只能是拿到一個靜態DTO仍舊需要在Domain的Service中去構建 ConcreteEntity(ConcreteEntityObject), 一樣有違反DDD經驗的嫌疑(Entity並不是由Repository得到). 這隻是我隨意想到的, 可能思路已經不對了. 以下為blog主人的回複: 你的問題非常有價值。首先應該認識到產生依賴的根源,就是你把Concrete Repository也當做是整個系統架構的一個部分。然而事實是,Concrete Repository是一個可替代組件。要解決你的問題,你需要引入三件事情:介面、泛型以及依賴注入。 首先,將Repository的介面定義在領域層,介面採用泛型定義,也就是類似我在本文中的定義方式。注意:在領域層裡的僅僅是一個介面而已,它跟技術架構沒有任何關係,就像領域事件(Domain Events)一樣,將一切與技術架構相關的具體實現委託給Concrete Repository,這樣仍然能夠保持領域模型的純淨度;其次,在定義Concrete Repository的Assembly上直接引用Domain Model,這樣做是合理的,因為Concrete Repository不是系統架構中的一部分,而僅僅是一個可替換的組件,因此其實現方式是可以任意的,不會影響整個系統架構;再次,使用依賴注入,將 Concrete Repository注入到Domain Model中,至此,Domain Model一直是在使用Repository的介面,而沒有關心這個介面的背後是否是Concrete Repository,當然,在你的Concrete Repository中也可以使用規約模式對對象進行篩選和驗證。常用的依賴注入架構有Spring.NET和Castle Windsor。 根據你的提問,我臨時開發了一個Sample,是:http://www.sunnychen.org/attachments /RepositoryStorm.rar。通過這個例子,你可以看到一個非常純淨的領域模型。 |
Re:Entity Framework之領域驅動實踐(七) [ 2010-2-11 12:55:00 | By: haojie77 ] 關於我說的把EF產生的 EntityObject放入Infrastructure中, 代碼上貌似還是可以實現. Entity Data Model 從名字上看,微軟本身對其的定義還是基於資料的. 所以我才想到把EF產生的Entity當做DTO, 這樣很多情況下也不用另外構建DTO,直接用EntityObject就可以了. 這裡可能扯得有點遠了, 也許我這個想法本身有不少疏漏和錯誤. 在這裡希望能學習到各方面的知識:) namespace DomainModel.Model { class Customer : RealEntity { public Customer(EntityObject eObject) : base(eObject) { } } } namsapce Infrasturcture.DomainBase { public abstract class RealEntity { private EntityObject _EntityObject; public RealEntity(EntityObject eObject) { _EntityObject = eObject; } } } namespace Infrasturcture.Repositories { class CustomerRespository<T> : IRepository<T> where T : RealEntity { public T FindBy(object key) { EntityObject eObject = null;// Use linq to entity to get it return Activator.CreateInstance(typeof(T), eObject) as T; } } } 以下為blog主人的回複: 你有這方面的考慮是很好的。我只能說是Microsoft對Entity的定義使你在理解上產生了偏差。如我前面的文章所述,EF最大的一個缺點就是不支援實體行為,即使有,也是硬生生地從SQL Stored Procedure來產生的,但這不能成為實體行為。於是,在EF中,Entity就成了充斥著getter/setter的資料對象了,也就是DDD所提到的貧血模型。 從我個人的角度,我仍然會將EntityObject放在領域層,這樣就不需要你定義的那個RealEntity類;EntityObject是要有行為的,才能參與領域活動,如何使其具有行為?我的解決方案在前文也有講述:使用C#的partial關鍵字。 通常情況下,DTO是用來在層與層之間交換資料用的,在DDD中屬於值對象,你將EntityObject用作DTO是不合適的,因為 EntityObject具有EF所給定的實體鍵,應該視為實體,既然是實體,就不能穿越層的界定線。我更偏向於在Domain和UI之間使用DTO,而不是Domain和Repository之間。因為Repository維護的是實體的生命週期,而不是DTO。 最後,關於您的Domain和Repository解耦的問題,我已經在您的上個評論中留下了一個案例的,您可以通過下載這個案例來瞭解如何在 DDD中引入Repository。 |