EntityFramework之領域驅動設計實踐(七)

模型對象的生命週期 - 倉儲

上文中已經提到了管理領域模型對象生命週期的兩大角色,即工廠與倉儲,並對工廠的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值(也就是我們常說的實體鍵)是不變的,因此,在領域層看來,從倉儲中讀取得到的對象與當時保存進去的對象並沒有什麼兩樣。

你可能已經體會到,倉儲就是一個數據庫,它與數據庫一樣,有讀取、保存、查詢、刪除的操作。我只能說,你已經瞭解到倉儲的職能,並沒有瞭解到它的角色。倉儲是領域層與基礎結構層的一個銜接組件,領域層通過倉儲訪問外部存儲機制,這樣就使得領域層無需關心任何技術架構上的實現細節。因此,倉儲這個角色的職責不僅僅是讀取、保存、查詢、刪除,它還解耦了領域層與基礎結構層。在實踐中,可以使用依賴注入的方式,將倉儲實例注入到領域層,從而獲得靈活的體系結構。

下面是我們案例中,倉儲接口的代碼:

隱藏行號 複製代碼 倉儲接口
  1. public interface IRepository<TEntity>
    
  2.     where TEntity : EntityObject, IAggregateRoot
    
  3. {
    
  4.     void Add(TEntity entity);
    
  5.     TEntity GetByKey(int id);
    
  6.     IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec);
    
  7.     void Remove(TEntity entity);
    
  8.     void Update(TEntity entity);
    
  9. }
    
  10. 
    

IRepository是一個泛型接口,泛型類型被where子句限定爲EntityFramework中的EntityObject,與此同時,where子句還限定了泛型類型必須實現IAggregateRoot接口。換句話講,IRepository接口的泛型類型必須是繼承於EntityObject類,並實現了IAggregateRoot接口的引用類型。根據我們在 “聚合”一文中的表述,我們可以實現針對Customer、Order以及Category實體類的倉儲類。

這裏只給出了倉儲實現的一個引子,至少到目前爲止我們已經簡單地定義了倉儲實現的一個框架,也就是上面這個IRepository泛型接口。接口中具體要包括哪些方法,不是本系列文章要討論的關鍵問題。爲了描述與演示,我們只爲IRepository接口設計如上四個方法,即Add、GetByKey、Remove和Update。接下來,我將詳細描述在基於實體框架(EntityFramework)的倉儲設計中所遇到的困難,以及如何在實踐中解決這些困難。

 

-----【以下爲原文網友評論及回覆信息】-----
 

Re:實體框架之領域驅動實踐(七)

[ 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:實體框架之領域驅動實踐(七)

[ 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:實體框架之領域驅動實踐(七)

[ 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。


出自:http://www.cnblogs.com/daxnet/archive/2010/07/07/1772638.html

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