EntityFramework之领域驱动设计实践(六)

模型对象的生命周期 - 工厂

首先应该认识到,是对象就有生命周期。这一点无论在面向对象语言还是在领域驱动设计中都适用。在领域驱动设计中,模型对象生命周期可以简要地用下图表示:

28169563317

通过上图可以看到,对象通过工厂从无到有创建,创建后处于活动状态,此时可以参与领域层的业务处理;对象通过仓储实现持久化(也就是我们常说的“保存”)和重建(也就是我们常说的“读取”)。内存中的对象通过析构而消亡,处于持久化状态的对象则通过仓储进行撤销(也就是我们常说的“删除”)。整个状态转换过程非常清晰。

现在引出了管理模型对象生命周期的两种角色:工厂和仓储。同时也需要注意的是,工厂和仓储的操作都是基于聚合根(Aggregate Root)的,而不仅仅是针对实体的。关于仓储,内容会比较多,我在下一节单独讲述。在本节介绍一下工厂在.NET实体框架(EntityFramework)中的实现。

在打开了.NET实体框架自动生成的Entity Data Model Source Code文件后,我们发现,.NET实体框架为每一个实体添加了一个工厂方法,该方法包含了一系列原始数据类型和值类型的参数。比如,我们案例中的 Customer实体就有如下的代码:

隐藏行号 复制代码 Customer Factory
  1. /// <summary>
    
  2. /// Create a new Customer object.
    
  3. /// </summary>
    
  4. /// <param name="id">Initial value of the Id property.</param>
    
  5. /// <param name="name">Initial value of the Name property.</param>
    
  6. /// <param name="billingAddress">Initial value of the BillingAddress property.</param>
    
  7. /// <param name="deliveryAddress">Initial value of the DeliveryAddress property.</param>
    
  8. /// <param name="loginName">Initial value of the LoginName property.</param>
    
  9. /// <param name="loginPassword">Initial value of the LoginPassword property.</param>
    
  10. /// <param name="dayOfBirth">Initial value of the DayOfBirth property.</param>
    
  11. public static Customer CreateCustomer(global::System.Int32 id, Name name, Address billingAddress, 
  12.                                              Address deliveryAddress, global::System.String loginName, 
  13.                                              global::System.String loginPassword, global::System.DateTime dayOfBirth)
    
  14. {
    
  15.     Customer customer = new Customer();
    
  16.     customer.Id = id;
    
  17.     customer.Name = StructuralObject.VerifyComplexObjectIsNotNull(name, "Name");
    
  18.     customer.BillingAddress = StructuralObject.VerifyComplexObjectIsNotNull(billingAddress, "BillingAddress");
    
  19.     customer.DeliveryAddress = StructuralObject.VerifyComplexObjectIsNotNull(deliveryAddress, "DeliveryAddress");
    
  20.     customer.LoginName = loginName;
    
  21.     customer.LoginPassword = loginPassword;
    
  22.     customer.DayOfBirth = dayOfBirth;
    
  23.     return customer;
    
  24. }
    
  25. 
    

 

那么在创建一个Customer实体的时候,就可以使用 Customer.CreateCustomer工厂方法。看来.NET实体框架已经离领域驱动设计的思想比较接近了,下面有几点需要说明:

 

  • 使用该工厂方法创建Customer实体时,需要给第一个参数 “global::System.Int32 id”赋值,而实际上这个ID值是用在持久化机制上的,在实体对象被创建的时候,这个ID值不应该由开发人员指定。因此,在这里要开发人员强行指定一个 id值就显得多余。事实上,.NET实体框架中的每个实体都是继承于EntityObject类,而该类中有个EntityKey的属性,是被用作实体的 Key的,因此我们这里的ID值肯定是由持久化机制进行维护的。从这里也可以看出,领域驱动设计中的实体会有两个标识符:一个是基于业务架构的,另一个是基于技术架构的。拿销售订单打比方,我们从界面上看到的更多是类似“SO0029473858” 这样的标识符,而不是一个整数或者GUID
  • 该工厂方法能够创建一个Customer实体,为实体的各个成员属性赋值,并连带创建与该实体相关的值对象,聚合成员(比如 Customer的CreditCards)是在使用的时候进行创建并填充的,这样做既符合“对象创建应该基于聚合”的思想,又能提高系统性能。比如,下面的单体测试用来检测使用工厂创建的Customer对象,其CreditCards属性是否为null(如果为null,则证明聚合根并没有合理地维护聚合的完整性):
    301440134452
  • .NET实体框架仅仅为每个实体提供了一个最为简单的工厂方法。“工厂”的概念,在领域驱动设计中具有如下的最佳实践:
    • 工厂可以隐藏对象创建的细节,因为对象的创建不属于业务领域需要考虑的问题
    • 工厂用来创建整个聚合,从而维护聚合所代表的领域含义
    • 可以在聚合根中添加工厂方法,也可以使用工厂类。也就是说,可以创建一个CustomerFactory的类,在其中定义 CreateCustomer方法。具体是选用工厂方法还是工厂类,应该根据需求而定
    • 当需要对被创建的实体传入参数时,应该尽可能地减小耦合性,比如可以使用抽象类或者接口作为参数类型

到这里你会发现,工厂和仓储好像有这一种联系,即它们都能够创建对象,而区别在于,工厂是对象从无到有的创建,仓储则更偏向于“重建”。仓储要比工厂更为复杂,因为仓储需要跟持久化机制这一技术架构打交道。在接下来的文章中,我会介绍一种基于.NET实体框架,但又不被实体框架制约的仓储的实现方式。

 

 

-----【以下为原文网友评论及回复信息】-----
 

Re:实体框架之领域驱动实践(六)

[ 2009-12-31 11:48:00 | By: xiaos(游客) ]

感觉repository 职责应该是关注get和load,而factory则是create。

以下为blog主人的回复:
不错,文中我也提到了:“工厂是对象从无到有的创建,仓储则更偏向于“重建””。Repository需要与技术架构打交道,而Factory则不需要。

Re:实体框架之领域驱动实践(六)

[ 2010-2-9 21:34:00 | By: haojie77 ]

我最近也在学习DDD, 你这篇文章中提到的"从这里也可以看出,领域驱动设计中的实体会有两个标识符:一个是基于业务架构的,另一个是基于技术架构的。" 我有两个疑问.
1. 我的感觉在domain中,Order的标识符就应该是诸如“SO0029473858”的字符串. 而GUID只是为了在数据库存储而用到的(或者是EF必须要用到), 是否所谓的GUID根本不能算是领域概念中实体的标识符? 之前看到别人的帖子说切忌不要把标识符当成数据库主键, 一个entity是否可以有n个(n>=2)标识符?
2. 标识符和数据库主键应该不是一一对应的关系,是吗? 标识符往往映射到数据库主键, 但标识符不一定就是数据库主键. 我这样的理解对吗?
我刚开始学习DDD, 所以对一些概念不是很清晰,希望能从您这里学习到更多关于DDD的东西,很期待你之后的(八,九,十...),谢谢!

以下为blog主人的回复:
是的,一定要将数据库主键与实体键区分开来。原因很简单,前者是技术架构的内容,而后者则是业务领域。在领域模型中,具有相同实体键的两个对象可以认定为同一个实体。当然,完全可以把业务实体键用作数据库主键,比如:数据库里完全可以用“SO0029473858”这样的销售订单号作为 SalesTable的主键,但请注意了,如果是这样的话,你必须在技术的角度生成这个键值,然后在持久化实体之前将其赋给实体键。换句话说,当使用实体键用作数据库的主键时,维护这个键值的机制是你的系统本身,而不是数据库。这可能会对性能造成一定的影响,《领域驱动设计》一书中也提到过这个问题。相关的经典案例就是Microsoft Dynamics AX中的Number Sequence机制,它是一个可以深度扩展的序列号生成机制,在Dynamics AX中,诸如销售订单号等都是由这个机制生成并维护的。
“一个entity是否可以有n个标识符”?答案是否定的。只能有一个。标识符用来唯一标识一个独立的实体。我上面所说的两个值,其中一个是技术架构层面的,在领域模型中,一个实体只能有一个标识符。
不知我上面的回答是否能够帮到你。PS:最近没时间更新,不过我会争取尽快更新。

posted @ 2010-07-07 09:15  dax.net  阅读(18026)  评论(12编辑  收藏  举报