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

倉儲的實現:基本篇

我們先從技術角度考慮倉儲的問題。實體框架(EntityFramework)中,操作數據庫是非常簡單的:在ObjectContext中使用LINQ to Entities即可完成操作。開發人員也不需要爲事務管理而操心,一切都由EF包辦。與原本的ADO.NET以及LINQ to SQL相比,EF更爲簡單,LINQ to Entities的引入使得軟件開發變得更爲“領域化”。
下面的代碼測試了持久化一個 Customer實體,並從持久化機制中查詢這個Customer實體的正確性。從代碼中可以看到,我們用了一種很自然的表達方式,表述了“我希望查詢一個名字爲Sunny的客戶”這樣一種業務邏輯。

隱藏行號 複製代碼 FindCustomerTest
  1. [TestMethod]
    
  2. public void FindCustomerTest()
    
  3. {
    
  4.     Customer customer = Customer.CreateCustomer("daxnet", "12345",
    
  5.         new Name { FirstName = "Sunny", LastName = "Chen" },
    
  6.         new Address(), new Address(), DateTime.Now.AddYears(-29));
    
  7.     using (EntitiesContainer ec = new EntitiesContainer())
    
  8.     {
    
  9.         ec.Customers.AddObject(customer);
    
  10.         ec.SaveChanges();
    
  11.     }
    
  12.     using (EntitiesContainer ec = new EntitiesContainer())
    
  13.     {
    
  14.         var query = from cust in ec.Customers
    
  15.                     where cust.Name.FirstName.Equals("Sunny")
    
  16.                     select cust;
    
  17.         Assert.AreNotEqual(0, query.Count());
    
  18.     }
    
  19. }
    
  20. 
    

如果你需要實現的系統並不複雜,那麼按上面的方式添加、查詢實體也不會有太大問題,你可以在 ObjectContext中隨心所欲地使用LINQ to Entities來方便地得到你需要的東西,更讓人興奮的是,.NET 4.0允許支持並行計算的PLINQ,如果你的計算機具有多核處理器,你將非常方便地獲得效率上的提升。然而,當你的架構需要考慮下面幾個方面時,單純的 LINQ to Entities方式就無法滿足需求了:

  1. 領域模型與技術架構分離。這是DDD的一貫宗旨,也就是說,領域模型中是不能混入任何技術架構實現的,業務和技術必須嚴格分離。考察以上實現,領域模型緊密依賴於實體框架,而目前實體框架並非是完全領域驅動的,它更偏向於一種技術架構。比如上面的Customer實體,在實體框架驅動的設計中,它已經被EF“牽着鼻子走”了
  2. 規約(Specification)模式的引入。以上實現中,雖然LINQ使得業務邏輯的表述方式更爲“領域化”,可以看成是一種 Domain Specific Language(Microsoft Dynamics AX早已引入了類似的語言集成的語法),但這種做法會使得模型對領域概念的描述變得難以更改。比如:可以用“from employee in employees where employee.Age >= 60 && employee.Gender.Equals(Gender.Male) select employee”來表述“找出所有男性退休職工”的概念,但這種邏輯是寫死在領域模型中的,倘若某天男性退休的年齡從60歲調至55歲,那麼上面的查詢就不正確了,此時不得不對領域模型作修改。更可怕的是,LINQ to Entities仍然沒有避免“SQL everywhere”的難處,領域模型中將到處充斥這這種LINQ查詢,弊端也不多說了。解決方法就是引入規約模式
  3. 倉儲實現的可擴展性。比如如果經過系統分析,發現今後可能需要用到其它的持久化解決方案,那麼你就不能直接使用實體框架

於是,也就回到了上篇博客中我描述的問題:倉儲不是Data Object,也不僅僅是進行數據庫CRUD操作的Data Manager,它承擔瞭解耦領域模型和技術架構的重要職責。爲了完美地解決上面提到的問題,我們仍然採用領域驅動設計中倉儲的設計模式,而將實體框架作爲倉儲的具體實現部分。在詳細介紹倉儲的設計與實現之前,讓我們回顧一下上文最後部分我提到的那個倉儲的接口:

隱藏行號 複製代碼 IRepository
  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. 
    

在本文的案例中,倉儲是這樣實現的:

  1. 將上述倉儲接口定義在實體、值對象和服務所在的領域層。有朋友問過我,既然倉儲需要與外部存儲機制打交道,那麼它必定需要知道技術架構方面的細節,而將其定義在領域層,就會使得領域層依賴於具體的技術實現方式,這樣就會使領域層變得“不純淨” 了。其實不然!請注意,我們這裏僅僅只是將倉儲的接口定義在了領域層,而不是倉儲的具體實現(Concrete Repository)。更通俗地說,接口作爲系統架構的基礎元素,決定了整個系統的架構模式,而基於接口的具體實現只不過是一種可替換的組件,它不能成爲系統架構中的一部分。由於領域層需要用到倉儲,我便將倉儲的接口定義在了領域層。當然,從.NET的實現技術考慮,你可以新建一個Class Library,並將上述接口定義在這個Class Library中,然後在領域層和倉儲的具體實現中分別引用這個Class Library
  2. 新建一個Class Library(在本文的案例中,命名爲EasyCommerce.Infrastructure.Repositories),添加對領域層 assembly的引用,並實現上述接口。由於我們採用實體框架作爲倉儲的具體實現,因此,將這個倉儲命名爲EdmRepository(Entity Data Model Repository)。EdmRepository有着類似如下的實現:
    隱藏行號 複製代碼 EdmRepository實現
    1. internal class EdmRepository<TEntity> : IRepository<TEntity>
      
    2.     where TEntity : EntityObject, IAggregateRoot
      
    3.  {
      
    4.     #region Private Fields
      
    5.         private readonly ObjectContext objContext;
      
    6.         private readonly string entitySetName;
      
    7.         #endregion
      
    8.  
      
    9.     #region Constructors
      
    10.     /// <summary>
      
    11.     /// 
      
    12.     /// </summary>
      
    13.     /// <param name="objContext"></param>
      
    14.     public EdmRepository(ObjectContext objContext)
      
    15.     {
      
    16.         this.objContext = objContext;
      
    17. 
      
    18.         if (!typeof(TEntity).IsDefined(typeof(AggregateRootAttribute), true))
      
    19.             throw new Exception();
      
    20. 
      
    21.         AggregateRootAttribute aggregateRootAttribute = (AggregateRootAttribute)typeof(TEntity)
      
    22.             .GetCustomAttributes(typeof(AggregateRootAttribute), true)[0];
      
    23. 
      
    24.         this.entitySetName = aggregateRootAttribute.EntitySetName;
      
    25.     }
      
    26.     #endregion
      
    27.  
      
    28.     #region IRepository<TEntity> Members
      
    29. 
      
    30.         public void Add(TEntity entity)
      
    31.         {
      
    32.             this.objContext.AddObject(EntitySetName, entity);
      
    33.         }
      
    34. 
      
    35.         public TEntity GetByKey(int id)
      
    36.         {
      
    37.             string eSql = string.Format("SELECT VALUE ent FROM {0} AS ent WHERE ent.Id=@id", EntitySetName);
      
    38.             var objectQuery = objContext.CreateQuery<TEntity>(eSql,
      
    39.                 new ObjectParameter("id", id));
      
    40.             if (objectQuery.Count() > 0)
      
    41.                 return objectQuery.First();
      
    42.             throw new Exception("Not found");
      
    43. 
      
    44.         }
      
    45. 
      
    46.         public void Remove(TEntity entity)
      
    47.         {
      
    48.             this.objContext.DeleteObject(entity);
      
    49.         }
      
    50. 
      
    51.         public void Update(TEntity entity)
      
    52.         {
      
    53.             // TODO
      
    54.         }
      
    55. 
      
    56.         public IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec)
      
    57.         {
      
    58.             throw new NotImplementedException();
      
    59.         }
      
    60. 
      
    61.         #endregion
      
    62.  
      
    63.     #region Protected Properties
      
    64.         protected string EntitySetName
      
    65.         {
      
    66.             get { return this.entitySetName; }
      
    67.         }
      
    68. 
      
    69.         protected ObjectContext ObjContext
      
    70.         {
      
    71.             get { return this.objContext; }
      
    72.         }
      
    73.         #endregion
      
    74. }
      
    75. 
      

    從上面的代碼可以看到,EdmRepository將實體框架抽象到ObjectContext這一層,這也使我們沒法通過LINQ to Entities來查詢模型中的對象。幸運的是,ObjectContext爲我們提供了一系列函數,用以實現實體的CRUD。爲了使用這些函數,我們需要知道與實體相關的EntitySetName,爲此,我定義了一個AggregateRootAttribute,並將其應用在聚合根上,以便在對實體進行操作的時候,能夠正確地獲得EntitySetName。類似的代碼如下:

    隱藏行號 複製代碼 Customer Partial Class
    1. [AggregateRoot("Customers")]
      
    2. partial class Customer : IAggregateRoot
      
    3. {
      
    4.     
      
    5. }
      
    6. 
      

    回頭來看EdmRepository的構造函數,在構造函數中,我們使用.NET的反射機制獲得了定義在聚合根類型上的EntitySetName

     

  3. 使用IoC/DI(控制反轉/依賴注入)框架,將倉儲的實現(EdmRepository)注射到領域模型中。至此,領域模型一直保持着對倉儲接口的引用,而對倉儲的具體實現方式一無所知。由於IoC/DI的引入,我們得到了一個純淨的領域模型。在這裏我也想提出一個衡量系統架構優劣度的重要指標,就是領域模型的純淨度。常見的 IoC/DI框架有Spring.NET和Castle Windsor MicroKernel。在本文的案例中,我採用了Castle Windsor。以下是針對Castle Windsor的配置文件片段:
    隱藏行號 複製代碼 Customer Partial Class
    1. <castle>
      
    2.   <components>
      
    3.     <!-- Object Context for Entity Data Model -->
      
    4.     <component id="ObjectContext" 
      
    5.                service="System.Data.Objects.ObjectContext, System.Data.Entity, Version=4.0.0.0, Culture=neutral,
      
    6.                type="EasyCommerce.Domain.Model.EntitiesContainer, EasyCommerce.Domain"/>
      
    7. 
      
    8.     <component id="CustomerRepository"
      
    9.                service="EasyCommerce.Domain.IRepository`1[[EasyCommerce.Domain.Model.Customer, EasyCommerce.Doma
      
    10.                type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepository`1[[EasyCommerce.Doma
      
    11.       <objContext>${ObjectContext}</objContext>
      
    12.     </component>
      
    13.     
      
    14.   </components>
      
    15. </castle>
      
    16. 
      

    通過這個配置片段我們還可以看到,在框架創建針對“客戶”實體的倉儲實例時,我們案例中的領域模型容器(EntitiesContainer)也以構造器注入的方式,被注射到了EdmRepository的構造函數中。接下來我們做一個單體測試:

    161914254244
    考察上面的代碼,倉儲的使用者(Client,可以是領域模型中的任何對象)對倉儲的具體實現一無所知

總結 

總之,倉儲的實現可以用下圖表述:

191014782220

回頭來看本文剛開始的三個問題:依賴注入可以解決問題1和3,而倉儲接口的引入,也使得規約模式的應用成爲可能。.NET中有一個泛型委託,稱爲Func<T, bool>,它可以作爲LINQ的where子句參數,實現類似規約的功能。有關規約模式,我將在其它的文章中討論。

從本文還可以瞭解到,依賴注入是維持領域模型純淨度的一大利器;另一大利器是領域事件,我將在後續的文章中詳述。對於本文開始的第三個問題,也就是倉儲實現的可擴展性,將在下篇文章中進行討論,包括的內容有:事務處理和可擴展的倉儲框架的實現。

 

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

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

[ 2010-2-24 11:22:00 | By: 文野(遊客) ]

你好,我同意衡量系統架構優劣度的重要指標,就是領域模型的純淨度這個觀點。但我一直有一些疑惑,很多ORM框架,或象上面例子中,給領域對象添加一些Attribute,作爲實體在持久化或其它方面的指導,這些Attribute,是不是使得領域模型變得不夠純淨了?象上面的[AggregateRoot(“Customers”)],至少從感覺上覺得,它跟領域模型或者說業務領域一點關係都沒有。如果要得到更純淨的領域模型,是不是可以加一層領域模型與數據持久模型的映射,這樣會不會好點?

以下爲blog主人的回覆:
如果你是使用NHibernate,那麼,你就需要在外部維護一個xml文件,用來表述實體與外部數據表的映射關係。這個xml文件打斷了領域模型與 NHibernate的耦合,那麼在使用的時候,你就可以根據本文介紹的辦法,使用NHibernate的Session實現你自己的倉儲,同樣,使用依賴注入將其注射到領域模型中。
的確有不少具有ORM類似功能的框架,在領域模型中使用了Attribute以確定對象的持久化方式,經典的代表作是Castle ActiveRecord以及微軟的LINQ to SQL。在這些案例中,所使用的Attribute直接定義了數據持久化方面的細節,也如你所說,打破了領域模型的純淨度。
在本文的案例中,AggregateRootAttribute與那些框架中所使用的Attribute不同,AggregateRoot是領域模型中的概念,而與具體的技術實現無關。換句話說,AggregateRootAttribute並沒有去“指使”整個領域模型去做某些與領域無關的事情。如果你的倉儲實現不需要用到這個Attribute(比如NHibernate的實現),你完全可以對其置之不理。
AggregateRootAttribute和IAggregateRoot接口一起,用以表述“被修飾的類指代的是一種聚合根”的概念,它跟具體的技術架構實現並無太大關係。

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

[ 2010-2-26 16:03:00 | By: haojie77 ]

"EdmRepository將實體框架抽象到ObjectContext這一層,這也使我們沒法通過LINQ to Entities來查詢模型中的對象." 從現在的實現方式看, 擁有ObjectContext的CustomerRepository是否被一直緩存起來了? 這是否意味着數據庫的鏈接始終保持着? 目前的EdmRepository只能進行聚合根的CRUD操作, 如果要獲得聚合中的其他實體(勢必要使用linq to entity的操作, 若不對請加以指正), 要在哪裏實現呢? 是否在EdmRepository中追加定義一些specification的查詢? (在<<NET.Domain.Driven.Design.with.C#>>中聚合中的其他實體也是從該聚合根的 Repository來獲得.) 非常期待您之後的講座!

以下爲blog主人的回覆:
首先,本文所討論的只是倉儲的一種鬆耦合實現方式,有關ObjectContext生命週期的問題,我會在下一講中闡述。因爲倉儲的操作是事務性的,在下一講我會引入“事務上下文”(Transaction Context)的概念,你就會明白事務上下文是如何管理ObjectContext的了;其次,要獲得聚合內部的實體,只能通過聚合根來實現:1、通過 Repository獲得聚合根實體;2、使用聚合根實體的屬性、方法直接獲得聚合內的實體。比如:
IRepository<Customer> custRep = container.GetService<IRepository<Customer>>(); Customer sunny = custRep.GetBySpecification(new NameSpecification("Sunny Chen")); sunny.Address = xxxx;
於是,這個Address對象就是通過sunny這個聚合根實體來獲得的。我想想這個跟你所說的<<.NET DDD>>一書中的描述是一樣的。

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

[ 2010-2-28 0:14:00 | By: ruson(遊客) ]

按您文中所述,領域模型中用到了倉儲,這裏項目如果分開的話就會有相互引用,因爲倉庫實現中會用到領域Domain。雖然用依賴注入可以解決。但覺得這本身已經不太合理了。
領域模型應該是純淨的,只關注領域業務,而不需要知道有倉儲的存在。倉儲應該由service去調用,service調用倉儲來做領域的持久化操作。

以下爲blog主人的回覆:
對於領域模型是否能夠直接使用倉儲,國外很多領域驅動的社區對此有很大的爭議。有的人認爲,領域模型不應該直接使用倉儲,有的人認爲,這樣做並沒有什麼不妥,事實上也有類似的大型成功案例,在領域模型中直接使用倉儲實現實體持久化。所以這並沒有一個絕對的標準。DDD是一種實踐指導,而不是一種絕對的理論。所以,在目前需求的界定下,只要合理,就是好的架構。
此外,關於Service,DDD將Service分爲三種:Application Service,Domain Service以及Infrastructure Service。Evans在他的DDD一書中有個很好的例子,詳細解釋了三者的區別。你所說的應該是Domain Service,既然是Domain Service,那麼他也是領域層的一部分,因此,是由Service來調用倉儲,還是由實體本身來調用,已經變得不重要了。
Service用以處理依靠單個實體無法完成的領域邏輯,它也需要關注領域模型中的方方面面。如果實體不允許使用倉儲,那麼僅僅由Service去使用也會變得不合理。

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

[ 2010-2-28 0:21:00 | By: ruson(遊客) ]

沒有最好的架構,只有最合理的架構。當系統不復雜且大部分都是對單一表的CURD操作時,領域驅動就變得像貧血模型,感覺是個雞肋。

以下爲blog主人的回覆:
對於軟件架構,我一直都持有保留意見,我主張不要過分的去強調某些技術或者某些概念,以使其能夠成爲一種所謂合理的架構。軟件開發的整個過程都是需求驅動的,需求決定架構這是毋庸置疑的。此外,本系列文章所討論的都是基於領域驅動思想下的軟件架構實踐,當然不會適用於所有的軟件系統。如果你的軟件簡單到只需要輸出一行Hello World,你甚至連面向對象都不需要了。

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

[ 2010-3-1 16:10:00 | By: haojie77 ]

您說的這個Address只是一個Customer的屬性. 我想知道的是如何得到某個特殊的在Customer聚合中的實體.
根據"2、使用聚合根實體的屬性、方法直接獲得聚合內的實體。" 那麼如果是要查找某一個Customer的Order, 您的意思是否是可以直接在Customer的GetOrder(spec)內 調用linq to entity?(這樣做是否把技術相關的東西引入了Domain中?) 還是GetOrder內調用領域服務CustomerService.GetOrder(spec), 在其內再調用CustomerRepository.GetOrder(spec)? (因爲之前有看到您也說不推薦讓Entity直接使用Repository, 應該讓Entity通過DomainService間接地去調用Repository.)

以下爲 blog主人的回覆:
你首先要知道,Address雖然是一個屬性,但是從領域驅動上看,它是一個值對象。聚合包含的不僅僅全都是實體,還包含着值對象。這裏我的例子就是通過聚合根訪問聚合內部對象的方法。
針對你的問題,你完全可以直接在Customer上創建所需的方法來達到你的目的,沒有必要引入Service,也不需要牽涉到Respository,因爲聚合的另一層意義就是界定持久化的範圍。換句話說,聚合中的所有對象都是“同時有,同時無”的。
比如,你可以創建這樣的方法來達到你的需求:

class Customer
{
List<SalesOrder> GetUndeliveredOrders(SalesOrderSpecification spec);
}

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