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

EntityFramework之领域驱动设计实践 - 前言程序员

EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject数据库

EntityFramework之领域驱动设计实践 (二):分层架构express

EntityFramework之领域驱动设计实践 (三):案例:一个简易的销售系统编程

EntityFramework之领域驱动设计实践 (四):存储过程 - 领域驱动的反模式设计模式

EntityFramework之领域驱动设计实践 (五):聚合缓存

EntityFramework之领域驱动设计实践 (六):模型对象的生命周期 - 工厂服务器

EntityFramework之领域驱动设计实践 (七):模型对象的生命周期 - 仓储网络

EntityFramework之领域驱动设计实践 (八):仓储的实现(基本篇)session

EntityFramework之领域驱动设计实践 (九):仓储的实现(深刻篇)数据结构

EntityFramework之领域驱动设计实践 (十):规约(Specification)模式

EntityFramework之领域驱动设计实践【扩展阅读】:服务(Services)

EntityFramework之领域驱动设计实践【扩展阅读】:CQRS体系结构模式

EntityFramework之领域驱动设计实践:总结

EntityFramework之领域驱动设计实践 - 前言

根据网友的讨论结果,以及本身在实践中的不断积累,在整理的过程当中,我会将原文中的描述做相应调整。不只如此,也有很多关心领域驱动设计的网友在原文的评论栏目中提了问题或做了批注,我也针对网友的问题给予了细致的答复,为了可以让更多的朋友了解到问题的本质,本次整理稿会将评论部分也一一列出,供你们参考。

EntityFramework

EntityFramework是微软继LINQ to SQL以后推出的一个更为完整的领域建模和数据持久化框架。初见于.NET Framework 3.5版本,4.0的.NET Framework已经集成了EntityFramework。使用.NET 4.0的朋友就不须要下载和安装额外的插件了。与LINQ to SQL相比,EntityFramework从概念上将系统设计的关注点从数据库驱动转移到模型/领域驱动上。

领域驱动设计(DDD)

领域驱动设计并非一门技术,也不是一种方法论。它是一种考虑问题的方式,是一种经验积累,它关注于那些处理复杂领域问题的软件项目。为了得到项目成功,团队须要具有一系列的设计实践、开发技术和开发准则。与此相关的技术与设计/代码重构也是领域驱动设计讨论的重点。

本系列文章就是着重讨论EntityFramework在领域驱动设计上的实践,也但愿DDD与.NET的爱好者可以从文中得到启发,将解决方案用在本身的实际项目中。

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

从DataTable到EntityObject

虽然从技术角度讲,DataTable与EntityObject并无什么可比性,然而,它暗示了一场革命正在悄然进行着,即便是微软,也摆脱不了这场革命的飓风。

软件设计思想须要革命,须要摆脱原有的思路,而走向面向领域的道路。你或许会以为听起来很玄乎,然而目前软件开发的现状使你不得不接受这样的现实,仍然有大帮的从业人员整天扯着数据库不放,仍然有大帮的人在问:“我要实现xxxx功能,个人数据库应该如何设计?”这些人犯了根本性的错误,就是把软件的目的搞错了,软件研究的是什么?是研究如何使用计算机来解决实际(领域)问题,而不是去研究数据应该如何保存更合理。这方面的事情我在我之前的博文中已经说过不少次了,在此就再也不重复了。

固然,不少读者会跟我有着相同的观点,也会以为我很“火星”,但这都没关系,上面我所说的都是一个引子,但愿可以帮助更多“步入歧途”的从业人员 “走上正路”。不错,软件设计应该从“数据库驱动”走向“领域驱动”,而领域驱动设计的实践经验正是为设计和开发大型复杂的软件系统提供了实践指导。

回到咱们的副标题,从DataTable到EntityObject,你看到了什么?看到的是微软在领域驱动上的进步,从DataTable这一纯粹的数据组织形式,到EntityObject这一实体对象,微软带给咱们的不只仅是技术框架,更是一套面向领域的解决方案。

.NET 4.0来了,随之而来的是实体框架(EntityFramework,简称“EF”),在本系列文章中,我将结合领域驱动设计的实践知识,来剖析EF的具体应用过程,固然,如今的EF还并非那么完善,我也很是期待可以看到,从此微软可以继续发展和完善EF,使其成为微软领域驱动工具箱中的重要角色。

先不说EF,首先咱们简要地分析一下,做为一种框架,要支持领域驱动的思想,须要知足哪些硬性需求,以后再仔细想一想,.NET EF是否真的可以很好地应用在领域驱动上。

  • 首先须要可以正确对待“领域模型”的概念。领域模型与数据模型不一样,它表述的是领域中各个类及其之间的关系。类关系是多样的,好比组合、聚合、继承、实现、约束等,而数据模型不是一对多,就是多对多。从领域驱动设计的角度看,数据库只不过是存储实体的一个外部机制,是属于技术层面的东西。数据模型主要用于描述领域模型对象的持久化方式,应该是先有领域模型,才有数据模型,领域模型须要经过某种映射而产生相应的数据模型。所以,框架必须支持从领域模型到数据模型的映射。
    EF不只支持从领域模型生成数据库的DDL,并且支持从数据库结构来生成“领域模型”。我却是以为后者能够去掉,由于从数据库获得的已经不是领域模型了。你会问为何,我能够告诉你,单纯的数据是没办法描述领域概念的。好比:你的数据库里有一个表叫作“Customer”,在经过数据库结构生成“领域模型”的时候,Visual Studio固然会帮你生成一个“领域对象”叫作Customer,但若是我把这数据表的名字改成“abc”,虽然里面仍是存的客户信息,但EF并不知道这一点,也是照样生成一个“abc”的类,而这样的东西能算是领域对象吗?所以,数据依赖于实体,是实体的状态,离开实体的数据毫无心义

     
    上图中标记的内容,根据领域驱动设计的思想,是不该该保存的。然而.NET是一个产品,它须要顾及到各类需求,所以,“从数据库生成模型”的功能也将被保留下来
  • 对“聚合”概念的支持。聚合是一系列表述同一律念的相互关联的实体的集合,好比销售订单、销售订单行以及商品,这三个实体能够整合成一个聚合,销售订单则是聚合的根。关于聚合的问题将在后续文章中讨论。为何引入聚合?这是领域驱动设计处理大型软件系统的一种独到的方式。软件系统的实体对象可能很是多,之间的关系也可能错综复杂,那么,当咱们须要从外部持久化机制“唤醒”(或者说读取)某个实体对象的时候,是否是须要将它以及它所关联的全部对象都一并读入内存呢?固然不是,由于咱们只须要关注整个问题的某个方面。好比在读取客户数据的时候,咱们或许会将这个客户的全部订单显示出来,但不会将每一个订单的订单行也所有读出来,由于如今咱们尚未决定是否真的须要查看全部的订单行。
    EF目前不支持聚合概念,全部的实体都被一股脑地塞进ObjectContext对象的EntitySet属性当中,不过这不要紧,接下来的文章我会介绍如何在EF中引入聚合的概念
  • 值对象支持。了解领域驱动设计的朋友都知道,领域模型对象分为实体、值对象和服务。之前的LINQ to SQL不支持值对象,很郁闷,如今好了,EF支持值对象,其表现为ComplexType类型。在这里我想提两点,首先,EF还不支持枚举类型,不要小看枚举类型,与整型类型相比,它可以更好地表达领域语义,好比销售订单的状态能够有 Created,Picked,Packed,Shipped,Delivered,Invoiced,Returned和Cancelled,若是用 0,1,2,3,4,5,6,7来表示,你就会看不懂了,由于这些整数都不是“自描述”的,你须要去读额外的文档来了解它们的意思。其次就是我不太喜欢将 ComplexType定义为引用类型,我以为用值类型就会更加轻量。固然,我不反对使用引用类型,毕竟EF也是出于框架设计的须要
  • 实体不只有状态,还应该有行为。这是很天然的事情,咱们应该使用“富领域模型”,而不只仅是搞一些 POCO加一些getter/setter方法。由于对象自己就应该有行为,这才可以以天然的方式描述现实领域。很惋惜,EF的Entity Data Model Designer只支持对象状态(属性)的图形化定义,而不支持行为定义,这也给EF带来了一个硬伤:无法定义行为的重载和重写,而这些却偏偏是业务逻辑的重要组成部分。我更但愿在下一代EF中,可以看到的是“Entity Model Designer”,而不是“Entity Data Model Designer”。固然,咱们也能够经过使用C#中部分类(partial class)的特性,向实体注入行为,这并不影响实体对领域概念的描述性。
    最糟糕的就算是,EF竟然支持从数据库的存储过程来生成实体对象的方法。这从根本上把技术问题和领域问题混为一谈,我认为这个功能也能够去掉,由于存储过程面向的是数据,而实体方法面向的是领域。有关存储过程的问题,我在后面的文章中也会提到

从上面的描述,咱们对EF的功能有了个大概的了解,接下来的系列文章,我会和你们一块儿,一步步地探讨,如何在EF上应用领域驱动设计的思想,进而完成咱们的案例程序。本系列文章均为我我的原创,或许在某些问题上你会有不一样意见,没关系,你能够直接签写留言,或者发邮件给我,期待与你的探讨,期待与你在软件架构设计的道路上共同进步。

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

分层架构

在引入实例之前,咱们有必要回顾,并进一步了解分层架构。“层”是一种体系结构模式[POSA1],也是被广大软件从业人员用得最为普遍并且最为灵活的模式之一。记得在CSDN上,时常有朋友问到:“分层是什么?为何要分层?三层架构是否是就是表现层、业务逻辑层和数据访问层?”

到这里,你可能会以为这些朋友的问题很简单,分层嘛,不就是将具备不一样职责的组件分离开来,组成一套层内部高聚合,层与层之间低耦合的软件系统吗?不错!这是分层的目标。可是,咱们应该如何分层呢?

领域驱动设计的讨论一样也是创建在层模式的基础上的,但与传统的分层架构相比,它更注重领域架构和技术架构的分离。

传统的三层架构

如上文那位朋友提的问题那样,最简单的分层方式天然就是“表现层、业务逻辑层和数据访问层”,咱们能够用下图来表示这个思想:

注意图中打虚线的“基础结构层”,从实践的表现上来看,这部份内容可能就是一些帮助类,好比 SQLHelper之类的,也多是一些工具类,好比TextUtility之类。这些东西能够被其它各层所访问。而基于分层的概念,表现层只能跟业务逻辑层打交道,而业务逻辑层在数据持久化方面的操做,则依赖于数据访问层。表现层对数据访问层的内容一无所知。

从领域驱动的角度看,这种分层的方式有必定的弊端。首先,为各个层面提供服务的“基础结构层”的职责比较紊乱,它能够是纯粹的技术框架,也能够包含或处理必定的业务逻辑,这样一来,业务逻辑层与“基础结构层”之间就会存在依赖关系;其次,这种结构过度地突出了“数据访问”的地位,把“数据访问”与 “业务逻辑”放在了等同的地位,这也难怪不少软件人员一上来就问:“个人数据表该如何设计?”

领域驱动设计的分层

领域驱动设计将软件系统分为四层:基础结构层、领域层、应用层和表现层。与上述的三层相比,数据访问层已经不在了,它被移到基础结构层了。

  • 基础结构层:该层专为其它各层提供技术框架支持。注意,这部份内容不会涉及任何业务知识。众所周知的数据访问的内容,也被放在了该层当中,由于数据的读写是业务无关的
  • 领域层:包含了业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。这部份内容的具体表现形式就是领域模型(Domain Model)。领域驱动设计提倡富领域模型,即尽可能将业务逻辑归属到领域对象上,实在没法归属的部分则以领域服务的形式进行定义。有关领域对象和领域服务的内容,我会在接下来的案例中进行阐述
  • 应用层:该层不包含任何领域逻辑,但它会对任务进行协调,并能够维护应用程序的状态,所以,它更注重流程性的东西。在某些领域驱动设计的实践中,也会将其称为“工做流层”。应用层是领域驱动中最有争议的一个层次,也会有不少人对其职责感到模糊不清。好比,有些国外的开发人员会以为,既然不包含领域逻辑,那又如何协调工做任务呢?我会在《应用层与实体事件》章节对这些问题进行探讨
  • 表现层:这个好理解,跟三层里的表现层意思差很少,但请注意,“Web服务”虽然是服务,但它是表现层的东西

从上图还能够看到,表现层与应用层之间是经过数据传输对象(DTO)进行交互的,数据传输对象是没有行为的POCO对象,它的目的只是为了对领域对象进行数据封装,实现层与层之间的数据传递。为什么不能直接将领域对象用于数据传递?由于领域对象更注重领域,而DTO更注重数据。不只如此,因为“富领域模型”的特色,这样作会直接将领域对象的行为暴露给表现层。

从下一个章节开始,我将以最简单的销售系统为例,介绍EntityFramework下领域驱动设计的应用。

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

案例:一个简易的销售系统

从如今开始,咱们将以一个简易的销售系统为例,探讨EntityFramework在领域驱动设计上的应用。为了方便讨论,咱们的销售系统很是简单,不会涉及客户存在多个收货地址的状况,也不会包含任何库存管理的内容。假设咱们的系统只须要维护产品类型、产品以及客户信息,并可以帮客户下订单、跟踪订单状态,以及接受客户退货。从简单的分析咱们大体能够了解到,这个系统将会有以下实体:客户、单据、产品及其类型。单据分为销售订单和退货单两种,每一个单据能够有多个单据行(好比销售订单行和退货单行)。不只如此,系统容许每一个客户有多张信用卡,以便在结帐的时候,选择一张信用卡进行支付。在使用EF的Entity Data Model Designer进行设计后,咱们获得下面的模型:

上面的模型表述了领域模型中各个实体及其之间的关系。咱们先不去讨论整个系统的业务会是什么样的,咱们先看看EF是如何支持实体和值对象概念的。

实体

首先看看实体这个概念。在领域驱动设计的理论中,实体是模型中须要区分个体的对象,也就是说,针对某种类型,咱们既要知道它是什么,还须要知道它是哪一个。我在前面的博文中有介绍过实体这个概念。实体都有一个标识符,以便跟同类型的其它实体进行区分。EF Entity Data Model Designer上可以画出的都是实体,你能够看到每一个实体都有个Id成员,其Entity Key属性被设置为True,同时被分配了一种标识符的生成方式,以下图所示:

在从领域模型映射到数据模型的过程当中,这个标识符一般都是被映射为关系数据库某个数据表的主键,这个应该是很容易理解的。

其次,EF不支持实体行为,所以,整个模型只能被称为Entity Data Model,而不是Entity Model,由于它只支持对实体数据的描述。幸好从.NET 2.0开始,托管语言开始支持partial特性,同一个类能够以部分类(partial class)的特性写入多个代码文件中。所以,若是须要向上述模型中的实体加入行为,咱们能够在工程中加入几个代码文件,而后使用部分类的特色,为实体添加必要的行为。好比,下面的部分类向订单行中加入了一个只读属性,该属性用于计算某一单据行所拥有的总金额:

有朋友会问,为何咱们要另外使用部分类,而不是直接在模型文件 edmx的源代码上直接修改?由于这个源代码文件是框架动态生成的,若是在上面修改,等下次模型被更新的时候,你所作的更改便会丢失。

对于实体的行为,EF支持从数据库存储过程生成实体对象行为的过程。对此,我持批判态度:EF把数据模型与实体模型混为一谈了,这种作法只能让软件人员感到更加困惑。我在下一篇文章将重点表述我对这个问题的见解。我也相信微软在下一代实体框架中可以处理好这个问题。

再次,EF对实体对象关系的支持主要有关联和继承。根据Multiplicity的设置,关联又能够支持到组合关联与聚合关联。我以为EF中对继承关系的支持是一个亮点。继承表述了“什么是一种什么”的观点,好比在咱们的案例中,“销售订单”和“退货单”都是一种“单据”。若是从传统的数据库驱动的设计方案,咱们很天然地会使用“Orders”数据表中的整型字段“OrderType”来保存当前单据的类型(好比0表示销售订单,1表示退货单),那么,在获取系统中全部销售订单的时候,咱们会使用下面的代码:

List<Order> GetSalesOrders(IDbConnection connection)
{
    IDbCommand command = new SqlCommand("SELECT * FROM [Orders] WHERE [OrderType]=0",
        (SqlConnection)connection);
    List<Order> orders = new List<Order>();
    using (IDataReader dr = command.ExecuteReader())
    {
        while (dr.Read())
        {
            Order order = new Order();
            order.Id = Convert.ToInt32(dr["Id"]);
            // ...
            orders.Add(order);
        }
        dr.Close();
    }
    return orders;
}

从技术角度讲,上面的代码没什么问题,运行的也很好,可以得到系统中全部销售订单的列表。可是, [OrderType]=0这种写法并不包含任何领域语义,若是让另外一个开发人员来跟进这段代码,他不得不先去查阅其它项目文档,以了解这个 [OrderType]=0的具体涵义。在引入了继承关系的EF中,咱们只须要下面的Linq to Entities,便可既方便、又明了地得到全部销售订单的列表了:

List<Order> GetSalesOrders()
{
    using (EntitiesContainer container = new EntitiesContainer())
    {
        return (from order in container.Orders
                where order is SalesOrder
                select order).ToList();
    }
} 

简单明了吧?EF带给咱们的不只仅是一个技术框架,也不只仅是一个数据存取的解决方案。

值对象

EF支持值对象,这很好!在EF中能够定义Complex Types,而一个Complex Type下能够定义多个Primitive Type和多个Complex Type。与LINQ to SQL相比,这是一大进步。

对于值对象的两点问题我在第一篇文章中已经讲过了,在此就不重复了。

综上所述,EF基本上可以支持领域驱动设计的思想(即便有些方面不完善,但目前也能够找到替代的方案)。我想,只要可以对领域驱动有清晰的认识,就可以很好地将实体框架应用于领域驱动的实践中。

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

存储过程 - 领域驱动的反模式

EntityFramework(EF)中有一项功能,就是可以根据数据库中的存储过程生成实体的行为(或称方法,如下统称方法)。我在本系列的第一篇博文中就已经提到,这种作法并不可取!由于存储过程是技术架构中的内容,而咱们所关注的倒是领域模型。

Andrey Yemelyanov在其“Using ADO.NET EF in DDD: A Pattern Approach”一文中,有下面这段话:

In this context, the architect also identified the anti-pattern of using the EF (ineffective use): the EF is used to mechanically derive the domain model from the database schema (database-driven approach). Therefore, the main topic of the guidelines should be the domain-driven development with the EF and the primary focus should be on how to build a conceptual model over a relational model and expose it to application programmers.

黑体部分的意思是:(被采访的架构师)同时也指出了使用EF的一种“反模式”,即经过使用数据库结构来机械化地生成领域模型(数据库驱动的方式)。这也就证实了我在第一篇文章中指出的“只能经过领域模型来映射到数据模型,反过来则不行”的观点。

我可以理解不少作系统的朋友喜欢考虑数据库方面的事情,毕竟数据存储也是软件系统中不可或缺的部分(固然,内存数据库另当别论),数据库结构设计的好坏直接影响到系统的运行效率。好比:加不加索引、如何加索引,将对查询效率形成重大影响。但请注意:你把过多的精力放在了技术架构上,而本来更重要的业务架构却被扔到了一边。

何时选择存储过程?我认为存储过程的操做对象应该是数据,而不是对象,业务层也不可能把业务对象交给数据库去处理。其实,存储过程自己的意义也就是将数据放在DB服务器上处理,以减小客户程序与服务器之间的网络流量和往返次数,从而提升效率。因此,能够把查询次数较多的、与业务无关的数据处理交给存储过程(好比数据统计),而不要单纯地认为存储过程可以帮你解决业务逻辑问题,那样作只会让你的领域模型变得混乱,长此以往,你将没法应对复杂的业务逻辑与需求变动。

在作技术选型的时候还要注意一点,也就是存储过程的数据库相关性。存储过程是特定于某种关系型数据库机制的,好比,SQL Server的存储过程与Oracle的存储过程并不通用,而有些数据库系统甚至不支持存储过程。所以过多使用存储过程将会给你带来一些没必要要的麻烦。我我的对是否使用存储过程给出以下几点意见:一、根据需求来肯定;二、不推荐使用;三、出于效率等技术考虑,须要使用的,酌情处理。

回过头来看实体框架,虽然如今支持从数据库存储过程生成实体方法的过程,但这是一种反模式,我也不但愿从此EF还提供将实体方法映射到存储过程的功能,由于行为不一样于数据,数据是状态,而行为是业务,它与领域密切相关,它不该该被放到“基础结构层”这一技术架构中。

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

聚合

聚合(Aggregate)是领域驱动设计中很是重要的一个概念。简单地说,聚合是这样一组领域对象(包括实体和值对象),这组领域对象联合起来表述一个完整的领域概念。好比,根据Eric Evans《领域驱动设计》一书中的例子,一辆车包含四个轮子,轮子离开“车”就毫无心义,此时这个联合体就是聚合,而“车”就是聚合根(Aggregate Root)。

从实践中得知,并不是领域模型中的每一个实体都可以完整地表述一个明确的领域概念,就好比客户与送货地址的关系。假设在某个应用中,系统须要为每一个客户维护多个送货地址,此时送货地址就是一个实体,而不是值对象。那么这样一来,领域模型中至少就有了“客户”和“送货地址”两个实体,而事实上,“送货地址”是针对“客户”的,离开“客户”,“送货地址”就变得毫无心义。因而,“送货地址”就和“客户”一块儿,完整地表达了“客户能够有多个送货地址,并能对它们进行维护”的思想。

在《实体框架之领域驱动实践(三) - 案例:一个简易的销售系统》一文中,咱们简单地设计了一个领域模型,其中包含了一些必要的实体和值对象。如今,我用不一样颜色的笔在这个领域模型上圈出了三个聚合:客户、订单以及产品分类,以下图所示:

【注意】:若是像上图所示,Category-Item组成一个聚合,那么此时聚合根就应该是Item,而不是Category,由于Category对Item从概念上并无包含/被包含的关系,而更多状况下,Category是 Item的一种信息描述,即某个Item是能够归类到某个Category的。在这种状况下,咱们不须要对Category进行维护,Category就以值对象的形式存在于领域模型中。若是是另外一种应用场合,好比,咱们的系统须要针对Category进行促销,那么咱们须要维护Category的信息,由此Category和Item就分属两个不一样的聚合,聚合根为各自自己。

首先是“客户-信用卡”聚合,这个聚合表示了一个客户能够拥有多张信用卡,相似于上面所讲的 “客户-送货地址”的概念;其次是“订单-订单行”的聚合,相似地,虽然订单行也是一个实体,由于在应用中须要对每一个订单行进行区分,可是订单行离开订单就变得毫无心义,它是“订单”概念的一部分;最后是“产品分类-产品”的聚合。

每一个聚合都有一个根实体(聚合根,Aggregate Root),这个根实体是聚合所表述的领域概念的主体,外部对象须要访问聚合内的实体时,只能经过聚合根进行访问,而不能直接访问。从技术角度考虑,聚合肯定了实体生命周期的关注范围,即当某个实体被建立时,同时须要建立以其为根的整个聚合,而当持久化某个实体时,一样也须要持久化整个聚合。好比,在从外部持久化机制重建“客户”对象的同时,也须要将其所拥有的“信用卡”赋给“客户”实体(具体如何操做,根据需求而定)。不要去关注聚合内实体的生命周期问题,若是你真的这么作了,那么你就须要考虑下你的设计是否合理。

由此引出了“领域对象生命周期”的问题,这个问题我会在后面两节单独讨论,但目前至少知道:

1.领域对象从无到有的建立,不是针对某个实体的,而是针对某个聚合的

2.领域对象的持久化(一般所说的“保存”)、重建(一般所说的“查询”)和销毁(一般所说的“删除”)也不是针对某个实体的,而是针对某个聚合的

很惋惜,微软的EntityFramework(实体框架,EF)目前并不支持“聚合”的概念,全部的实体都被一股脑地塞到 ObjectContext中:

为了实现聚合的概念,咱们又一次地须要用到“部分类(partial class)”的功能。咱们首先定义一个IAggregateRoot的接口,修改每一个聚合根的实体类,使其实现IAggregateRoot接口,以下:

  public interface IAggregateRoot
  {
  }

 

[AggregateRoot("Orders")]
partial class Order : IAggregateRoot
{
    public Single TotalDiscount
    {
        get
        {
            return this.Lines.Sum(p => p.Discount);
        }
    }
    public Single TotalAmount
    {
        get
        {
            return this.Lines.Sum(p => p.LineAmount);
        }
    }
} 

到这里又有问题了,接口IAggregateRoot中什么都没有定义?!我在个人技术博客中,特别解释了C#中接口的三种用途,请参考这篇文章:《C#基础:多功能的接口》。在这里,咱们将IAggregateRoot接口用做泛型约束。在看完后续的两篇介绍领域对象生命周期的文章后,你就可以更好地理解这个问题了。事实上,在领域驱动设计的社区中,很多人都是这样用的。

最后说明一下,因为实体框架使全部的实体类继承于EntityObject类,而从面向对象的角度,接口是没办法去继承于类的,所以,在这里咱们的 IAggregateRoot接口好像跟实体没什么太大的关系,而事实上聚合根应该是一种实体。在不少领域驱动的项目中,设计人员专门设计了 IEntity接口,全部实现了该接口的类都被认定为实体类,因而,IAggregateRoot接口也就很天然地继承IEntity接口,以表示“聚合根是一种实体”的概念,代码大体以下:

public interface IEntity
{
    Guid Id { get; set; }
}
public interface IAggregateRoot : IEntity
{
}

总的来讲,领域模型须要根据领域概念分红多个聚合,每一个聚合都有一个实体做为“聚合根”,通俗地说,领域对象从无到有的建立,以及CRUD操做都应该做用在聚合根上,而不是单独的某个实体。当你的代码须要直接对聚合内部的实体进行CRUD操做时,就说明你的模型设计已经存在问题了。

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

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

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

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

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

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

/// <summary>
/// Create a new Customer object.
/// </summary>
/// <param name="id">Initial value of the Id property.</param>
/// <param name="name">Initial value of the Name property.</param>
/// <param name="billingAddress">Initial value of the BillingAddress property.</param>
/// <param name="deliveryAddress">Initial value of the DeliveryAddress property.</param>
/// <param name="loginName">Initial value of the LoginName property.</param>
/// <param name="loginPassword">Initial value of the LoginPassword property.</param>
/// <param name="dayOfBirth">Initial value of the DayOfBirth property.</param>
public static Customer CreateCustomer(global::System.Int32 id, Name name, Address billingAddress,  
                                      Address deliveryAddress, global::System.String loginName,  
                                      global::System.String loginPassword, global::System.DateTime dayOfBirth)
{
    Customer customer = new Customer();
    customer.Id = id;
    customer.Name = StructuralObject.VerifyComplexObjectIsNotNull(name, "Name");
    customer.BillingAddress = StructuralObject.VerifyComplexObjectIsNotNull(billingAddress, "BillingAddress");
    customer.DeliveryAddress = StructuralObject.VerifyComplexObjectIsNotNull(deliveryAddress, "DeliveryAddress");
    customer.LoginName = loginName;
    customer.LoginPassword = loginPassword;
    customer.DayOfBirth = dayOfBirth;
    return customer;
}

那么在建立一个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,则证实聚合根并无合理地维护聚合的完整性):

  • .NET实体框架仅仅为每一个实体提供了一个最为简单的工厂方法。“工厂”的概念,在领域驱动设计中具备以下的最佳实践:
    • 工厂能够隐藏对象建立的细节,由于对象的建立不属于业务领域须要考虑的问题
    • 工厂用来建立整个聚合,从而维护聚合所表明的领域含义
    • 能够在聚合根中添加工厂方法,也能够使用工厂类。也就是说,能够建立一个CustomerFactory的类,在其中定义 CreateCustomer方法。具体是选用工厂方法仍是工厂类,应该根据需求而定
    • 当须要对被建立的实体传入参数时,应该尽量地减少耦合性,好比能够使用抽象类或者接口做为参数类型

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

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值(也就是咱们常说的实体键)是不变的,所以,在领域层看来,从仓储中读取获得的对象与当时保存进去的对象并无什么两样。

你可能已经体会到,仓储就是一个数据库,它与数据库同样,有读取、保存、查询、删除的操做。我只能说,你已经了解到仓储的职能,并无了解到它的角色。仓储是领域层与基础结构层的一个衔接组件,领域层经过仓储访问外部存储机制,这样就使得领域层无需关心任何技术架构上的实现细节。所以,仓储这个角色的职责不只仅是读取、保存、查询、删除,它还解耦了领域层与基础结构层。在实践中,能够使用依赖注入的方式,将仓储实例注入到领域层,从而得到灵活的体系结构。

下面是咱们案例中,仓储接口的代码:

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

IRepository是一个泛型接口,泛型类型被where子句限定为EntityFramework中的EntityObject,与此同时,where子句还限定了泛型类型必须实现IAggregateRoot接口。换句话讲,IRepository接口的泛型类型必须是继承于EntityObject类,并实现了IAggregateRoot接口的引用类型。根据咱们在 “聚合”一文中的表述,咱们能够实现针对Customer、Order以及Category实体类的仓储类。

这里只给出了仓储实现的一个引子,至少到目前为止咱们已经简单地定义了仓储实现的一个框架,也就是上面这个IRepository泛型接口。接口中具体要包括哪些方法,不是本系列文章要讨论的关键问题。为了描述与演示,咱们只为IRepository接口设计如上四个方法,即Add、GetByKey、Remove和Update。接下来,我将详细描述在基于实体框架(EntityFramework)的仓储设计中所遇到的困难,以及如何在实践中解决这些困难。

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

仓储的实现:基本篇

咱们先从技术角度考虑仓储的问题。实体框架(EntityFramework)中,操做数据库是很是简单的:在ObjectContext中使用LINQ to Entities便可完成操做。开发人员也不须要为事务管理而操心,一切都由EF包办。与本来的ADO.NET以及LINQ to SQL相比,EF更为简单,LINQ to Entities的引入使得软件开发变得更为“领域化”。

下面的代码测试了持久化一个 Customer实体,并从持久化机制中查询这个Customer实体的正确性。从代码中能够看到,咱们用了一种很天然的表达方式,表述了“我但愿查询一个名字为Sunny的客户”这样一种业务逻辑。

[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方式就没法知足需求了:

  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,它承担了解耦领域模型和技术架构的重要职责。为了完美地解决上面提到的问题,咱们仍然采用领域驱动设计中仓储的设计模式,而将实体框架做为仓储的具体实现部分。在详细介绍仓储的设计与实现以前,让咱们回顾一下上文最后部分我提到的那个仓储的接口:

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

在本文的案例中,仓储是这样实现的:

1.将上述仓储接口定义在实体、值对象和服务所在的领域层。有朋友问过我,既然仓储须要与外部存储机制打交道,那么它一定须要知道技术架构方面的细节,而将其定义在领域层,就会使得领域层依赖于具体的技术实现方式,这样就会使领域层变得“不纯净” 了。其实否则!请注意,咱们这里仅仅只是将仓储的接口定义在了领域层,而不是仓储的具体实现(Concrete Repository)。更通俗地说,接口做为系统架构的基础元素,决定了整个系统的架构模式,而基于接口的具体实现只不过是一种可替换的组件,它不能成为系统架构中的一部分。因为领域层须要用到仓储,我便将仓储的接口定义在了领域层。固然,从.NET的实现技术考虑,你能够新建一个Class Library,并将上述接口定义在这个Class Library中,而后在领域层和仓储的具体实现中分别引用这个Class Library

2.新建一个Class Library(在本文的案例中,命名为EasyCommerce.Infrastructure.Repositories),添加对领域层 assembly的引用,并实现上述接口。因为咱们采用实体框架做为仓储的具体实现,所以,将这个仓储命名为EdmRepository(Entity Data Model Repository)。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。相似的代码以下:

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

回头来看EdmRepository的构造函数,在构造函数中,咱们使用.NET的反射机制得到了定义在聚合根类型上的EntitySetName

3. 使用IoC/DI(控制反转/依赖注入)框架,将仓储的实现(EdmRepository)注射到领域模型中。至此,领域模型一直保持着对仓储接口的引用,而对仓储的具体实现方式一无所知。因为IoC/DI的引入,咱们获得了一个纯净的领域模型。在这里我也想提出一个衡量系统架构优劣度的重要指标,就是领域模型的纯净度。常见的 IoC/DI框架有Spring.NET和Castle Windsor MicroKernel。在本文的案例中,我采用了Castle Windsor。如下是针对Castle Windsor的配置文件片断:

<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子句参数,实现相似规约的功能。有关规约模式,我将在其它的文章中讨论。

从本文还能够了解到,依赖注入是维持领域模型纯净度的一大利器;另外一大利器是领域事件,我将在后续的文章中详述。对于本文开始的第三个问题,也就是仓储实现的可扩展性,将在下篇文章中进行讨论,包括的内容有:事务处理和可扩展的仓储框架的实现。

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

仓储的实现:深刻篇

早在年前的时候就已经在CSAI博客发表了上一篇文章:《仓储的实现:基础篇》。苦于日夜奔波于工做与生活之间,一直没有可以抽空继续探讨仓储的实现细节,也让不少关注EntityFramework和领域驱动设计的朋友们备感失望。

闲话很少说,如今继续考虑,如何让仓储的操做在相同的事物处理上下文中进行。DDD引入仓储模式,其目的之一就是可以经过仓储隐藏对象持久化的技术细节,使得领域模型变得更为“纯净”。因而可知,仓储的实现是须要基础结构层的组件支持的,表现为对数据库的操做。在传统的关系型数据库操做中,事务处理是一个很重要的概念,虽然从目前某些大型项目看,事务处理会下降效率,但它保证了数据的完整性。关系型数据库仍然是目前数据持久化机制的主流,事务处理的实现仍是颇有必要的。

为了迎合仓储模式,就须要对经典的ObjectContext使用方式做一些调整。好比,本来咱们能够很是简单地使用using (EntitiesContainer ec = new EntitiesContainer())语句来界定LINQ to Entities的操做范围,并使用ObjectContext的SaveChanges成员方法提交事务,而在引入了仓储的实现中,就不能继续采用这种经典的使用方式。这让EntityFramework看上去变得很奇怪,也很牵强,我相信不少网友会批评个人作法,由于我把问题复杂化了。

其实,这应该是关注点不一样罢了。关注EntityFramework的开发人员,天然以为经典的调用方式简单明了,而从DDD的角度看呢?只能把关注点放在仓储上,而把EntityFramework当成是仓储的一种技术选型(固然从DDD角度讲,咱们彻底能够不选择EntityFramework,而去选择其它技术)。因此本文暂且抛开EntityFramework,继续在上文的基础上,讨论仓储的实现。

前面提到,仓储的实现须要考虑事务处理,并且根据DDD的经验,针对每个聚合根,都须要有个仓储对其进行持久化以及对象从新组装等操做。为此,个人想法是,将仓储操做“界定”在某一个事务处理上下文(Context)中,仓储的实例是由Context得到的,这有点像EntityFramework中ObjectContext与EntityObject的关系那样。因为仓储是来自于transaction context,因此它知道目前处于哪一个事务上下文中。我定义的这个transaction context以下:

public interface IRepositoryTransactionContext : IDisposable
{
    IRepository<TEntity> GetRepository<TEntity>()
        where TEntity : EntityObject, IAggregateRoot;
    void BeginTransaction();
    void Commit();
    void Rollback();
} 

上面第三行代码定义了一个接口方法,这个方法的主要做用就是返回一个针对指定聚合根实体的仓储实例。剩下那三行代码就很明显了,那是标准的transaction操做:启动事务、提交事务以及回滚事务。

在设计上,能够根据须要,选择合适的技术来实现IRepositoryTransactionContext。咱们如今讨论的是EntityFramework,因此我将给出EntityFramework的具体实现。固然,若是你不选用EntityFramework,而是用NHibernate实现数据持久化,这样的设计一样可以使你达到目的。如下是基于EntityFramework的实现:EdmRepositoryTransactionContext的伪代码。

internal class EdmRepositoryTransactionContext : IRepositoryTransactionContext
{
    private ObjectContext objContext;
    private Dictionary<Type, object> repositoryCache = new Dictionary<Type, object>();
    public EdmRepositoryTransactionContext(ObjectContext objContext)
    {
        this.objContext = objContext;
    }
    #region IRepositoryTransactionContext Members
    public IRepository<TEntity> GetRepository<TEntity>() where TEntity : EntityObject, IAggregateRoot
    {
        if (repositoryCache.ContainsKey(typeof(TEntity)))
        {
            return (IRepository<TEntity>)repositoryCache[typeof(TEntity)];
        }
        IRepository<TEntity> repository = new EdmRepository<TEntity>(this.objContext);
        this.repositoryCache.Add(typeof(TEntity), repository);
        return repository;
    }
    public void BeginTransaction() 
    { 
        // We do not need to begin a transaction here because the object context,
        // which would handle the transaction, was created and injected into the
        // constructor by Castle Windsor framework.
    }
    public void Commit()
    {
        this.objContext.SaveChanges();
    }
    public void Rollback()
    {
        // We also do not need to perform the rollback operation because
        // entity framework will handle this for us, just when the execution
        // point is stepping out of the using scope.
    }
    #endregion
    #region IDisposable Members
    public void Dispose()
    {
        this.repositoryCache.Clear();
        this.objContext.Dispose();
    }
    #endregion
} 

EdmRepositoryTransactionContext被定义为internal,这个设计是合理的,由于Domain层是不须要知道事务上下文的具体实现,它将会被IoC/DI容器注入到Domain层中(本系列文章采用Castle Windsor框架)。在EdmRepositoryTransactionContext的构造函数中,它须要EntityFramework的ObjectContext对象来初始化实例。一样,因为IoC/DI的使用,咱们在代码中也是不须要去建立这个ObjectContext的,交给Castle Windsor就OK了。第13行的GetRepository方法简单地采用了Dictionary对象来实现缓存仓储实例的效果,固然这种作法还有待改进。

EdmRepositoryTransactionContext是不须要BeginTransaction的,咱们将方法置空,由于EntityFramework的事务会由ObjectContext来管理,同理,Rollback也被置空。

EdmRepository的实现就比较显而易见了,请参见上文。

此外,咱们还能够针对NHibernate实现仓储模式,只须要实现IRepositoryTransactionContext和IRepository接口便可,好比:

internal class NHibernateRepositoryTransactionContext : IRepositoryTransactionContext
{
    ITransaction transaction;
    Dictionary<Type, object> repositoryCache = new Dictionary<Type, object>();
    public ISession Session { get { return DatabaseSessionFactory.Instance.Session; } }
    #region IRepositoryTransactionContext Members
    public IRepository<TEntity> GetRepository<TEntity>() 
        where TEntity : EntityObject, IAggregateRoot
    {
        if (repositoryCache.ContainsKey(typeof(TEntity)))
        {
            return (IRepository<TEntity>)repositoryCache[typeof(TEntity)];
        }
        IRepository<TEntity> repository = new NHibernateRepository<TEntity>(this.Session);
        this.repositoryCache.Add(typeof(TEntity), repository);
        return repository;
    }
    public void BeginTransaction()
    {
        transaction = DatabaseSessionFactory.Instance.Session.BeginTransaction();
    }
    public void Commit()
    {
        transaction.Commit();
    }
    public void Rollback()
    {
        transaction.Rollback();
    }
    #endregion
    #region IDisposable Members
    public void Dispose()
    {
        transaction.Dispose();
        ISession dbSession = DatabaseSessionFactory.Instance.Session;
        if (dbSession != null && dbSession.IsOpen)
            dbSession.Close();
    }
    #endregion
} 

 

internal class NHibernateRepository<TEntity> : IRepository<TEntity>
    where TEntity : EntityObject, IAggregateRoot
{
    ISession session;
    public NHibernateRepository(ISession session)
    {
        this.session = session;
    }
    #region IRepository<TEntity> Members
    public void Add(TEntity entity)
    {
        this.session.Save(entity);
    }
    public TEntity GetByKey(int id)
    {
        return (TEntity)this.session.Get(typeof(TEntity), id);
    }
    public IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec)
    {
        throw new NotImplementedException();
    }
    public void Remove(TEntity entity)
    {
        this.session.Delete(entity);
    }
    public void Update(TEntity entity)
    {
        this.session.SaveOrUpdate(entity);
    }
    #endregion
} 

在NHibernateRepositoryTransactionContext中使用了一个DatabaseSessionFactory的类,该类主要用于管理NHibernate的Session对象,具体实现以下(该实现已被用于个人Apworks应用开发框架原型中):

/// <summary>
/// Represents the factory singleton for database session.
/// </summary>
internal sealed class DatabaseSessionFactory
{
    #region Private Static Fields
    /// <summary>
    /// The singleton instance of the database session factory.
    /// </summary>
    private static readonly DatabaseSessionFactory databaseSessionFactory = new DatabaseSessionFactory();
    #endregion
    #region Private Fields
    /// <summary>
    /// The session factory instance.
    /// </summary>
    private ISessionFactory sessionFactory = null;
    /// <summary>
    /// The session instance.
    /// </summary>
    private ISession session = null;
    #endregion
    #region Constructors
    /// <summary>
    /// Privately constructs the database session factory instance, configures the
    /// NHibernate framework by using the assemblies listed in the configured spaces(paths)
    /// that are decorated by <see cref="EntityVisibleAttribute"/>.
    /// </summary>
    private DatabaseSessionFactory()
    {
        Configuration nhibernateConfig = new Configuration();
        nhibernateConfig.Configure();
        nhibernateConfig.AddAssembly(typeof(IAggregateRoot).Assembly);
        sessionFactory = nhibernateConfig.BuildSessionFactory();
    }
    #endregion
    #region Public Properties
    /// <summary>
    /// Gets the singleton instance of the database session factory.
    /// </summary>
    public static DatabaseSessionFactory Instance
    {
        get
        {
            return databaseSessionFactory;
        }
    }
    /// <summary>
    /// Gets the singleton instance of the session. If the session has not been
    /// initialized or opened, it will return a newly opened session from the session factory.
    /// </summary>
    public ISession Session
    {
        get
        {
            ISession result = session;
            if (result != null && result.IsOpen)
                return result;
            return OpenSession();
        }
    }
    #endregion
    #region Public Methods
    /// <summary>
    /// Always opens a new session from the session factory.
    /// </summary>
    /// <returns>The newly opened session.</returns>
    public ISession OpenSession()
    {
        this.session = sessionFactory.OpenSession();
        return this.session;
    }
    #endregion
} 

简单小结一下。经过上面的例子能够看到,仓储的实现是不能依赖于任何技术细节的,由于领域模型并不关心技术问题。这并非DDD一书中怎么说,咱们就得怎么作。事实上的确如此,由于DDD的思想,使得咱们应该把关注点放在业务分析与领域建模上,而仓储实现的分离正是这一思想的具体表现形式。无论怎么样,采用其它的手段也罢,咱们仍是应该遵循“将关注点放在领域”这一宗旨。

接下来看如何在领域层结合IoC框架使用仓储。仍然以Castle Windsor为例。配置文件以下(超长部分我用省略号去掉了):

<castle>
  <components>
    <!-- Object Context for Entity Data Model -->
    <component id="ObjectContext"
               service="System.Data.Objects.ObjectContext, System.Data.Entity, Version=4.0.0.0,......" 
               type="EasyCommerce.Domain.Model.EntitiesContainer, EasyCommerce.Domain"/>
    <component id="GeneralRepository"
               service="EasyCommerce.Domain.IRepository`1[[EasyCommerce.Domain.Model.Customer, ......"
               type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepository`1[[EasyCo......">
      <objContext>${ObjectContext}</objContext>
    </component>
    <component id="TransactionContext"
               service="EasyCommerce.Domain.IRepositoryTransactionContext, EasyCommerce.Domain......"
               type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepositoryTransactionContext, ...">
    </component>
  </components>
</castle> 

如下是调用代码:

[TestMethod]
public void TestCreateCustomer()
{
    IWindsorContainer container = new WindsorContainer(new XmlInterpreter());
    using (IRepositoryTransactionContext tx = container.GetService<IRepositoryTransactionContext>())
    {
        tx.BeginTransaction();
        try
        {
            Customer customer = Customer.CreateCustomer("daxnet", "12345",
                new Name { FirstName = "Sunny", LastName = "Chen" },
                new Address(), new Address(), DateTime.Now.AddYears(-29));
            IRepository<Customer> customerRepository = tx.GetRepository<Customer>();
            customerRepository.Add(customer);
            tx.Commit();
        }
        catch
        {
            tx.Rollback();
            throw;
        }
    }
} 

测试结果及数据库数据结果:

【注意】:在使用NHibernate的仓储实现时,因为NHibernate的延迟加载特性,须要将实体的属性设置为virtual,以便由NHibernate产生Proxy Class进而实现延迟加载;可是由EntityFramework自动生成的源代码并不会将实体属性设置为virtual,而采用partial class也没法解决这个问题。所以须要在代码生成技术上作文章。个人作法是,针对edmx产生一个基于T4的代码生成模板,而后修改这个模板,分别在WritePrimitiveTypeProperty和WriteComplexTypeProperty方法中的适当位置加上virtual关键字:

    private void WritePrimitiveTypeProperty(EdmProperty primitiveProperty, CodeGenerationTools code)
    {
        MetadataTools ef = new MetadataTools(this);
#>
    /// <summary>
    /// <#=SummaryComment(primitiveProperty)#>
    /// </summary><#=LongDescriptionCommentElement(primitiveProperty, 1)#>
    [EdmScalarPropertyAttribute(EntityKeyProperty=<#=code.CreateLiteral(ef.IsKey(primitiveProperty))#>,  
IsNullable=<#=code.CreateLiteral(ef.IsNullable(primitiveProperty))#>)]
    [DataMemberAttribute()]
    <#=code.SpaceAfter(NewModifier(primitiveProperty))#><#=Accessibility.ForProperty(primitiveProperty)#> virtual 
 <#=code.Escape(primitiveProperty.TypeUsage)#> <#=code.Escape(primitiveProperty)#>
    {
        <#=code.SpaceAfter(Accessibility.ForGetter(primitiveProperty))#>get
        {
<#+             if (ef.ClrType(primitiveProperty.TypeUsage) == typeof(byte[]))
                {
#>
            return StructuralObject.GetValidValue(<#=code.FieldName(primitiveProperty)#>);

 

    private void WriteComplexTypeProperty(EdmProperty complexProperty, CodeGenerationTools code)
    {
#>
    /// <summary>
    /// <#=SummaryComment(complexProperty)#>
    /// </summary><#=LongDescriptionCommentElement(complexProperty, 1)#>
    [EdmComplexPropertyAttribute()]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    [XmlElement(IsNullable=true)]
    [SoapElement(IsNullable=true)]
    [DataMemberAttribute()]
    <#=code.SpaceAfter(NewModifier(complexProperty))#><#=Accessibility.ForProperty(complexProperty)#> virtual  
<#=MultiSchemaEscape(complexProperty.TypeUsage, code)#><#=code.Escape(complexProperty)#>
    {
        <#=code.SpaceAfter(Accessibility.ForGetter(complexProperty))#>get
        {
            <#=code.FieldName(complexProperty)#> = GetValidValue(<#=code.FieldName(complexProperty)#>,  
"<#=complexProperty.Name#>",  
false, <#=InitializedTrackingField(complexProperty, code)#>);
            <#=InitializedTrackingField(complexProperty, code)#> = true; 

始终坚持一个原则:不要在生成的代码上直接修改,一是工做量巨大,另外一方面就是,代码是自动生成的,从此模型修改了,代码将会从新生成。

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

规约(Specification)模式

原本针对规约模式的讨论,我并无想将其列入本系列文章,由于这是一种概念性的东西,从理论上讲,与EntityFramework好像扯不上关系。但应广大网友的要求,我决定仍是在这里讨论一下规约模式,并介绍一种专门针对.NET Framework的规约模式实现。

不少时候,咱们都会看到相似下面的设计:

public interface ICustomerRespository 
 { 
    Customer GetByName(string name); 
    Customer GetByUserName(string userName); 
    IList<Customer> GetAllRetired(); 
} 

接下来的一步就是实现这个接口,并在类中分别实现接口中的方法。很明显,在这个接口中,Customer仓储一共作了三个操做:经过姓名获取客户信息;经过用户名获取客户信息以及得到全部当前已退休客户的信息。这样的设计有一个好处就是一目了然,可以很方便地看到Customer仓储到底提供了哪些功能。文档化的开发方式特别喜欢这样的设计。

仍是那句话,应需而变。若是你的系统很简单,而且从此扩展的可能性不大,那么这样的设计是简洁高效的。但若是你正在设计一个中大型系统,那么,下面的问题就会让你感到困惑:

  1. 这样的设计,便于扩展吗?从此须要添加新的查询逻辑,结果一大堆相关代码都要修改,怎么办?
  2. 随着时间的推移,这个接口会变得愈来愈大,团队中你一榔头我一棒子地对这个接口进行修改,最后整个设计变得一团糟
  3. GetByName和GetByUserName都OK,由于语义一目了然。可是GetAllRetired呢?什么是退休?超过法定退休年龄的算退休,那么病退的是否是算在里面?这里返回的全部Customer中,仅仅包含了已退休的男性客户,仍是全部性别的客户都在里面?

规约模式就是DDD引入用来解决以上问题的一种特殊的模式。规约是一种布尔断言,它表述了给定的对象是否知足当前约定的语义。经典的规约模式实现中,规约类只有一个方法,就是IsSatisifedBy(object);以下:

public class Specification 
 { 
    public virtual bool IsSatisifedBy(object obj) 
    { 
        return true; 
    } 
} 

仍是先看例子吧。在引入规约之后,上面的代码就能够修改成:

public interface ICustomerRepository 
 { 
    Customer GetBySpecification(Specification spec); 
    IList<Customer> GetAllBySpecification(Specification spec); 
} 

public class NameSpecification : Specification 
 { 
    protected string name; 
    public NameSpecification(string name) { this.name = name; } 
    public override bool IsSatisifedBy(object obj) 
    { 
        return (obj as Customer).FirstName.Equals(name); 
    } 
} 

public class UserNameSpecification : NameSpecification 
 { 
    public UserNameSpecification(string name) : base(name) { } 
    public override bool IsSatisifedBy(object obj) 
    { 
        return (obj as Customer).UserName.Equals(this.name); 
    } 
} 

public class RetiredSpecification : Specification 
 { 
    public override bool IsSatisifedBy(object obj) 
    { 
        return (obj as Customer).Age >= 60; 
    } 
} 

public class Program1 
 { 
    static void Main(string[] args) 
    { 
        ICustomerRepository cr; // = new CustomerRepository(); 
        Customer getByNameCustomer = cr.GetBySpecification(new NameSpecification("Sunny")); 
        Customer getByUserNameCustomer = cr.GetBySpecification(new UserNameSpecification("daxnet")); 
        IList<Customer> getRetiredCustomers = cr.GetAllBySpecification(new RetiredSpecification()); 
    } 
} 

经过使用规约,咱们将Customer仓储中全部“特定用途的操做”所有去掉了,取而代之的是两个很是简洁的方法:分别经过规约来得到Customer实体和实体集合。规约模式解耦了仓储操做与断言条件,从此咱们须要经过仓储实现其它特定条件的查询时,只须要定制咱们的Specification,并将其注入仓储便可,仓储的实现无需任何修改。与此同时,规约的引入,使得咱们很清晰地了解到,某一次查询过滤,或者某一次数据校验是以什么样的规则实现的,这给断言条件的设计与实现带来了可测试性。

为了实现复合断言,一般在设计中引入复合规约对象。这样作的好处是,能够充分利用规约的复合来实现复杂的规约组合以及规约树的遍历。不只如此,在.NET 3.5引入Expression Tree之后,规约将有其特定的实现方式,这个咱们在后面讨论。如下是一个经典的实现方式,注意ICompositeSpecification接口,它包含两个属性:Left和Right,ICompositeSpecification是继承于ISpecification接口的,而Left和Right自己也是ISpecification类型,因而,整个Specification的结构就能够当作是一种树状结构。

还记得在《EntityFramework之领域驱动设计实践(八)- 仓储的实现:基本篇》里提到的仓储接口设计吗?当初尚未牵涉到任何Specification的概念,因此,仓储的FindBySpecification方法采用.NET的Func<TEntity, bool>委托做为Specification的声明。如今咱们引入了Specification的设计,因而,仓储接口能够改成:

public interface IRepository<TEntity> 
    where TEntity : EntityObject, IAggregateRoot 
{ 
    void Add(TEntity entity); 
    TEntity GetByKey(int id); 
    IEnumerable<TEntity> FindBySpecification(ISpecification spec); 
    void Remove(TEntity entity); 
    void Update(TEntity entity); 
} 

针对规约模式实现的讨论,咱们才刚刚开始。如今,又出现了下面的问题:

  1. 直接在系统中使用上述规约的实现,效率如何?好比,仓储对外暴露了一个FindBySpecification的接口。可是,这个接口的实现是怎么样的呢?因为规约的IsSatisifedBy方法是基于领域实体的,因而,为了实现根据规约过滤数据,貌似咱们只可以首先从仓储中得到全部的对象(也就是数据库里全部的记录),再对这些对象应用给定的规约从而得到所须要的子集,这样作确定是低效的。Evans在其提出Specification模式后,也一样提出了这样的问题
  2. 从.NET的实践角度,这样的设计,可否知足各类持久化技术的架构设计要求?这个问题与上面第一个问题是一模一样的。好比,LINQ to Entities采用LINQ查询对象,而NHibernate又有其本身的Criteria API,Db4o也有本身的LINQ机制。总所周知,Specification是值对象,它是领域层的一部分,一样也不会去关心持久化技术实现细节。换句话说,咱们须要隐藏不一样持久化技术架构的具体实现
  3. 规约实现的臃肿。根据经典的Specification实现,假设咱们须要查找全部过时的、未付款的支票,咱们须要建立这样两个规约:OverdueSpecification和UnpaidSpecification,而后用Specification的And方法链接二者,再将完成组合的Specification传入Repository。时间一长,项目里充斥着各类Specification,可能其中有至关一部分都只在一个地方使用。虽然将Specification定义为类能够增长模型扩展性,但同时也会使模型变得臃肿。这就有点像.NET里的委托方法,为了解决相似的问题,.NET引入了匿名方法

基于.NET的Specification能够使用LINQ Expression(下面简称Expression)来解决上面全部的问题。为了引入Expression,咱们须要对ISpecification的设计作点点修改。代码以下:

public interface ISpecification 
 { 
    bool IsSatisfiedBy(object obj); 
    Expression<Func<object, bool>> Expression { get; } 
    // Other member goes here... 
 } 
public abstract class Specification : ISpecification 
 { 
    #region ISpecification Members 
    public bool IsSatisfiedBy(object obj) 
    { 
        return this.Expression.Compile()(obj); 
    } 
    public abstract Expression<Func<object, bool>> Expression { get; } 
    #endregion 
 } 

仅仅引入一个Expression<Func<object, bool>>属性,就解决了上面的问题。在实际应用中,咱们实现Specification类的时候,由原来的“实现IsSatisfiedBy方法”转变为“实现Expression<Func<object, bool>>属性”。如今主流的.NET对象持久化机制(好比EntityFramework,NHibernate,Db4o等等)都支持LINQ接口,因而:

  1. 经过Expression能够将LINQ查询直接转交给持久化机制(如EntityFramework、NHibernate、Db4o等),由持久化机制在从外部数据源获取数据时执行过滤查询,从而返回的是通过Specification过滤的结果集,与本来传统的Specification实现相比,提升了性能
  2. 与1同理,基于Expression的Specification是能够通用于大部分持久化机制的
  3. 鉴于.NET Framework对LINQ Expression的语言集成支持,咱们能够在使用Specification的时候直接编写Expression,而无需建立更多的类。好比:
public abstract class Specification : ISpecification 
 { 
    // ISpecification implementation omitted 
  
    public static ISpecification Eval(Expression<Func<object, bool>> expression) 
    { 
        return new ExpressionSpec(expression); 
    } 
} 

internal class ExpressionSpec : Specification 
{ 
    private Expression<Func<object, bool>> exp; 
    public ExpressionSpec(Expression<Func<object, bool>> expression) 
    { 
        this.exp = expression; 
    } 
    public override Expression<Func<object, bool>> Expression 
    { 
        get { return this.exp; } 
    } 
} 

class Client 
{ 
    static void CallSpec() 
    { 
        ISpecification spec = Specification.Eval(o => (o as Customer).UserName.Equals("daxnet")); 
        // spec.... 
    } 
} 

下图是基于LINQ Expression的Specification设计的完整类图。与经典Specification模式的实现相比,除了LINQ Expression的引入外,本设计中采用了IEntity泛型约束,用于将Specification的操做约束在领域实体上,同时也提供了强类型支持。

上图的右上角有个ISpecificationParser的接口,它主要用于将Specification解析为某一持久化框架能够认识的对象,好比LINQ Expression或者NHibernate的Criteria。固然,在引入LINQ Expression的Specification中,这个接口是能够不去实现的;而对于NHibernate,咱们能够借助NHibernate.Linq命名空间来实现这个接口,从而将Specification转换为NHibernate Criteria。相关代码以下:

internal sealed class NHibernateSpecificationParser : ISpecificationParser<ICriteria> 
{ 
    ISession session; 

    public NHibernateSpecificationParser(ISession session) 
    { 
        this.session = session; 
    } 
    #region ISpecificationParser<Expression> Members 

    public ICriteria Parse<TEntity>(ISpecification<TEntity> specification) 
        where TEntity : class, IEntity 
    { 
        var query = this.session.Linq<TEntity>().Where(specification.GetExpression()); 

        //Expression<Func<TEntity, bool>> exp = obj => specification.IsSatisfiedBy(obj); 

        //var query = this.session.Linq<TEntity>().Where(exp); 

        System.Linq.Expressions.Expression expression = query.Expression; 
        expression = Evaluator.PartialEval(expression); 
        expression = new BinaryBooleanReducer().Visit(expression); 
        expression = new AssociationVisitor((ISessionFactoryImplementor)this.session.SessionFactory) 
            .Visit(expression); 
        expression = new InheritanceVisitor().Visit(expression); 
        expression = CollectionAliasVisitor.AssignCollectionAccessAliases(expression); 
        expression = new PropertyToMethodVisitor().Visit(expression); 
        expression = new BinaryExpressionOrderer().Visit(expression); 

        NHibernateQueryTranslator translator = new NHibernateQueryTranslator(this.session); 
        var results = translator.Translate(expression, ((INHibernateQueryable)query).QueryOptions); 
        ICriteria ca = results as ICriteria; 
        
        return ca; 
    } 

    #endregion 
} 

其实,Specification相关的话题远不止本文所讨论的这些,更多内容须要咱们在实践中发掘、思考。本文也只是对规约模式及其在.NET中的实现做了简要的讨论,文中也会存在欠考虑的地方,欢迎各位网友各抒己见,提出宝贵意见。

EntityFramework之领域驱动设计实践【扩展阅读】:服务(Services)

服务(Services)

从本讲开始,所涉及的DDD话题可能与EntityFramework关系不大了。网友千万别骂我是标题党,呵呵。因为这部份内容并不是是特定于EntityFramework的,更多的是在介绍模式及实践心得,因此EntityFramework的内容就会偏少了。为了使得针对一些话题的讨论可以延续下去,我仍然将这些文章安排在本系列中,但愿读者朋友可以谅解。我也在标题中标注了【扩展阅读】,表示所讨论的内容已经不只仅局限于EntityFramework了。

为了表示补偿,透露一下EntityFramework 4.0的最新特性:EF CTP 4.0在“代码优先”开发模式以及提高开发生产率方面作了重要改进。EF CTP 4.0引入了两种新的类型:DbContext和DbSet。DbContext是ObjectContext的简化版。详情请见http://www.infoq.com/news/2010/07/EF-CTP-4。

言归正传,本文将对DDD中的又一重要角色:服务(Services)作一些简单的介绍。提起服务,不少朋友都会想到“SOA”。而在领域驱动设计里,服务贯穿于整个系统的各个层面。根据应用系统的领域驱动分层思想,服务被归类为:应用层服务、领域服务以及基础结构层服务。应用层服务为表现层提供接口,根据DDD的思想,应用层很薄,不承担任何业务逻辑的处理,仅仅是起到coordination的做用。所以,应用层服务也不会牵涉到业务逻辑。在CQRS模式中,Command Service以及Query Service就是应用层服务。基础结构层服务是显而易见的,好比,邮件发送服务、数据服务、事件总线等等。这些服务是与领域无关的,只跟技术实现相关。假想这样一个例子:将货物从仓库A转移到仓库B,若是转仓成功,则向仓库管理员及操做员发送SMS。这是仓储管理领域常见的业务需求,经典的写法相似以下:

public class TransferService : IDomainService
    {
        public void Transfer(Warehouse a, 
                             Warehouse b, 
                             Item item, Qty qty)
        {
            using (IRepositoryTransactionContext ctx = Ioc.GetService())
            {
                Inventory oItemInA = a.GetInventory(item);
              if (oItemInA.Qty < qty)
              {
                 // raise not enough inventory event or exception
                  throw new Exception();
              }
              Inventory oItemInB = b.GetInventory(item);
              if (oItemInB == null)
                  oItemInB = b.CreateInventory(item);
              oItemInA.Qty -= qty;
              oItemInB.Qty += qty;
              ctx.SaveChanges();
          }
      }
  } 

在上面的伪代码中,咱们已经看到了领域服务(Domain Service)的影子。在DDD里,领域服务用以处理那种“放在哪里都不合适”的业务逻辑。好比上面的转仓业务,从面向对象的角度看,既不是仓库应有的操做,也不是货物(Item)的行为。为了明确领域对象的职责,DDD将这样的业务逻辑“抽”出来,置于领域服务当中。对于发送SMS的需求,就须要由应用层服务经过“协调”进行处理了。好比:在调用了领域服务并得到响应之后,根据响应结果以及外部配置信息,进而调用基础结构层服务(SMSService)实现SMS的发送。

看到这里你会发现,其实哪些业务应该放在实体中,哪些须要使用服务来处理,并无一个绝对的标准。仍是那句老话:凭经验。你还会发现,若是从实体将业务逻辑所有“抽”到服务里,实体将成为仅包含getter/setter的对象,因而贫血模型产生了。正由于DDD提倡面向领域,并将通用语言和领域模型摆在很重要的位置,所以,DDD是不主张贫血模型的。

我的认为,领域服务的引入,增长了模型的抗需求变动的能力。咱们能够经过需求分析,找出业务逻辑中易变的部分,以领域服务的方式“注入”到领域模型中,从此如有需求变动,则能够无需更改任何现有组件,完成业务处理逻辑的改变。[TBD: 这样的想法还有待进一步证明]

有关领域服务的内容,本文暂且讨论这些。读者朋友能够在实践中提出问题,而后在此与你们分享讨论。本文还引出了一个话题,就是应用层服务的协调问题。好比,本文的例子中,是在应用层服务中调用SMSService实现SMS发送,若是直接将这部份内容写在应用层服务中,势必下降系统的扩展性。好比,从此但愿不只要发送SMS,并且还要发送Email。DDD的解决方案是引入事件模型。在完成转仓操做时,向事件总线(Event Bus)发送事件,由事件订阅者Subscriber捕获并处理(Handle)事件。因而,从此咱们只须要实现一个“WarehouseTransferSendEmailEventHandler”的事件处理器,并在Handle Event的调用中,向相关人员发送Email便可。NServiceBus就是一款经典的基于.NET的企业级应用通讯的框架,在基于事件的DDD架构中,NServiceBus发挥了重要做用。

从下一讲开始,我将着重讨论领域事件以及Event Sourcing,并对DDD的CQRS模式做个引子。

EntityFramework之领域驱动设计实践【扩展阅读】:CQRS体系结构模式

CQRS体系结构模式

本文将对CQRS(Command Query Responsibility Segregation,命令查询职责分离)模式作一个相对全面的介绍。能够这么说,CQRS打破了经典的领域驱动设计实践,在应用CQRS的整个过程当中,你将会以另外一种不一样的角度去考虑问题并寻求解决方案。好比,CQRS是事件驱动的体系结构,事件是如何产生如何分发又是如何处理的?事件驱动的体系结构适用于哪些类型的应用系统?CQRS中的仓储,与经典DDD中的仓储又有何异同?等等这些问题,都给咱们留下了无限的思考空间。

背景

在讲CQRS以前,咱们先了解一下CQS(Command-Query Separation,命令查询)模式。名字上看,二者没什么差异,然而CQRS应该说是,在DDD的实践中引入CQS理论而出现的一种体系结构模式。CQS模式最先由著名软件大师Bertrand Meyer(Eiffel语言之父,面向对象开-闭原则OCP提出者)提出,他认为,对象的行为仅有两种:命令和查询,不存在第三种状况。用他本身的话来讲,就是:“提问永远没法改变答案”。根据CQS,任何方法均可以拆分为命令和查询两个部分。好比,下面的代码:

private int i = 0; 

private int Add(int factor) 
{ 
    i += factor; 
    return i; 
} 

能够替换为:

private void AddCommand(int factor) 
{ 
    i += factor; 
} 

private int QueryValue() 
{ 
    return i; 
} 

当命令和查询被分离的时候,咱们将会有更多的机会去把握整个事情的细节。好比咱们能够对系统的“命令”部分和“查询”部分分别采用不一样的技术架构,以使得系统具备更好的扩展性,并得到更好的性能。在DDD领域中,Greg Young和Eric Evans根据Bertrand Meyer的CQS模式,结合实际项目经验,总结了CQRS体系结构模式。

结构

整个系统结构被分为两个部分:命令部分和查询部分。我根据本身的体会,描绘了CQRS的体系结构简图以下,供你们参考。在讨论CQRS体系结构以前,咱们有必要事先弄清楚这样几个概念:对象状态、事件溯源(Event Sourcing)、快照(Snapshots)以及事件存储(Event Store)。讨论的过程当中你会发现,不少概念与咱们以前对经典DDD的理解相比,有着很大的不一样。

 

对象状态

这是一个你们耳熟能详的概念了。什么是对象状态?在被面向对象编程(OOP)“熏陶”了好久的朋友,一听到“对象状态”,立刻想到了一对对的getter/setter属性。尤为是.NET程序员,在C# 3.0及之后版本中,引入了Auto-Property的概念,因而,对象的属性就很容易地成为了对象状态的代名词。在这里,咱们应该看到问题的本质,即便是Auto-Property,它也无非是对对象字段的一种封装,只不过在使用Auto-Property的时候,C#编译器会在后台建立一个私有的、匿名的字段(field),而Property则成为了从外部访问该字段的惟一途径。换句话说,对象的状态是保存在这些字段里的,对象属性无非是访问字段的facade。在这里澄清这样一个事实,就是为了当你继续阅读本文的时候,不至于对事件溯源(Event Sourcing)的某些具体实现感到困惑。在Event Sourcing的具体实现中,领域对象再也不须要具有公有的属性,至少外界没法经过公有属性改变对象状态(即setter被定义为private,甚至没有setter)。这与经典的DDD设计相比,无疑是一个重大改变。例如,如今我要改变某个Customer的状态,若是采用经典DDD的实现方式,就是:

[TestMethod] 
public void TestChangeCustomerName() 
{ 
    IocContainer c = IocContainer.GetIocContainer(); 
    using (IRepositoryTransactionContext ctx = c.GetService<IRepositoryTransactionContext>()) 
    { 
        IRepository<Customer> customerRepository = ctx.GetRepository<Customer>(); 
        Customer customer = customerRepository 
            .Get(Specification<Customer> 
            .Eval(p=>p.FirstName.Equals("sunny") && p.LastName.Equals("chen"))); 
        // Here we use the properties directly to update the state 
        customer.FirstName = "dax";  
        customer.LastName = "net"; 
        customerRepository.Update(customer); 
        ctx.Commit(); 
    } 
} 

如今,不少ORM工具都须要聚合根具备public的getter/setter,这自己就是技术实现上的一种约束,好比某些ORM工具会使用reflection,经过读写对象的property来改变对象状态。为何ORM工具要选择properties,而不是fields?由于这些框架不但愿本身的介入会改变对象对其状态的封装级别(也就是访问限制)。在引入CQRS后,ORM已经没有太多的用武之地了,固然从技术选型的角度看,你仍然能够选择ORM,但就像关系型数据库那样,它已经显得没那么重要了。

事件溯源(Event Sourcing)

在某些状况下,咱们不只须要知道对象的当前状态是什么,并且还须要知道,对象经历了哪些路程,才得到了当前这样的状态。Martin Fowler在介绍Event Sourcing的时候,举了个邮包跟踪(Package Tracking)的例子。在经典的DDD实践中,咱们只能经过Shipment.Location来得到邮包的当前位置,却没办法得到邮包经历过哪些地址而最终到达当前的地址。

为了使咱们的业务系统具备记录对象历史状态的能力,咱们使用事件驱动的领域模型来实现咱们的业务系统。简而言之,就是对模型对象状态的修改,仅容许经过事件的途径实现,外界没法经过任何其余途径修改对象的状态。那么,记录对象的状态修改历史,就只须要记录事件的类型以及发生顺序便可,由于对象的状态是由领域事件更改的。因而,也就能理解上面所讲的为何在Event Sourcing的实现中,领域对象将再也不具备公有属性,或者说,至少再也不具备公有的setter属性。

当对象的状态被修改后,咱们可能但愿将对象保存到持久化机制,这一点与经典的DDD实践上的考虑是相似的。而与之不一样的是,如今咱们保存的已再也不是某个领域对象在某个时间点上的状态,而是促使对象将其状态改变到当前点的一系列事件。由此,仓储(Repository)的实现须要发生变化,它须要有保存领域事件的功能,同时还须要有经过一系列保存的事件数据,重建聚合根的能力。看到这里,你就知道为何会有Event Sourcing这个概念了:所谓Event Sourcing,就是“经过事件追溯对象状态的起源(与通过)”,它容许你经过记录下来的事件,将你的领域模型恢复到以前任意一个时间点。你可能会兴奋地说:个人领域模型开始支持事件回放与模型重建了!

Event Sourcing让咱们“透过现象看本质”,使咱们更进一步地了解到“对象持久化”的具体含义,其实也就是对象状态的持久化。只不过,Event Sourcing并非直接保存了对象的状态,而是一系列引发状态变化的领域事件。

仍然以上面的更改客户姓名为例,在引入领域事件与Event Sourcing以后,整个模型的结构发生了变化,如下是相关代码,仅供参考。

[Serializable] 
public partial class CustomerCreatedEvent : DomainEvent 
 { 
    public string UserName { get; set; } 
    public string Password { get; set; } 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public DateTime DayOfBirth { get; set; } 
} 

[Serializable] 
public partial class ChangeNameEvent : DomainEvent 
 { 
    public string FirstName{get;set;} 
    public string LastName{get;set;} 
} 

public partial class Customer : SourcedAggregationRoot 
 { 
    private DateTime dayOfBirth; 
    private string userName; 
    private string password; 
    private string firstName; 
    private string lastName; 

    public Customer(string userName, string password,  
        string firstName, string lastName, DateTime dayOfBirth) 
    { 
        this.RaiseEvent<CustomerCreatedEvent>(new CustomerCreatedEvent 
        { 
            DayOfBirth = dayOfBirth, 
            FirstName = firstName, 
            LastName = lastName, 
            UserName = userName, 
            Password = password 
            
        }); 
    } 

    public void ChangeName(string firstName, string lastName) 
    { 
        this.RaiseEvent<ChangeNameEvent>(new ChangeNameEvent 
        { 
            FirstName = firstName, 
            LastName = lastName 
        }); 
    } 

    // Handles the ChangeNameEvent by using Reflection 
    [Handles(typeof(ChangeNameEvent))] 
    private void DoChangeName(ChangeNameEvent e) 
    { 
        this.firstName = e.FirstName; 
        this.lastName = e.LastName; 
    } 

    // Handles the CustomerCreatedEvent by using Reflection 
    [Handles(typeof(CustomerCreatedEvent))] 
    private void DoCreateCustomer(CustomerCreatedEvent e) 
    { 
        this.firstName = e.FirstName; 
        this.lastName = e.LastName; 
        this.userName = e.UserName; 
        this.password = e.Password; 
        this.dayOfBirth = e.DayOfBirth; 
    } 
} 

上面的代码中定义了两个Domain Event:CustomerCreatedEvent和ChangeNameEvent。在Customer聚合根的构造函数中,直接触发CustomerCreatedEvent以便该事件的订阅者对Customer对象进行初始化;而在Customer聚合根的ChangeName方法中,则直接触发ChangeNameEvent以便该事件的订阅者对Customer的first name和last name做修改。Customer的基类SourcedAggregationRoot则在领域事件被触发的时候经过Reflection机制得到内部的事件处理函数,并调用该函数完成事件处理。在上面的例子中,也就是DoChangeName和DoCreateCustomer这两个方法。在这里须要注意的是,相似DoChangeName和DoCreateCustomer这样的事件处理函数中,仅容许包含对对象状态的设置逻辑。由于若是引入其它操做的话,很难保证经过这些操做,对象的状态不会发生改变。

深刻思考上面的设计会发现一个问题,也就是当模型对象变得很是庞大,或者随着时间的推移,领域事件将变得愈来愈多,因而经过Event Sourcing来重建聚合根的过程也会变得愈来愈耗时,由于每一次从建都须要从最先发生的事件开始。为了解决这个问题,Event Sourcing引入了“快照(Snapshots)”。

快照(Snapshots)

Snapshot的设计其实很简单。标准的CQRS实现中,采用“每产生N个领域事件,则对对象作一次Snapshot”的简单规则。设计人员其实能够根据本身的实际状况定义N的取值,甚至能够选用特定的Snapshot规则,以提升对象重建的效率。当须要经过仓储得到某一个聚合根实体时,仓储会首先从Snapshot Store中得到最近一次的快照,而后再在由此快照还原的聚合根实体上逐个应用快照以后所产生的领域事件,由此大大加速了对象重建的过程。快照一般采用GoF Memento模式实现。请注意:CQRS引入快照的概念仅仅是为了解决对象重建的效率问题,它并不能替代领域事件所能表述的含义。换句话说,即便引入快照,也不能表示咱们可以将快照以前的全部事件从事件存储(Event Store)中删除。由于,咱们记录领域事件的目的,是为了Event Sourcing,而不是Snapshots。

 

事件存储(Event Store)

一般,事件存储是一个关系型数据库,用来保存引发领域对象状态更改的全部领域事件。如上所述,在CQRS结构的系统实现中,数据库已经再也不直接保存对象的当前状态了,保存的只是引发对象状态发生变化的领域事件。因而,数据库的数据结构很是单一,就是单纯的领域事件数据。事件数据的写入、读取都变得很是简单高速,根本无需ORM的介入,直接使用SQL或者存储过程操做事件存储便可,既简单又高效。读到这里,你会发现,虽然系统是用的一个称之为Event Store的机制保存了领域事件,但这个Event Store已经成为了整个系统数据存储的核心。更进一步考虑,Event Store中的事件数据是在仓储执行“保存”操做时,从领域模型中收集并写入的,也就意味着,最新的、最真实的数据仍然存在于领域模型中,正好符合DDD面向领域的思想,同时也引出了另外一深层次的考虑:In Memory Domain!

回到结构

在完成对“对象状态”、“事件溯源(Event Sourcing)”、“快照(Snapshots)”以及“事件存储(Event Store)”的讨论后,咱们再来看整个CQRS的结构,这样就显得更加清楚。上文【CQRS体系结构模式】图中,用户操做被分为命令部分(图中上半部分)和查询部分(图中下半部分)。

  1. 用户与领域层的交互,是以命令的方式进行的:用户经过Command Service向领域模型发送命令。Command Service一般被实现为.NET WCF Service。Command Bus在接收到命令后,将命令指派到命令执行器由其负责执行(能够参考GoF Command模式。TBD: 能够选择更符合CQRS实现的其它途径)。命令执行器在执行命令时,经过领域事件更改对象状态,并经过仓储保存领域对象。而仓储并不是直接将对象状态保存到外部持久化机制,而仅仅是从领域对象中得到已产生的一系列领域事件,并将这些事件保存到Event Store,同时将事件发布到事件总线Event Bus
  2. Event Handler能够订阅Event Bus中的事件,并在事件发生时做相关处理。上文在讨论服务的时候,有个例子就是利用基础结构层服务发送SMS消息,在CQRS的体系结构中,咱们彻底能够在此订阅Warehouse Transferred事件,并调用基础结构层服务发送SMS消息。Domain Model彻底不知道本身的内部事件被触发后,会出现什么状况,而Event Handler则会处理这些状况(Domain Model与基础结构层彻底解耦)
  3. 在Event Handler中,有一种特殊的Event Handler,称之为Synchronizer或者Denormalizer,其做用就是为了同步“Query Database”。Query Database是为查询提供数据源的存储机制,用户在UI上看到的查询数据均来源于此数据库。所以,CQRS不只分离了用户操做,并且分离了数据源,这样作的一个最大的优势就是,设计人员能够根据UI的需求来配置和优化Query Database,例如,能够将Query Database设计为一张数据表对应一个UI界面,因而,用户查询变得很是灵活高效。这里也能够使用DDD中的Repository结合ORM实现数据读取,与处于Domain Layer中的Repository不一样,这个Repository就是DDD中所提到的经典型仓储了,你能够灵活地使用规约模式。固然,你也能够不使用ORM而直接SQL甚至No SQL,一切取决于用户需求与技术选型。咱们还能够根据须要,对Synchronizer和Denormalizer的实现采用缓存,好比,对于无需实时更新的内容,能够每捕获N个事件同步一次Query Database,或者当有客户端query请求时,再作一次同步,这也是提升效率的一种有效方法
  4. 用户UI经过Data Proxy得到查询结果数据,WCF将数据以DTO的形式发送给客户端

总结

本文介绍了CQRS模式的基本结构,并对其中一些重要概念做了注释,也是我在实践和思考当中总结出来的内容(PS:转载请注明出处)。学习过DDD而刚刚开始CQRS的朋友,在阅读一些资料的时候势必会感到疑惑,但愿本文可以帮助到这些朋友。好比最开始阅读的时候,我也不知道为何必定要经过领域事件去更改对象状态,而不是在对象状态变动的时候,去触发领域事件,由于当时我仍然但愿可以在Domain Model中方便地使用getter/setter,我当时也但愿可以让Domain Model同时适应于经典DDD和CQRS架构。在通过屡次尝试后发现,这种作法是不合理、不可取的,也正如Udi Dahan所说:CQRS是一种模式,既然是模式,就是用来解决特定问题的。

仍是一句老话:视需求而定。不要由于CQRS因此CQRS。虽然能够很大程度地提高系统性能,虽然能够使系统具备auditing的能力,虽然能够实现Domain-Centralized,虽然可让数据存储变得更加简单,虽然给咱们提供了不少技术选型的机会,可是,CQRS也有不少不足点,好比结构实现较繁杂,数据同步稳定性难以获得保证,事件溯源(Event Sourcing)出错时,模型对象状态的恢复等等。仍是引用Udi Dahan的一句话:简单,但不容易!

EntityFramework之领域驱动设计实践:总结

是时候总结一下本系列文章了。仍是应该自我批评一下,因为我的琐事多,加上工做繁忙,整个系列文章弄了大半年才断断续续写完。在撰写文章的过程当中,也获得了你们的理解与支持,并让更多的朋友开始关注领域驱动设计,非常感激!在接下来的其它博文中,我将继续讨论领域驱动设计的实践经验。

本系列文章首先从领域驱动设计的基础思想出发,讨论了基于.NET EntityFramework的领域驱动设计经验,这包括对实体、值对象、工厂和仓储实现方式的讨论、对EntityFramework所提供的开发工具功能点的讨论,还包括了规约模式及其.NET实现。从讨论中咱们能够了解到,目前Microsoft .NET EntityFramework在对领域驱动设计的支持上仍是有一些不足的地方,好比对值对象的支持、对实体行为以及事件驱动的支持,但同时咱们也看到了.NET在DDD上取得的巨大进步,这包括:具备DSL(Domain Specific Language)特质的语言集成查询(LINQ)、面向实体的领域对象模型(LINQ to SQL是面向数据的)、复合数据类型支持。毕竟.NET EntityFramework是一种产品,它须要考虑广大用户的需求,因此也会包括一些Anti-DDD的元素在里面。

在讨论了经典DDD的EntityFramework实践经验以后,本系列文章还引出了两个扩展话题:服务与CQRS体系结构模式。CQRS体系结构模式是近年来DDD社区总结出来的一种新的DDD实践模式,也是目前DDD社区中讨论的较多的一种体系结构模式。它以CQS思想为基础,以事件溯源(Event Sourcing)为核心,将应用系统的命令与查询实现职责分离,从而得到Event Auditing的功能,同时大大提升了系统的运行效率,并为架构技术选型和团队资源配置(Resource Configuration)带来了广阔空间。有关CQRS的设计与实现,我会在后续的文章中继续介绍。

相关文章
相关标签/搜索