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

posted @ 2010-07-07 12:51 dax.net 阅读(...) 评论(...) 编辑 收藏