程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> EntityFramework之領域驅動設計實踐(八):倉儲的實現:基本篇

EntityFramework之領域驅動設計實踐(八):倉儲的實現:基本篇

編輯:關於.NET

我們先從技術角度考慮倉儲的問題。實體框架(EntityFramework)中,操作數據庫是非常簡單的:在ObjectContext中使用 LINQ to Entities即可完成操作。開發人員也不需要為事務管理而操心,一切都由EF包辦。與原本的ADO.NET以及LINQ to SQL相比,EF更為簡單,LINQ to Entities的引入使得軟件開發變得更為“領域化”。

下面的代碼測試了持久化一個 Customer實體,並從持久化機制中查詢這個Customer實體的正確性。從代碼中可以看到,我們用了一種很自然的表達方式,表述了“我希望查詢一個名字為Sunny的客戶”這樣一種業務邏輯。

FindCustomerTest

[TestMethod]
public void FindCustomerTest()
{
   Customer customer = Customer.CreateCustomer("daxnet", "12345",
     new Name { FirstName = "Sunny", LastName = "Chen" },
     new Address(), new Address(), DateTime.Now.AddYears(-29));
   using (EntitiesContainer ec = new EntitiesContainer())
   {
     ec.Customers.AddObject(customer);
     ec.SaveChanges();
   }
   using (EntitiesContainer ec = new EntitiesContainer())
   {
     var query = from cust in ec.Customers
           where cust.Name.FirstName.Equals("Sunny")
           select cust;
     Assert.AreNotEqual(0, query.Count());
   }
}

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

領域模型與技術架構分離。這是DDD的一貫宗旨,也就是說,領域模型中是不能混入任何技術架構實現的,業務和技術必須嚴格分離。考察以上實現,領域模型緊密依賴於實體框架,而目前實體框架並非是完全領域驅動的,它更偏向於一種技術架構。比如上面的Customer實體,在實體框架驅動的設計中,它已經被EF“牽著鼻子走”了

規約(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查詢,弊端也不多說了。解決方法就是引入規約模式

倉儲實現的可擴展性。比如如果經過系統分析,發現今後可能需要用到其它的持久化解決方案,那麼你就不能直接使用實體框架

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

IRepository

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);
}

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

將上述倉儲接口定義在實體、值對象和服務所在的領域層。有朋友問過我,既然倉儲需要與外部存儲機制打交道,那麼它必定需要知道技術架構方面的細節,而將其定義在領域層,就會使得領域層依賴於具體的技術實現方式,這樣就會使領域層變得“不純淨” 了。其實不然!請注意,我們這裡僅僅只是將倉儲的接口定義在了領域層,而不是倉儲的具體實現(Concrete Repository)。更通俗地說,接口作為系統架構的基礎元素,決定了整個系統的架構模式,而基於接口的具體實現只不過是一種可替換的組件,它不能成為系統架構中的一部分。由於領域層需要用到倉儲,我便將倉儲的接口定義在了領域層。當然,從.NET的實現技術考慮,你可以新建一個Class Library,並將上述接口定義在這個Class Library中,然後在領域層和倉儲的具體實現中分別引用這個Class Library

新建一個Class Library(在本文的案例中,命名為EasyCommerce.Infrastructure.Repositories),添加對領域層 assembly的引用,並實現上述接口。由於我們采用實體框架作為倉儲的具體實現,因此,將這個倉儲命名為EdmRepository(Entity Data Model Repository)。EdmRepository有著類似如下的實現:

EdmRepository實現

internal class EdmRepository<TEntity> : IRepository<TEntity>
   where TEntity : EntityObject, IAggregateRoot
  {
   #region Private Fields
     private readonly ObjectContext objContext;
     private readonly string entitySetName;
     #endregion

   #region Constructors
   /// <summary>
   /// 
   /// </summary>
   /// <param name="objContext"></param>
   public EdmRepository(ObjectContext objContext)
   {
     this.objContext = objContext;

     if (!typeof(TEntity).IsDefined(typeof(AggregateRootAttribute), true))
       throw new Exception();

     AggregateRootAttribute aggregateRootAttribute = (AggregateRootAttribute)typeof(TEntity)
       .GetCustomAttributes(typeof(AggregateRootAttribute), true)[0];

     this.entitySetName = aggregateRootAttribute.EntitySetName;
   }
   #endregion

   #region IRepository<TEntity> Members

     public void Add(TEntity entity)
     {
       this.objContext.AddObject(EntitySetName, entity);
     }

     public TEntity GetByKey(int id)
     {
       string eSql = string.Format("SELECT VALUE ent FROM {0} AS ent WHERE ent.Id=@id", EntitySetName);
       var objectQuery = objContext.CreateQuery<TEntity>(eSql,
         new ObjectParameter("id", id));
       if (objectQuery.Count() > 0)
         return objectQuery.First();
       throw new Exception("Not found");

     }

     public void Remove(TEntity entity)
     {
       this.objContext.DeleteObject(entity);
     }

     public void Update(TEntity entity)
     {
       // TODO
     }

     public IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec)
     {
       throw new NotImplementedException();
     }

     #endregion

   #region Protected Properties
     protected string EntitySetName
     {
       get { return this.entitySetName; }
     }

     protected ObjectContext ObjContext
     {
       get { return this.objContext; }
     }
     #endregion
  }

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

Customer Partial Class

[AggregateRoot("Customers")]
partial class Customer : IAggregateRoot
  {

}

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

使用IoC/DI(控制反轉/依賴注入)框架,將倉儲的實現(EdmRepository)注射到領域模型中。至此,領域模型一直保持著對倉儲接口的引用,而對倉儲的具體實現方式一無所知。由於 IoC/DI的引入,我們得到了一個純淨的領域模型。在這裡我也想提出一個衡量系統架構優劣度的重要指標,就是領域模型的純淨度。常見的 IoC/DI框架有Spring.NET和Castle Windsor MicroKernel。在本文的案例中,我采用了Castle Windsor。以下是針對Castle Windsor的配置文件片段:

Customer Partial Class

<castle>
  <components>
   <!-- Object Context for Entity Data Model -->
   <component id="ObjectContext"
         service="System.Data.Objects.ObjectContext, System.Data.Entity, Version=4.0.0.0, Culture=neutral,
         type="EasyCommerce.Domain.Model.EntitiesContainer, EasyCommerce.Domain"/>

   <component id="CustomerRepository"
         service="EasyCommerce.Domain.IRepository`1[[EasyCommerce.Domain.Model.Customer, EasyCommerce.Doma
         type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepository`1[[EasyCommerce.Doma
    <objContext>${ObjectContext}</objContext>
   </component>

  </components>
</castle>

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

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

總結

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

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

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved