引言
本文主要是参考Martion Fowler所著的《企业应用架构模式》与Eric Evans所著的《领域驱动设计》这两本泰山之作,加上本人在近年实际的工作过程中开发SOA系统所认识到的问题所写的一篇文章,欢迎各位点评。
最后两节 细说应用层 、系统总体架构 是本文的重点,着重说明领域驱动设计与SOA之间的关系,对DDD有一定基础的朋友可以越过前面的几节,直接查看第七、八节。
源代码下载 (数据库可以在.edmx文件根据模型生成)
目录
一、SOA与DDD的定义
二、DDD的分层结构
三、把业务关系转化为领域模型
四、细说Repository
五、领域层的服务
六、工厂模式Factory
七、细说应用层
八、系统总体架构
一、SOA与DDD的定义
SOA与DDD都是常用的系统架构,但两者之间所针对的核心是不同的。
SOA(面向服务架构)由Gartner 在1996年提出来,它是一种分布式的软件架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行部署、组合和使用。简单来说,SOA就是一种大型系统开发的体系架构,在基于SOA架构的系统中,具体应用程序的功能是由一些松耦合并且具有统一接口的组件(也就是service)组合构建起来的,它是针对多核心多平台之间的数据交换。
DDD(领域驱动设计)由Eric Evans在2004提出,它的核心内容是“如何将业务领域概念映射到软件工程当中”。它推翻了“软件从数据层开发设计”的旧习惯,强调领域模型在软件中发挥的强大力量,注重如何把企业内部复杂的业务流程转化为软件。
也许可以认为SOA针对的是大型系统的总体架构,注重如何把系统进行项目分离,隔离开发,最后实现系统合并。而DDD是针对单个项目的开发管理过程,注重如何利用领域模型把业务需求转化为软件。两者之间并没有存在理论上的冲突,能把两者结合,各展所长,更能发挥各自的优势。
二、DDD的分层结构
1. 概念
从概念上来说,领域驱动设计架构主要分为基础设施层、领域层、应用层、表现层4个概念层。
基础结构层:是为各层提供各项通用技术能力而构建的,它可以为领域层提供像Hibernate、LINQ、ADO.NET等持久化机制,为应用层传递消息,为表现层提供插件等等。
领域层:它是系统的核心部分,代表业务的逻辑概念。它会根据业务的实际流程定制了业务信息以及业务规则,并按一定的关系制定领域模型。领域模型尽管需要依赖基础结构层进行保存,但领域模型之间的逻辑关系是跟基础结构层相隔离的。即使基础结构层从NHibernate技术转换成LINQ技术,也不会影响到领域层的结构。领域模型只会依赖实际的业务逻辑,它只会根据业务的转变而灵活变动。
应用层:它的任务是协调领域层与表现层之间的关系,也可以作为系统与外界沟通的桥梁,在这层里面不会包括任何的业务逻辑。在SOA面向服务架构,这一层起着重要的作用,在第七节将详细说明。
表现层:它是常用的界面开发,可以以页面(ASP.NET、JSP),窗口(WinForm、WPF、Swing)等形式表现,它的主要职责是负责与用户进行信息沟通(注意:在一般的项目开发中,Web服务会作为与外界通讯的接口放置在表现层中,但在SOA中,Web服务会大多置于应用层中,下面将会作进一步解释)
2. 开发实例
在此先举个常见的订单管理例子,在下面的章节里都会以这个实例为参考:
每个用户在Person表里面都会有一个对应的帐户,里面记录了用户的姓名、地址、电话、积分(Point)等基本信息。
在Order表里记录的是每次交易的过程,每次商品的送货费(Freightage)为10元,当商品价格(Price)超过98元可免费送货,当用户Person积分(Point)超过2000分可获7折优惠(Favorable),1000~2000分可获8折,1000分以下可有9折,最后总体价格为为(TotalPrice)。
在最后结单的时候Order表里会产生订单号码OrderNumber和下订日期Delivery,Person表的积分也会加上订单总价的点数。
最后OrderItem表包含了物品Goods、物品价格Price、购买数量Count等属性,它主要记录每次订单的详细交易状况。
上面的业务逻辑跟淘宝、当当等等大型购物网基本相似。之所以用这样一个例子作为参考,是想表现一下DDD是如果利用领域模型去适应多变的业务逻辑关系。
三、把业务关系转化为领域模型
1. 概念
模型驱动设计设计(MODEL-DRIVEN-DESIGN)是DDD里面的核心,它代表的是各个对象之间的关系,把复杂的逻辑关系转化为模型。
模型主要分为实体(Entity)、值对象(Value Object)与服务(Service)三种。
实体:实体所包含的不单止是一连串的属性,更重要的是与事件的联系,在一个生命周期中环境的变化与事件发生,将引起实体内部产生变化。好像在实体Order里面,Person的积分(Point)和OrderItem的价格(Price)都会直接影响总体价格(TotalPrice)的大小,而总体价格也会影响到运费Freightage的多少等等。在Order实体的一切,都会受到Person、OrderItem等这些外部因数的影响,这样的对象被视为实体。在不同的时刻,实体会有不同的状态,所以在开发过程中我们需要为实体加上一个“标识符”来区分对象的身份,它是实体的生命周期里的唯一标志。
值对象:当所用到的对象只有属性而没有其他逻辑关系的时候,我们就可以把它视为是值对象。值对象没有状态,也不需要有 “标识符”。在多数情况下它可以作为一个属性存在于一个实体的内部。一般情况下值对象的属性是不可改变的,当需要更改属性时,可以把整个对象删除,然后重新加入一个新对象。
服务:当实体之间存在某些操作,它们并不单一地附属于某一个实体,而是跟多个实体都有关联的时候,就可以使用服务来封装这些操作。值得注意的是服务并非单独指Web Service, 也并非单单存在于领域层,而是在各个层当中都会存在服务,每一层的服务都有着不同的职能。在基础结构层服务可能是用于构建身份验证、电子邮件、错误处理等等操作;在领域层,服务更多时候是一种操作,它用于协调多个实体之间的关系,处理各类的业务问题;在应用层(特别是在分布式开发系统内),服务多以Web Service、TCP/IP套接字、MSMQ等等方式实现,服务在此处会作为一个与外界通讯的接口;
备注 :这里面也存在一定的争义,Eric 认为实体所代表的只是多个对象之间的关系,而它们的动作将会由服务来体现出来,这被称为贫血型模型。但在开发过程中,越来越多人会把动作加入到实体里面,这被称为充血型模型。其实不同的问题应该客观分析,分别对待,在这个例子里面将会以按照 Eric 的定义来开发服务,在后面的开发过程中大家也可以从中体现一下服务层所带来的好处。
2. 实例说明
先以ADO.NET Entity Framework实现模型,Person、Order分别属于两个实体,它们都将继承Root接口,在它们的生命周期内都会生成一个Guid作为标志。此处把OrderItem作为一个值对象置于Order实体内,这意味着OrderItem会通过Order来获取,外界不能跨越Order直接获取OrderItem。当然这应该由具体的业务情况来确定,当外界需要单独调用OrderItem类的时候,就应该考虑把OrderItem独立成为一个实体类。
在这里可利用分部类为实体增加Guid属性,关于分部类于分部方法的详细介绍可参考C#综合揭秘——分部类和分部方法
namespace Business.DomainModel { public interface Root { } public partial class Order:Root { private Guid _guid; public Order() { _guid = System.Guid.NewGuid(); } //为根对象设置唯一的Guid; public Guid GUID { get { return _guid; } } } public partial class Person:Root { public Person() { _guid = System.Guid.NewGuid(); } //为根对象设置唯一的Guid; private Guid _guid; public Guid GUID { get { return _guid; } } } } |
四、细说Repository
1.概念
Repository是把持久化对象转换成领域模型的一种方式,可用于获取、更新持久对象并管理它们的生命周期。它使应用程序与持久化技术实现解耦,程序无需受制于使用Oracle还是MySql数据库,也不会受到Hibernate、LINQ、ADO.NET等数据层的约束,使开发人员可以把注意力集中到领域模型当中。
Repository与传统三层模式的DAL层有点相似,但Repository针对的是每一个根对象来划分边界的。在这个例子当中,Person与Order都会有对应的PersonRepository、OrderRepository。而OrderItem只是Order的子属性,所以它的插入、更新、删除都会包含在OrderRepository当中。当多个对象之间建立起联系后,关系将是复杂的,特别是在LINQ里面,程序可以轻易通过Person的导航属性里获取OrderItem的值,最后很容易使代码变得混乱。所以确立Repository的边界,可以在有效管理每个Repository的职能。
2.实例说明
注意OrderItem的存取、删除都包含在OrderRepository里面。在获取、修改Order的时候,也会利用“显式加载” context.Order.Include("OrderItem") 的方法,使OrderItem实现同步更新。而通过PersonRepository.GetPerson(int )获取的Person对象,它内部的Order属性将是null值,这必须清晰按照领域模型的边界划分的。
当LINQ面世以后,数据的获取变得简单,特别在一些小型的系统开发时,很多人会不自觉地把这种领域模型的分界规则打破。但随着系统的复杂化,问题就会逐渐呈现。比如当Order对象的属性被更新,使用OrderRepository.Update(Order)更新数据库后,页面的Person对象未能同步实现更新,在Person与数据库交换数据的时候,Order又被变回旧值。
在混乱的数据层开发中,这种情况非常常见,所以在下会坚持Repository的原则,把Repository的职能清晰按照领域模型划分。
namespace Business.IRepository { public interface IOrderRepository { Order GetOrder(int id); IListGetList(); IListGetListByPerson(int personID); int AddOrder(Order order); int DeleteOrder(int id); int UpdateOrder(Order order); int AddOrderItem(OrderItem orderItem); int DeleteOrderItem(int id); } public interface IPersonRepository { int AddPerson(Person person); int AttachPerson(Person person); int UpdatePerson(Person person); Person GetPerson(int id); IListGetList(); } } namespace Business.Repository { public class OrderRepository:IOrderRepository { //根据ID获取单个Order public Order GetOrder(int id) { BusinessContext _context = new BusinessContext(); Order order = null; try { using (TransactionScope scope = new TransactionScope()) { //由于OrderItem是Order实体中的一个属性,必须通过OrderRepository同步获取 var list = _context.Order.Include("OrderItem") .Where(x => x.ID == id); if (list.Count() > 0) order = list.First(); else order = new Order(); scope.Complete(); } } catch (Exception ex) { //出错处理,并返回一个空对象 Business.Common.ExceptionManager.DataException.DealWith(ex); order = new Order(); } _context.Dispose(); return order; } .................. .................. } public class PersonRepository:IPersonRepository { public int AddPerson(Person person) { return LinqHelp.Add(person); } public Person GetPerson(int id) { return LinqHelp.Get(id); } ................. ................. } } |
在更新Order这种复杂的领域模型时,如果要分辨单个OrderItem属性是新建值还是更新值,然后分别处理,那将是比较麻烦的,而且OrderItem只是一个值对象,ID编码等属性对它没有任何实在意义。所以在更新List<OrderItem>属性时都会先把它全部删除,然后重新加载,在OrderItem数量不多的时候,这是一种十分有效的方法。
namespace Business.Repository { public class OrderRepository:IOrderRepository { ................. ................. //更新Order,因为难以别哪些是原有的OrderItem,哪些OrderItem是新插入 //使用简单的方法,会先把原有的OrderItem的删除,再重新插入 public int UpdateOrder(Order order) { int returnValue = -1; BusinessContext _context = new BusinessContext(); try { using (TransactionScope scope = new TransactionScope()) { var list = _context.Order.Include("OrderItem") .Where(x => x.ID == order.ID); if (list.Count() > 0) { //更新Order列 Order _order = list.First(); _order.Count = order.Count; _order.Delivery = order.Delivery; _order.Favorable = order.Favorable; _order.Freightage = order.Freightage; _order.OrderNumber = order.OrderNumber; _order.PersonID = order.PersonID; _order.Price = order.Price; _order.TotalPrice = order.TotalPrice; //删除原有的订单明细项OrderItem if (list.First().OrderItem.Count != 0) foreach (var item in list.First().OrderItem) DeleteOrderItem(item.ID); //加入新的订单明细项OrderItem if (order.OrderItem.Count != 0) { foreach (var item in order.OrderItem) { var _orderItem = new OrderItem(); _orderItem.Count = item.Count; _orderItem.Goods = item.Goods; _orderItem.OrderID = item.OrderID; _orderItem.Price = item.Price; AddOrderItem(_orderItem); } } returnValue = _context.SaveChanges(); } else returnValue = 0; scope.Complete(); } } catch (Exception ex) { Business.Common.ExceptionManager.DataException.DealWith(ex); returnValue=-1; } _context.Dispose(); return returnValue; } //插入OrderItem public int AddOrderItem(OrderItem orderItem) { return LinqHelp.Add(orderItem); } //删除OrderItem public int DeleteOrderItem(int id) { EntityKey key = new EntityKey("BusinessContext.OrderItem", "ID", id); return LinqHelp.Delete(key); } } } |
五、领域层的服务
1. 例子说明
在第二节已基本介绍过服务的作用了,领域层服务的作用主要是为了解决业务上的逻辑问题,更多的时候,服务是一个与业务相关的动作。比如在上述例子中:
在Order表里记录的是每次交易的过程,每次商品的送货费(Freightage)为10元,当商品价格(Price)超过98元可免费送货,当用户 Person积分(Point)超过2000分可获7折优惠(Favorable),1000~2000分可获8折,1000分以下可有9折,最后总体价 格为为(TotalPrice)。
这复杂的业务逻辑,完全可以由一个领域服务类AccountManager来完成
namespace Business.Service.DomainService { public class AccountManager { private Person _person; private Order _order; public AccountManager(Person person, Order order) { _person = person; _order = order; } ///计算总体收费 public void Account() { //计算商品数量 GoodsCount(); //计算商品价格 PriceAccount(); //计算优惠等级 FavorableAccount(); double price1 = (_order.Price - _order.Favorable).Value; //计算运费 FreightageAccount(price1); //计算总体价费 _order.TotalPrice = price1 + _order.Freightage.Value; } //计算商品数量 private void GoodsCount() { _order.Count=0; foreach (var OrderItem in _order.OrderItem) _order.Count += OrderItem.Count; } //商品总体价格 private void PriceAccount() { _order.Price = 0; foreach (var OrderItem in _order.OrderItem) _order.Price += OrderItem.Price * OrderItem.Count; } //优惠分为三等,积分小于1000有9折,小于2000分为8折,大于2000为7折 private void FavorableAccount() { int point = (int)_person.Point.GetInt(); if (point < 1000) _order.Favorable = _order.Price * 0.1; if (point >= 1000 && point < 2000) _order.Favorable = _order.Price * 0.2; if (point > 2000) _order.Favorable = _order.Price * 0.3; } //如果价格在98元以上,可免运费。其余运费为10元 private void FreightageAccount(double price) { if (price >= 98) _order.Freightage = 0; else _order.Freightage = 10; } } } |
你可能会说,在这个业务流程中,除了积分优惠Person.Point以外,其他的业务都只与Order的属性有关,按照充血型模型的方案,完全可以把这些业务放到Order的方法当中,而把积分优惠独立成为一个服务。但在下在很多的开发过程中发现,为模型附上动作会带来一连串的问题,好像你不知道哪些操作应该在模型动作上实现,哪里应该在服务中实现......。对于这些无休止的争论不会因为这里的一个小例子而停止,但在这里我会坚持使用贫血型模型,利用服务来完成所有的动作。
再举一个例子:在最后结单的时候Order表里会产生订单号码OrderNumber和下订日期Delivery,Person表的积分也会加上订单总价的点数。对应这个操作,也可以单独开发一个PaymentManager服务类进行管理。
namespace Business.Service.DomainService { public class PaymentManager { //下单结算 public void Payment(Order order,Person person) { //确定下单,建立订单号 order.OrderNumber = Guid.NewGuid().ToString(); order.Delivery = DateTime.Now; //增加积分 if (person.Point.HasValue) person.Point += (int)order.TotalPrice.GetValueOrDefault(); else person.Point = (int)order.TotalPrice.GetValueOrDefault(); } } } |
利用领域层的服务,使得每个Manager服务类的职能非常明确,业务管理起来也十分地方便,领域层可以随着业务的改变而灵活变动。而且领域层具有 “高内聚,低耦合” 特性,它并不依赖其它任何一层,而只是把业务逻辑包含在里面。
出处:http://www.uml.org.cn/zjjs/201504301.asp