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