从DataTable到EntityObject
虽然从技术角度讲,DataTable与EntityObject并没有什么可比性,然而,它暗示了一场革命正在悄然进行着,即使是微软,也摆脱不了这场革命的飓风。
软件设计思想需要革命,需要摆脱原有的思路,而走向面向领域的道路。你或许会觉得听起来很玄乎,然而目前软件开发的现状使你不得不接受这样的现实,仍然有大帮的从业人员成天扯着数据库不放,仍然有大帮的人在问:“我要实现xxxx功能,我的数据库应该如何设计?”这些人犯了根本性的错误,就是把软件的目的搞错了,软件研究的是什么?是研究如何使用计算机来解决实际(领域)问题,而不是去研究数据应该如何保存更合理。这方面的事情我在我以前的博文中已经说过很多次了,在此就不再重复了。
当然,很多读者会跟我有着相同的观点,也会觉得我很“火星”,但这都不要紧,上面我所说的都是一个引子,希望能够帮助更多“步入歧途”的从业人员“走上正路”。不错,软件设计应该从“数据库驱动”走向“领域驱动”,而领域驱动设计的实践经验正是为设计和开发大型复杂的软件系统提供了实践指导。
回到我们的副标题,从DataTable到EntityObject,你看到了什么?看到的是微软在领域驱动上的进步,从DataTable这一纯粹的数据组织形式,到EntityObject这一实体对象,微软带给我们的不仅仅是技术框架,更是一套面向领域的解决方案。
.NET 4.0来了,随之而来的是实体框架(Entity Framework,简称“EF”),在本系列文章中,我将结合领域驱动设计的实践知识,来剖析EF的具体应用过程,当然,现在的EF还并不是那么完善,我也非常期待能够看到,今后微软能够继续发展和完善EF,使其成为微软领域驱动工具箱中的重要角色。
先不说EF,首先我们简要地分析一下,作为一种框架,要支持领域驱动的思想,需要满足哪些硬性需求,之后再仔细想想,.NET
EF是否真的能够很好地应用在领域驱动上。
- 首先需要能够正确对待“领域模型”的概念。领域模型与数据模型不同,它表述的是领域中各个类及其之间的关系。类关系是多样的,比如组合、聚合、继承、实现、约束等,而数据模型不是一对多,就是多对多。从领域驱动设计的角度看,数据库只不过是存储实体的一个外部机制,是属于技术层面的东西。数据模型主要用于描述领域模型对象的持久化方式,应该是先有领域模型,才有数据模型,领域模型需要通过某种映射而产生相应的数据模型。因此,框架必须支持从领域模型到数据模型的映射。
EF不仅支持从领域模型生成数据库的DDL,而且支持从数据库结构来生成“领域模型”。我倒是觉得后者可以去掉,因为从数据库得到的已经不是领域模型了。你会问为什么,我可以告诉你,单纯的数据是没办法描述领域概念的。比如:你的数据库里有一个表叫做“Customer”,在通过数据库结构生成“领域模型”的时候,Visual
Studio当然会帮你生成一个“领域对象”叫做Customer,但如果我把这数据表的名字改为“abc”,虽然里面还是存的客户信息,但EF并不知道这一点,也是照样生成一个“abc”的类,而这样的东西能算是领域对象吗?因此,数据依赖于实体,是实体的状态,离开实体的数据毫无意义
- 对“聚合”概念的支持。聚合是一系列表述同一概念的相互关联的实体的集合,比如销售订单、销售订单行以及商品,这三个实体可以整合成一个聚合,销售订单则是聚合的根。关于聚合的问题将在后续文章中讨论。为什么引入聚合?这是领域驱动设计处理大型软件系统的一种独到的方式。软件系统的实体对象可能非常多,之间的关系也可能错综复杂,那么,当我们需要从外部持久化机制“唤醒”(或者说读取)某个实体对象的时候,是不是需要将它以及它所关联的所有对象都一并读入内存呢?当然不是,因为我们只需要关注整个问题的某个方面。比如在读取客户数据的时候,我们或许会将这个客户的所有订单显示出来,但不会将每个订单的订单行也全部读出来,因为现在我们还没有决定是否真的需要查看所有的订单行。
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上应用领域驱动设计的思想,进而完成我们的案例程序。本系列文章均为我个人原创,或许在某些问题上你会有不同意见,不要紧,你可以直接签写留言,或者发邮件给我,期待与你的探讨,期待与你在软件架构设计的道路上共同进步。
分层架构
在引入实例以前,我们有必要回顾,并进一步了解分层架构。“层”是一种体系结构模式[POSA1],也是被广大软件从业人员用得最为广泛而且最为灵活的模式之一。记得在CSDN上,时常有朋友问到:“分层是什么?为什么要分层?三层架构是不是就是表现层、业务逻辑层和数据访问层?”
到这里,你可能会觉得这些朋友的问题很简单,分层嘛,不就是将具有不同职责的组件分离开来,组成一套层内部高聚合,层与层之间低耦合的软件系统吗?不错!这是分层的目标。但是,我们应该如何分层呢?
领域驱动设计的讨论同样也是建立在层模式的基础上的,但与传统的分层架构相比,它更注重领域架构和技术架构的分离。
传统的三层架构
如上文那位朋友提的问题那样,最简单的分层方式自然就是“表现层、业务逻辑层和数据访问层”,我们可以用下图来表示这个思想:
注意图中打虚线的“基础结构层”,从实践的表现上来看,这部分内容可能就是一些帮助类,比如SQLHelper之类的,也可能是一些工具类,比如TextUtility之类。这些东西可以被其它各层所访问。而基于分层的概念,表现层只能跟业务逻辑层打交道,而业务逻辑层在数据持久化方面的操作,则依赖于数据访问层。表现层对数据访问层的内容一无所知。
从领域驱动的角度看,这种分层的方式有一定的弊端。首先,为各个层面提供服务的“基础结构层”的职责比较紊乱,它可以是纯粹的技术框架,也可以包含或处理一定的业务逻辑,这样一来,业务逻辑层与“基础结构层”之间就会存在依赖关系;其次,这种结构过分地突出了“数据访问”的地位,把“数据访问”与“业务逻辑”放在了等同的地位,这也难怪很多软件人员一上来就问:“我的数据表该如何设计?”
领域驱动设计的分层
领域驱动设计将软件系统分为四层:基础结构层、领域层、应用层和表现层。与上述的三层相比,数据访问层已经不在了,它被移到基础结构层了。
- 基础结构层:该层专为其它各层提供技术框架支持。注意,这部分内容不会涉及任何业务知识。众所周知的数据访问的内容,也被放在了该层当中,因为数据的读写是业务无关的
- 领域层:包含了业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。这部分内容的具体表现形式就是领域模型(Domain
Model)。领域驱动设计提倡富领域模型,即尽量将业务逻辑归属到领域对象上,实在无法归属的部分则以领域服务的形式进行定义。有关领域对象和领域服务的内容,我会在接下来的案例中进行阐述
- 应用层:该层不包含任何领域逻辑,但它会对任务进行协调,并可以维护应用程序的状态,因此,它更注重流程性的东西。在某些领域驱动设计的实践中,也会将其称为“工作流层”。应用层是领域驱动中最有争议的一个层次,也会有很多人对其职责感到模糊不清。比如,有些国外的开发人员会觉得,既然不包含领域逻辑,那又如何协调工作任务呢?我会在《应用层与实体事件》章节对这些问题进行探讨
- 表现层:这个好理解,跟三层里的表现层意思差不多,但请注意,“Web服务”虽然是服务,但它是表现层的东西
从上图还可以看到,表现层与应用层之间是通过数据传输对象(DTO)进行交互的,数据传输对象是没有行为的POCO对象,它的目的只是为了对领域对象进行数据封装,实现层与层之间的数据传递。为何不能直接将领域对象用于数据传递?因为领域对象更注重领域,而DTO更注重数据。不仅如此,由于“富领域模型”的特点,这样做会直接将领域对象的行为暴露给表现层。
从下一个章节开始,我将以最简单的销售系统为例,介绍实体框架(Entity Framework,简称EF)下领域驱动设计的应用。
案例:一个简易的销售系统
从现在开始,我们将以一个简易的销售系统为例,探讨Entity Framework(实体框架,简称EF)在领域驱动设计上的应用。为了方便讨论,我们的销售系统非常简单,不会涉及客户存在多个收货地址的情况,也不会包含任何库存管理的内容。假设我们的系统只需要维护产品类型、产品以及客户信息,并能够帮客户下订单、跟踪订单状态,以及接受客户退货。从简单的分析我们大致可以了解到,这个系统将会有如下实体:客户、单据、产品及其类型。单据分为销售订单和退货单两种,每个单据可以有多个单据行(比如销售订单行和退货单行)。不仅如此,系统允许每个客户有多张信用卡,以便在结账的时候,选择一张信用卡进行支付。在使用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表示退货单),那么,在获取系统中所有销售订单的时候,我们会使用下面的代码:
从技术角度讲,上面的代码没什么问题,运行的也很好,能够获得系统中所有销售订单的列表。但是,[OrderType]=0这种写法并不包含任何领域语义,如果让另一个开发人员来跟进这段代码,他不得不先去查阅其它项目文档,以了解这个[OrderType]=0的具体涵义。在引入了继承关系的EF中,我们只需要下面的Linq
to Entities,即可既方便、又明了地获得所有销售订单的列表了:
简单明了吧?EF带给我们的不仅仅是一个技术框架,也不仅仅是一个数据存取的解决方案。
值对象
EF支持值对象,这很好!在EF中可以定义Complex Types,而一个Complex
Type下可以定义多个Primitive Type和多个Complex Type。与LINQ to SQL相比,这是一大进步。
对于值对象的两点问题我在第一篇文章中已经讲过了,在此就不重复了。
综上所述,EF基本上能够支持领域驱动设计的思想(即使有些方面不完善,但目前也可以找到替代的方案)。我想,只要能够对领域驱动有清晰的认识,就能够很好地将实体框架应用于领域驱动的实践中。
|