Entity Framework模型在领域驱动设计界定上下文中的应用
2013-03-24 18:44
447 查看
Entity Framework模型在领域驱动设计界定上下文中的应用
【本文翻译自Julie Lerman发表在MSDN Magazine上的一篇技术文章,原文题为《Shrink EF Models with DDD Bounded Contexts》。对自己英语比较自信的朋友可以直接在MSDN Magazine的在线文章收录中阅读原文。】在使用Entity Framework(以下简称EF)来定义模型(Model)时,开发人员往往喜欢把应用程序中的所有模型对象都一股脑地塞进一个模型中。这种开发习惯估计是源于Database First的开发方式,在这种方式下,开发人员可以很方便地将数据库中的表和视图直接拖拽到EF模型设计器中,于是一个模型也就包含了由这些表或视图所映射的所有对象。当然,不正确的Code First的实践方式,同样也会造成这样的局面:在一个DbContext中为模型中的每一个实体都定义DbSet属性,甚至会不知不觉地将与这些实体关联的所有类型全部包含进去。
当开发一个具有大型领域模型的超大规模的应用程序时,与设计一个单一的大领域模型相比,将大领域模型根据应用程序的业务需要“切割”成一系列较小的模型是非常重要的,我们也往往能够从中获得更多的好处。在本文中我将向大家介绍领域驱动设计(DDD)中的一个重要概念:界定上下文(Bounded Context),并向大家展示如何根据界定上下文来设计基于Entity Framework Code First的模型。如果您是第一次接触DDD,那么本文将会为您提供一个很好的了解和学习DDD的机会;如果您已经开始在项目中使用DDD,那么本文或许能够为您提供一些Entity Framework与DDD的实践启示。
领域驱动设计与界定上下文
DDD是一个相当广泛的话题,它囊括了软件设计的所有方面。作为Domain Language(DomainLanguage.com)中DDD workshop的讲师,Paul Rayner是这样概括DDD的:“DDD主张一种更为实用的、覆盖面更广的以及可持续的软件设计方式:通过与领域专家的沟通,将领域模型适配到软件系统中,而正是领域模型帮助我们解决了那些重要的、复杂的业务问题”
DDD包括了很多软件设计模式,在这些众多的模式之中,界定上下文使我们能够很自然地在软件设计中使用Entity Framework。界定上下文主张根据特定的业务领域设计和开发一些较小的模型。Eric Evans在他的《领域驱动设计:软件核心复杂性应对之道》一书中,对界定上下文是这样描述的:“明确定义模型应用的上下文。根据团队组织、应用程序各个部分的使用率、物理显现(如代码库和数据库方案)明确设置界限。在这些界限中要保持模型严格的一致性,但不要被外界问题干扰和迷惑。”(选自《领域驱动设计:软件核心复杂性应对之道》一书,陈大峰、张泽鑫等译,2006年3月版)
更小的模型为我们的软件设计和开发带来了更多的好处,它使得团队能够根据自己的设计和开发职责确定更为明确的工作边界。小的模型也为项目带来了更好的可维护性:由于上下文由边界确定,因此对其的修改也不会给整个模型的其它部分造成影响。更进一步,就Entity Framework而言,相比大模型的读取和加载,小模型不仅加载速度快,而且内存占用也会相对较小,在一定程度上提升了应用程序的性能。
由于我是使用EF的DbContext来设计和开发界定上下文,我原本打算使用“界定的DbContext”这一词语来描述我们的上下文。然而,DbContext与界定上下文之间并不是完全等同的:DbContext是一个技术架构的类型实现,而界定上下文则是在描述一个完整的软件设计过程中的一个更为广泛的概念。因此,使用“限定的”或者“专注的”来描述本文中出现的DbContext或许更为准确。
经典的EF DbContext与界定上下文之间的对比
虽说DDD通常会被用在具有复杂领域业务的大型应用程序的开发之中,中小型应用程序的开发同样也能从DDD的理论和实践中获益。下面,我将以一个具有特定领域业务的应用程序为例,向大家介绍Entity Framework在界定上下文中的应用。该应用程序提供这样一种业务:它能为公司提供跟踪销售和市场信息的业务。通过分析不难发现,整个应用程序将包含多种对象,比如:客户(Customers)、订单(Orders)、订单行项目(Line Items)、产品(Products)、市场(Marketing)、销售人员(SalesPeoples),甚至还会包括公司雇员(Employees)。通常,我们都是在DbContext中定义包含了所有这些对象的DbSet属性,就像下面的代码所示:?
另一方面,对于这个庞大的DbContext类型,我们还会提出这样一些疑问,比如:公司市场部所使用的应用程序部分中,用户是否真的有必要去查询雇员的工资历史信息?运输部是否也需要像客服专员那样,通过应用程序去访问客户的详细数据,甚至是对这些数据进行修改?通常情况下,针对这类问题的答案都是:No。所以您也能够看到,将一个庞大的DbContext类型根据子系统所面对的子领域切分成更小的DbContext类型是完全可行的。
面向运输领域的DbContext的实现
DDD推荐使用具有明确上下文边界的、能够满足特定子领域的更为轻巧的领域模型,现在就让我们一起看看如何将DbContext类型的设计限定到运输领域。于是,你可以从原来那个庞大的DbContext中去除那些与运输领域无关的DbSet,而仅仅包含那些在运输领域中需要用到的对象。在此,我将Returns、Promotions、Categories、Payments、Employees以及SalaryHistories等从原来的DbContext中移除,于是便得到了下面的针对运输领域的DbContext的实现:?
面向运输领域的DbContext的优化:建立更为专注的模型
从上图中我们可以看到,虽然我们已经将与运输领域无关的DbSet属性从DbContext中移除掉了,但所产生的EF模型仍然包含了一些我们所不需要的对象。这是EF Code First根据DbContext产生模型的一种特点:它会自动地分析对象之间的关系,从而将关联对象也一并加入到模型之中。这就是为什么我们已经将Category和Payment的DbSet属性去除之后,这些对象仍然存在于模型中的原因。因此,我们需要重写DbContext的OnModelCreating方法,使得在模型产生的过程中忽略这些对象:?
接下来,我们还能进一步地优化我们的DbContext类,并能同时确保不会影响到产生的模型。现在,我们已经可以很直接地通过DbContext中已有的7个DbSet属性来访问我们需要的对象。但通过对这些对象及其之间关系的分析,我们不难发现,我们几乎不会直接去访问ShippingAddress的信息,因为我们可以通过Customer对象来获得这一信息。由于Customer保持着对ShippingAddress的引用,当EF Code First通过DbContext来生成模型的时候,它也会一并将ShippingAddress带入到模型当中,就像之前Category和Payment那样,所以,我们可以在DbContext中直接将ShippingAddress的DbSet属性移除。当然,在进行了细致的分析之后,或许还能移除其它的DbSet属性,不过为了简化描述,在本案例中我们只针对ShippingAddress的DbSet属性作优化。
?
?
?
另外还有件事情,就是每一条Line Item的数据都会包含一个ShipmentId,在运输领域中,每当一条Line Item完成发货之后,系统都会通过设置ShipmentId来标识当前的Line Item已经发货。因此,与直接使用LineItem这一类型相比,我会另外单独创建一个新的类型来处理这件事情:
?
通过更进一步的优化,我们的ShippingContext已经变成了下面这幅模样:
?
改进后的DbContext与数据库初始化
在应用程序中使用面向某个界定上下文的DbContext时,需要注意EF Code First在根据模型进行数据库初始化的两个默认行为。第一个就是Code First会默认地将DbContext的名称用作数据库名。当我们采用面向界定上下文的DbContext时,比如应用程序中存在ShippingContext、CustomerContext以及SalesContext等等时,我们就需要规避Code First的这一行为,我们需要让这些DbContext公用同一个数据库。
EF的另一个默认行为就是,Code First会根据DbContext所创建的模型来建立数据库结构(database schema)。然而现在我们的DbContext只包含了数据库中某一个部分的定义。因此,我们不需要EF通过DbContext类型来初始化数据库。
我们可以通过每个DbContext类的构造函数来解决这些问题。例如,在ShippingContext类中,你可以在构造函数中明确指定使用DPSalesDatabase数据库,并且禁用数据库初始化:
?
?
?
面向特定领域DbContext的实践
在完成了以上一系列工作之后,我写了一些自动化集成测试脚本,对以下应用场景进行了测试:获取所有未发货的项目
获取有着未发货项目的订单相关的OrderShippingDetails信息,这些信息还包含了Customer和Shipping的信息
获取一条未发货的项目,并创建一个发货对象。然后将这个发货对象设置到Line Item上,同时在数据库中新建一条发货的记录,并在数据库中将发货对象的键值更新到Line Item上
这些应用场景基本上覆盖了运输领域绝大部分的业务功能。在执行了测试脚本后,所有的测试都很成功,这也证明了我们的DbContext能够正常运行。这些测试也同样包含在了与本文相关的案例下载中。
总结
我们不仅针对运输领域创建了一个面向特定领域(界定上下文)的DbContext,而且通过这个思考过程,我们也创建了一些更为高效的领域对象(比如ItemToBeShipped等)。将DDD中的界定上下文定位在运输领域以及在这个上下文中使用DbContext,这就意味着我们不需要在应用程序的运输领域部分涉及过多的无关对象,因此我可以在不影响其它子领域或者说其它团队工作的前提下,在我自己所关注的领域中使用这些有限的相关的对象。你可以通过阅读我和Rowan Miller合作的《Programming Entity Framework: DbContext》((O’Reilly Media, 2011))一书来更多地了解DDD中的界定上下文,以及在界定上下文中应用面向特定领域DbContext的实践案例程序。
相关文章推荐
- Entity Framework模型在领域驱动设计界定上下文中的应用
- TDD应用试例(根据领域驱动模型设计的培训内容)
- 【译文】借助“上下文地图”进行战略级别的“领域驱动设计”
- 领域驱动设计之领域模型
- 领域模型驱动设计(DDD)之模型提炼
- 领域驱动设计模型和Smark UI
- 领域模型驱动设计(Domain Driven Design)入门概述 -----DDD 解释
- 领域驱动设计在大规模项目中的应用心得 --发表于《软件世界》08年第1期
- 领域驱动设计学习-让领域模型发挥作用
- 领域模型驱动应用心得
- Byteart Retail V3 - 全新的面向.NET与领域驱动设计的企业应用实践案例
- 领域驱动设计(3)模型驱动设计
- 领域模型驱动设计(Domain Driven Design)入门概述
- 一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?
- 贫血模型;DTO:数据传输对象(Data Transfer Object);AutoMapper ;Domain Model(领域模型);DDD(领域驱动设计)
- 说说领域驱动设计和贫血、失血、充血模型
- 浅谈领域模型驱动中表的设计方法
- 【tornado】系列项目(一)之基于领域驱动模型架构设计的京东用户管理后台
- 【无私分享:ASP.NET CORE 项目实战】EntityFramework下领域驱动设计的应用
- 领域驱动设计之领域模型