一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?
写在前面
阅读目录:
- 问题根源是什么?
- 《领域驱动设计-软件核心复杂性应对之道》分层概念
- Repository(仓储)职责所在?
- Domain Model(领域模型)重新设计
- Domain Service(领域服务)的加入
- MessageManager.Domain.Tests 的加入
- Application Layer(应用层)的协调?
- Unit Of Work(工作单元)工作范围及实现?
- 版本发布
- 后记
在上一篇《我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践》博文中,简单介绍了领域驱动设计的一些理念,并简单完成基于领域驱动设计的具体项目 MessageManager,本人在设计 MessageManager 项目之前,并没有看过 Eric Evans 的《Domain-Driven Design –Tackling Complexity in the Heart of Software》和 Martin Fowler 的《Patterns of Enterprise Application Architecture》,《企业应用架构模式》这本书正在阅读,关于领域驱动设计,主要学习来源是园中的 netfocus、dax.net、以及清培兄的部分博文(小弟先在此谢过各位大神的无私奉献),还有就是解道中的领域驱动设计专题,当然还有一些来自搜索引擎的部分资料,再加上自己的一些揣摩和理解,也就成为了属于自己的“领域驱动设计”。
MessageManager 项目是对自己所理解领域驱动设计的检验,如果你仔细看过上一篇博文,你会发现 MessageManager 其实只是领域驱动设计的“外壳”,就像我们种黄瓜,首先要搭一个架子,以便黄瓜的生长,MessageManager 项目就相当于这个架子,核心的东西“黄瓜”并不存在,当时在设计完 MessageManager 项目的时候,其实已经发现问题的存在,所以在博文最后留下了下面两个问题:
- Domain Model(领域模型):领域模型到底该怎么设计?你会看到,MessageManager 项目中的 User 和 Message 领域模型是非常贫血的,没有包含任何的业务逻辑,现在网上很多关于 DDD 示例项目多数也存在这种情况,当然项目本身没有业务,只是简单的“CURD”操作,但是如果是一些大型项目的复杂业务逻辑,该怎么去实现?或者说,领域模型完成什么样的业务逻辑?什么才是真正的业务逻辑?这个问题很重要,后续探讨。
- Application(应用层):应用层作为协调服务层,当遇到复杂性的业务逻辑时,到底如何实现,而不使其变成 BLL(业务逻辑层)?认清本质很重要,后续探讨。
另外再贴一些园友们在上一篇的问题评论:
关于以上的问题,本篇博文只是做一些解读,希望可以对那些痴迷于领域驱动设计的朋友们一些启示,写得有不当之处,也欢迎指出。
问题根源是什么?
出现上述问题的原因是什么?需求简单?设计不合理?准确的来说,应该都不是,我觉得问题的根源是没有真正去理解领域驱动设计,或者说没有真正用领域驱动设计的理念去设计或实现,领域驱动设计的概念网上一找一大堆,你看过几篇文章之后也能写出来之类的文章,为什么?因为都是泛泛之谈,诸如:领域模型是领域驱动的核心;领域驱动基本分为四层(用户层、应用层、领域层和基础层);领域包含实体、值对象和服务;还有一些聚合和聚合根之类的概念等等,文中也会给你列出一些关于这些概念的代码实现,让你瞬间感觉原来领域驱动设计是这么的高大上。
但如果拿这些概念去实践呢?却根本不是那么回事,现在使用领域驱动设计去开发的企业实在是太少了,原因有很多种,下面大致列出一些:
- 开发成本太高,换句话说,就是如果使用领域驱动设计开发,需要聘请高级程序员、高级架构师和建模专家,一般这种开发人员薪资都比较高,老板真的舍得吗?
- 开发周期长,花在需求分析的时间比较长,甚至比程序实现还要长,这个对老板来说是要命的,开发周期长,一般会意味着公司的利润降低,公司利润降低,老板的钱包就瘪了,老板会愿意吗?
- 开发思维转变问题,使用领域驱动设计开发,需要公司里的程序员懂得领域驱动设计,要对面向对象(OO)设计有一定的理解,现实情况是,大部分的程序员虽然使用的是面向对象语言(比如 Java、C#),却做着面向过程的事(类似 C 语言函数式的开发)。现在让公司的程序员使用领域驱动设计开发,就好比以前是用手直接吃饭,现在让你使用筷子吃饭,你会习惯吗?这需要一种转变,很多程序员会很不习惯,这也是领域驱动设计推行难的主要原因。
- 关于领域驱动设计实践经验实在太少,大家脑子中只有模模糊糊的概念,却没有实实在在的实践,像 dax.net 这样去完成几个完整基于领域驱动设计项目的大神实在太少了,很多都是像我一样,理解一些概念后,放出一个简单的示例 Demo,然后就没有然后了。
Eric Evans 在2004年提出 DDD(领域驱动设计)的理念,距今已经十年了,推广却停滞不前,确实值得我们程序员去反思。
扯得有点远了,回到这个副标题:问题的根源是什么?答案或许会不令你满意,就是没有真正理解领域驱动设计。那你或许会问:那真正的领域驱动设计是什么?这个我想只有 Eric Evans 可以回答,但也不要把领域驱动设计看得这么绝对,领域驱动设计只是一种指导,具体的实现要用具体的方法,正如有句古话:师傅领进门,修行在个人。每个人有每个人的具体悟道,但再变化也不要忘了师出同门。
还有一点就是,有朋友指出简单的业务逻辑是体现不出领域驱动设计的,关于这一点首先我是比较赞同的,但如果去拿一些大型业务场景去做领域驱动设计的示例,我个人觉得也不太现实,毕竟时间成本太高了。我个人认为小的业务场景和大的业务场景都可以使用领域驱动设计实现,只是业务逻辑的复杂度不同,还有就是适用度也不同,小的业务场景用脚本驱动模式去实现,可能会比领域驱动设计区实现更简单、快速,但是但凡是业务场景(不论大小),必然包含业务逻辑(CRUD 除外),那也就可以使用领域驱动设计去开发,还是那句话,只是不太适合,但做演示示例还是可以的。
业务逻辑的复杂度主要体现在领域模型中,复杂性的业务逻辑,领域模型也就越复杂,但与简单性的领域模型实质是一样的。关于如何真正理解领域驱动设计?这一点我个人觉得方式就是“迭代”,只有不断的去实践,不断的去体会,才能真正的去理解领域驱动设计,就像 MessageManager 项目,每一次有些体会我就会觉得这样做不合理,那就推倒重建,可能这样做又不合理,那就推倒再重建。。。
闲话少说,来看这一次的“迭代”:
《领域驱动设计-软件核心复杂性应对之道》分层概念
注:这一节点是我后面添加的,真是天意,在我写这篇博客的时候,正好有位不知名的朋友,发消息说他看到我之前的一篇博文,我在文中跪求《领域驱动设计-软件核心复杂性应对之道》这本书,因为网上没得买。正好他有 Word 版,虽然内容有些错别字,但是真心感谢这位不知名的朋友。大致阅读了下目录结构,确实是我想要的,接下来会认真的拜读,有实质书的话当然更好,下面是摘自这本书的分层概念。
在面向对象的程序中,用户界面(UI)、数据库和其他支持代码,经常被直接写到业务对象中去。在UI和数据库脚本的行为中嵌入额外的业务逻辑。出现这种情况是因为从短期的观点看,它是使系统运行起来的最容易的方式。当与领域相关的代码和大量的其他代码混在一起时,就很难阅读并理解了。对UI的简单改动就会改变业务逻辑。改变业务规则可能需要小心翼翼地跟踪UI代码、数据库代码或者其他的程序元素。实现一致的模型驱动对象变得不切实际,而且自动化测试也难以使用。如果在程序的每个行为中包括了所有的技术和逻辑,那么它必须很简单,否则会难以理解。
将一个复杂的程序进行层次划分。为每一层进行设计,每层都是内聚的而且只依赖于它的下层。采用标准的架构模式来完成与上层的松散关联。将所有与领域模型相关的代码都集中在一层,并且将它与用户界面层、应用层和基础结构层的代码分离。领域对象可以将重点放在表达领域模型上,不需要关心它们自己的显示、存储和管理应用任务等内容。这样使模型发展得足够丰富和清晰,足以抓住本质的业务知识并实现它。
用户界面层(表示层) | 负责向用户显示信息,并且解析用户命令。外部的执行者有时可能会是其他的计算机系统,不一定非是人。 |
应用层 | 定义软件可以完成的工作,并且指挥具有丰富含义的领域对象来解决问题。这个层所负责的任务对业务影响深远,对跟其他系统的应用层进行交互非常必要这个层要保持简练。它不包括处理业务规则或知识,只是给下一层中相互协作的领域对象协调任务、委托工作。在这个层次中不反映业务情况的状态,但反映用户或程序的任务进度的状态 |
领域层(模型层) | 负责表示业务概念、业务状况的信息以及业务规则。尽管保存这些内容的技术细节由基础结构层来完成,反映业务状况的状态在该层中被控制和使用。这一层是业务软件的核心。 |
基础结构层 | 为上层提供通用的技术能力:应用的消息发送、领域持久化,为用户界面绘制窗口等。通过架构框架,基础结构层还可以支持这四层之间的交互模式。 |
一个对象所代表的事物是一个具有连续性和标识的概念(可以跟踪该事物经历的不同的状态,甚至可以让该事物跨越不同的实现),还是只是一个用来描述事物的某种状态的属性?这就是实体与值对象最基本的区别。明确地选用这两种模式中的一种来定义对象,可以使对象的意义更清晰,并可以引导我们构造出一个健壮的设计。
另外,领域中还存在很多的方面,如果用行为或操作来描述它们会比用对象来描述更加清晰。尽管与面向对象建模理念稍有抵触,但这些最好是用服务来描述,而不是将这个操作的职责强加到某些实体或值对象身上。服务用来为客户请求提供服务。在软件的技术层中就有许多服务。服务也会在领域中出现,它们用于对软件必须完成的一些活动进行建模,但是与状态无关。有时我们必须在对象模型中釆取一些折衷的措施——这是不可避免的,例如利用关系数据库进行存储时就会出现这种情况。本章将会给出一些规则,当遇到这种复杂情况时,遵守这些规则可以使我们保持正确的方向。
最后,我们对模块(Module)的讨论可以帮助理解这样的观点:每个设计决策都应该是根据对领域的正确理解来做出。高内聚、低关联这种思想往往被看成是理想的技术标准,它们对于概念本身也是适用的。在模型驱动的设计中,模块是模型的一部分,它们应该能够反映出领域中的概念。
Repository(仓储)职责所在?
言归正题。
Repository(仓储)的概念可以参考:http://www.cnblogs.com/dudu/archive/2011/05/25/repository_pattern.html,我个人比较赞同 dudu 的理解:Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。
关于 Repository 的定义,在《企业应用架构模式》书中也有说明:协调领域和数据映射层,利用类似于集合的接口来访问领域对象。书中把 Repository 翻译为资源库,其实是和仓储是一个意思,关于 Repository 这一节点的内容,我大概阅读了两三篇才理解了部分内容(这本书比较抽象难理解,需要多读几遍,然后根据自己的理解进行推敲揣摩),文中也给出了一个示例:查找一个人所在的部门(Java),以便于加深对 Repository 的理解。
我们先看一下 Repository 的定义前半句:协调领域和数据映射层,也就是 dudu 所说的介于领域层和数据映射层之间,理解这一点很重要,非常重要。然后我们再来看 MessageManager 项目中关于 Repository 的应用(实现没有问题),在哪应用呢?根据定义我们应该要去领域层去找 Repository 的应用,但是我们在 MessageManager.Domain 项目中找不到关于 Repository 的半毛应用,却在 MessageManager.Application 项目中找到了:
1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using System; 7 using System.Collections.Generic; 8 using AutoMapper; 9 using MessageManager.Application.DTO; 10 using MessageManager.Domain; 11 using MessageManager.Domain.DomainModel; 12 using MessageManager.Domain.Repositories; 13 14 namespace MessageManager.Application.Implementation 15 { 16 /// <summary> 17 /// Message管理应用层接口实现 18 /// </summary> 19 public class MessageServiceImpl : ApplicationService, IMessageService 20 { 21 #region Private Fields 22 private readonly IMessageRepository messageRepository; 23 private readonly IUserRepository userRepository; 24 #endregion 25 26 #region Ctor 27 /// <summary> 28 /// 初始化一个<c>MessageServiceImpl</c>类型的实例。 29 /// </summary> 30 /// <param name="context">用来初始化<c>MessageServiceImpl</c>类型的仓储上下文实例。</param> 31 /// <param name="messageRepository">“消息”仓储实例。</param> 32 /// <param name="userRepository">“用户”仓储实例。</param> 33 public MessageServiceImpl(IRepositoryContext context, 34 IMessageRepository messageRepository, 35 IUserRepository userRepository) 36 :base(context) 37 { 38 this.messageRepository = messageRepository; 39 this.userRepository = userRepository; 40 } 41 #endregion 42 43 #region IMessageService Members 44 /// <summary> 45 /// 通过发送方获取消息列表 46 /// </summary> 47 /// <param name="userDTO">发送方</param> 48 /// <returns>消息列表</returns> 49 public IEnumerable<MessageDTO> GetMessagesBySendUser(UserDTO sendUserDTO) 50 { 51 //User user = userRepository.GetUserByName(sendUserDTO.Name); 52 var messages = messageRepository.GetMessagesBySendUser(Mapper.Map<UserDTO, User>(sendUserDTO)); 53 if (messages == null) 54 return null; 55 var ret = new List<MessageDTO>(); 56 foreach (var message in messages) 57 { 58 ret.Add(Mapper.Map<Message, MessageDTO>(message)); 59 } 60 return ret; 61 } 62 /// <summary> 63 /// 通过接受方获取消息列表 64 /// </summary> 65 /// <param name="userDTO">接受方</param> 66 /// <returns>消息列表</returns> 67 public IEnumerable<MessageDTO> GetMessagesByReceiveUser(UserDTO receiveUserDTO) 68 { 69 //User user = userRepository.GetUserByName(receiveUserDTO.Name); 70 var messages = messageRepository.GetMessagesByReceiveUser(Mapper.Map<UserDTO, User>(receiveUserDTO)); 71 if (messages == null) 72 return null; 73 var ret = new List<MessageDTO>(); 74 foreach (var message in messages) 75 { 76 ret.Add(Mapper.Map<Message, MessageDTO>(message)); 77 } 78 return ret; 79 } 80 /// <summary> 81 /// 删除消息 82 /// </summary> 83 /// <param name="messageDTO"></param> 84 /// <returns></returns> 85 public bool DeleteMessage(MessageDTO messageDTO) 86 { 87 messageRepository.Remove(Mapper.Map<MessageDTO, Message>(messageDTO)); 88 return messageRepository.Context.Commit(); 89 } 90 /// <summary> 91 /// 发送消息 92 /// </summary> 93 /// <param name="messageDTO"></param> 94 /// <returns></returns> 95 public bool SendMessage(MessageDTO messageDTO) 96 { 97 Message message = Mapper.Map<MessageDTO, Message>(messageDTO); 98 message.FromUserID = userRepository.GetUserByName(messageDTO.FromUserName).ID; 99 message.ToUserID = userRepository.GetUserByName(messageDTO.ToUserName).ID; 100 messageRepository.Add(message); 101 return messageRepository.Context.Commit(); 102 } 103 /// <summary> 104 /// 查看消息 105 /// </summary> 106 /// <param name="ID"></param> 107 /// <returns></returns> 108 public MessageDTO ShowMessage(string ID, string isRead) 109 { 110 Message message = messageRepository.GetByKey(ID); 111 if (isRead == "1") 112 { 113 message.IsRead = true; 114 messageRepository.Update(message); 115 messageRepository.Context.Commit(); 116 } 117 return Mapper.Map<Message, MessageDTO>(message); 118 } 119 #endregion 120 } 121 }
对,你已经发现了 Repository 的踪迹,Repository 应用在应用层,这样就致使应用层和基础层(我把数据持久化放在基础层了)通信,忽略了最重要的领域层,领域层在其中起到的作用最多也就是传递一个非常贫血的领域模型,然后通过 Repository 进行“CRUD”,这样的结果是,应用层不变成所谓的 BLL(常说的业务逻辑层)才怪,另外,因为业务逻辑都放在应用层了,领域模型也变得更加贫血。
以上分析可以回答上一篇中遗留的问题:应用层作为协调服务层,当遇到复杂性的业务逻辑时,到底如何实现,而不使其变成 BLL(业务逻辑层)?其实关于第一个问题(领域模型如何设计不贫血)也是可以进行解答的,这个后一节点有说明,关于这一系列问题的造成我觉得就是 Repository 设计,出现了严重和理论偏移,以致于没有把设计重点发在业务逻辑上,在此和大家说声抱歉。
关于“应用层中的业务逻辑”,比如下面这段代码:
1 /// <summary> 2 /// 查看消息 3 /// </summary> 4 /// <param name="ID"></param> 5 /// <returns></returns> 6 public MessageDTO ShowMessage(string ID, string isRead) 7 { 8 Message message = messageRepository.GetByKey(ID); 9 if (isRead == "1") 10 { 11 message.IsRead = true; 12 messageRepository.Update(message); 13 messageRepository.Context.Commit(); 14 } 15 return Mapper.Map<Message, MessageDTO>(message); 16 }
对,你已经看出来了,查看消息,要根据阅读人,然后判断是否已读,如果是阅读人是收件人,并且消息是未读状态,要把此消息置为已读状态,业务逻辑没什么问题,但是却放错了位置(应用层),应该放在领域层中(领域模型),其实这都是 Repository 惹的祸,因为应用层根本没有和领域层通信,关于领域模型的设计下面节点有讲解。
看了以上的内容,是不是有点:拨开浓雾,见晴天的感觉?不知道你有没有?反正我是有,关于 Repository 我们再理解的深一点,先看一下后半句的定义:利用类似于集合的接口来访问领域对象。正如 dudu 理解的这样:Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。可以这样理解为 Repository 就像一个查询集合,只提供查询给领域层,但是我们发现在实际应用中 Repository 也提供了持久化操作,这一点确实让 Repository 有点不伦不类了,关于这一点我觉得 CQRS(Command Query Responsibility Segregation)模式可以很好的解决,翻译为命令查询的职责分离,顾名思义,就是命令(持久化)和查询职责进行分离,因为我没有对 CQRS 进行过研究,也没有看到过具体的示例,所以这边就不多说,但是我觉得这是和领域驱动设计的完美结合,后面有机会可以研究下。
说了那么多,那 Repository(仓储)职责到底是什么?可以这样回答:Repository,请服务好 Domain,而且只限服务于他(防止小三),他要什么你要给什么,为什么?因为他是你大爷,跟着他有肉吃。
Domain Model(领域模型)重新设计
领域模型是领域驱动设计的核心,这一点是毋容置疑的,那领域模型中的核心是什么?或者说实现的是什么?答案是业务逻辑,那业务逻辑又是什么?或者说什么样的“业务逻辑”才能称为真正意义上的业务逻辑,关于这个问题,在上一篇中遗留如下:
领域模型到底该怎么设计?你会看到,MessageManager 项目中的 User 和 Message 领域模型是非常贫血的,没有包含任何的业务逻辑,现在网上很多关于 DDD 示例项目多数也存在这种情况,当然项目本身没有业务,只是简单的“CRUD”操作,但是如果是一些大型项目的复杂业务逻辑,该怎么去实现?或者说,领域模 型完成什么样的业务逻辑?什么才是真正的业务逻辑?这个问题很重要,后续探讨。
什么才是真正的业务逻辑?CRUD ?持久化?还是诸如“GetUserByName、GetMessageByID”之类的查询,我个人感觉这些都不是真正意义上的业务逻辑(注意,是个人感觉),因为每个项目会有“CRUD”、持久化,并不只限于某一种业务场景,像“GetUserByName、GetMessageByID”之类的查询只是查询,了解了上面 Repository 的感觉,你会发现这些查询工作应该是 Repository 做的,他是为领域模型服务的。
说了那么多,那什么才是真正意义上的业务逻辑?我个人感觉改变领域模型状态或行为的业务逻辑,才能称为真正意义上的业务逻辑(注意,是个人感觉),比如我在 Repository 节点中说过的一个示例:读取消息,要根据当前阅读人和当前消息的状态来设置当前消息的状态,如果当前阅读人为收件人和当前消息为未读状态,就要把当前消息状态设置为已读,以前这个业务逻辑的实现是在应用层中:
1 /// <summary> 2 /// 查看消息 3 /// </summary> 4 /// <param name="ID"></param> 5 /// <returns></returns> 6 public MessageDTO ShowMessage(string ID, string isRead) 7 { 8 Message message = messageRepository.GetByKey(ID); 9 if (isRead == "1") 10 { 11 message.IsRead = true; 12 messageRepository.Update(message); 13 messageRepository.Context.Commit(); 14 } 15 return Mapper.Map<Message, MessageDTO>(message); 16 }
这种实现方式就会把应用层变为所谓的 BLL(业务逻辑层)了,正确的方式实现应该在 Domain Model(领域模型)中,如下:
1 /// <summary> 2 /// 阅读消息 3 /// </summary> 4 /// <param name="CurrentUser"></param> 5 public void ReadMessage(User CurrentUser) 6 { 7 if (!this.IsRead && CurrentUser.ID.Equals(ToUserID)) 8 { 9 this.IsRead = true; 10 } 11 }
因为 MessageManager 这个项目的业务场景非常简单,很多都是简单的 CRUD 操作,可以抽离出真正的业务逻辑实在太少,除了上面阅读消息,还有就是在发送消息的时候,要根据发送用户名和接受用户名,来设置消息的发送用户和接受用户的 ID 值,这个操作以前我们也是在应用层中实现的,如下:
1 /// <summary> 2 /// 发送消息 3 /// </summary> 4 /// <param name="messageDTO"></param> 5 /// <returns></returns> 6 public bool SendMessage(MessageDTO messageDTO) 7 { 8 Message message = Mapper.Map<MessageDTO, Message>(messageDTO); 9 message.FromUserID = userRepository.GetUserByName(messageDTO.FromUserName).ID; 10 message.ToUserID = userRepository.GetUserByName(messageDTO.ToUserName).ID; 11 messageRepository.Add(message); 12 return messageRepository.Context.Commit(); 13 }
改善在 Domain Model(领域模型)中的实现,如下:
1 /// <summary> 2 /// 加载用户 3 /// </summary> 4 /// <param name="sendUser"></param> 5 /// <param name="receiveUser"></param> 6 public void LoadUserName(User sendUser,User receiveUser) 7 { 8 this.FromUserID = sendUser.ID; 9 this.ToUserID = receiveUser.ID; 10 }
因为简单的 CRUD 操作不会发生变化,而这些业务逻辑会经常发生变化,比如往消息中加载用户信息,可能现在加载的是 ID 值,以后可能会添加其他的用户值,比如:用户地理位置等等,这样我们只要去修改领域模型就可以了,应用层一点都不需要修改,如果还是之前的实现方式,你会发现我们是必须要修改应用层的,领域模型只是一个空壳。
Domain Service(领域服务)的加入
关于 Domain Service(领域服务)的概念,可以参照:http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html#content_15,netfocus 兄关于领域服务讲解的很透彻,以下摘自个人感觉精彩的部分:
- 领域中的一些概念不太适合建模为对象,即归类到实体对象或值对象,因为它们本质上就是一些操作,一些动作,而不是事物。这些操作或动作往往会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作。如果强行将这些操作职责分配给任何一个对象,则被分配的对象就是承担一些不该承担的职责,从而会导致对象的职责不明确很混乱。但是基于类的面向对象语言规定任何属性或行为都必须放在对象里面。所以我们需要寻找一种新的模式来表示这种跨多个对象的操作,DDD认为服务是一个很自然的范式用来对应这种跨多个对象的操作,所以就有了领域服务这个模式。
- 我觉得模型(实体)与服务(场景)是对领域的一种划分,模型关注领域的个体行为,场景关注领域的群体行为,模型关注领域的静态结构,场景关注领域的动态功能。这也符合了现实中出现的各种现象,有动有静,有独立有协作。
- 领域服务还有一个很重要的功能就是可以避免领域逻辑泄露到应用层。
另外还有一个用来说明应用层服务、领域服务、基础服务的职责分配的小示例:
应用层服务
- 获取输入(如一个XML请求);
- 发送消息给领域层服务,要求其实现转帐的业务逻辑;
- 领域层服务处理成功,则调用基础层服务发送Email通知;
领域层服务
- 获取源帐号和目标帐号,分别通知源帐号和目标帐号进行扣除金额和增加金额的操作;
- 提供返回结果给应用层;
基础层服务
- 按照应用层的请求,发送Email通知;
通过上述示例,可以很清晰的理解应用层服务、领域服务、基础服务的职责,关于这些概念的理解,我相信 netfocus 兄是经过很多实践得出的,因为未实践看这些概念和实践过之后再看这些概念,完全是不同的感觉。
言归正传,为什么要加入 Domain Service(领域服务)?领域服务在我们之前设计 MessageManager 项目的时候并没有,其实我脑海中一直是有这个概念,因为 Repository 的职责混乱,所以最后领域模型变得如此鸡肋,领域服务也就没有加入,那为什么现在要加入领域服务呢?因为 Repository 的职责划分,使得领域模型变成重中之重,因为应用层不和 Repository 通信,应用层又不能直接和领域模型通信,所以才会有领域服务的加入,也必须有领域服务的加入。通过上面概念的理解,你可能会对领域服务的作用有一定的理解,首先领域服务没有状态,只有行为,他和 Repository 一样,也是为领域模型服务的,只不过他像一个外交官一样,需要和应用层打交道,用来协调领域模型和应用层,而 Repository 只是一个保姆,只是服务于领域模型。
概念理解的差不多了,我们来看一下具体的实现,以下是 MessageDomainService 领域服务中的一段代码:
1 public Message ShowMessage(string ID,User CurrentUser) 2 { 3 Message message = messageRepository.GetByKey(ID); 4 message.ReadMessage(userRepository.GetUser(new User { Name = CurrentUser.Name })); 5 messageRepository.Update(message); 6 messageRepository.Context.Commit(); 7 return message; 8 }
这段代码表示查看消息,可以看到其实领域服务做的工作就是工作流程的控制,注意是工作流程处理,并不是业务流程,业务流程 ReadMessage 是领域模型去完成的,领域模型的作用只是协调。还有个疑问就是,你会看到在领域服务中使用到了 Repository,在我们之前的讲解中,Repository 不是只服务于领域模型吗?其实换个角度来看,领域服务也可以看做是领域模型的一种表现,Repository 现在主要提供的是查询集合和持久化,领域模型不可以自身操作,那这些工作只有领域服务去完成,关于这一点,就可以看出 Repository 的使用有点不太合理,不知道使用 CQRS 模式会不会是另一种情形。
另外,你会看到这一段代码:messageRepository.Context.Commit();,这个是 Unit Of Work(工作单元)的事务提交,这个工作是领域服务要做的吗?关于这一点是有一些疑问,在下面节点中有解读。
MessageManager.Domain.Tests 的加入
关于单元测试可以参考:http://www.cnblogs.com/xishuai/p/3728576.html,MessageManager.Domain.Tests 单元测试在之前的 MessageManager 项目中并没有添加,不是不想添加,而是添加了没什么意义,为什么?因为之前的领域模型那么贫血,只是一些属性和字段,那添加单元测试有什么意思?能测出来什么东西?当把工作聚焦在领域模型上的时候,对领域的单元测试将会非常的有必要。
来看 DomainTest 单元测试的部分代码:
1 using MessageManager.Domain.DomainModel; 2 using MessageManager.Domain.DomainService; 3 using MessageManager.Repositories; 4 using MessageManager.Repositories.EntityFramework; 5 using NUnit.Framework; 6 using System; 7 using System.Collections.Generic; 8 using System.Linq; 9 using System.Text; 10 11 namespace MessageManager.Domain.Tests 12 { 13 [TestFixture] 14 public class DomainTest 15 { 16 [Test] 17 public void UserDomainService() 18 { 19 IUserDomainService userDomainService = new UserDomainService( 20 new UserRepository(new EntityFrameworkRepositoryContext())); 21 List<User> users = new List<User>(); 22 users.Add(new User { Name = "小菜" }); 23 users.Add(new User { Name = "大神" }); 24 userDomainService.AddUser(users); 25 //userDomainService.ExistUser(); 26 //var user = userDomainService.GetUserByName("小菜"); 27 //if (user != null) 28 //{ 29 // Console.WriteLine(user.Name); 30 //} 31 } 32 } 33 }
其实上面我贴的单元测试的代码有些不合理,你会看到只是测试的持久化操作,这些应该是基础层完成的工作,应该由基础层的单元测试进行测试的,那领域层的单元测试测试的是什么东西?应该是领域模型中的业务逻辑,比如 ReadMessage 内的操作:
1 [Test] 2 public void MessageServiceTest() 3 { 4 IMessageDomainService messageDomainService = new MessageDomainService( 5 new MessageRepository(new EntityFrameworkRepositoryContext()), 6 new UserRepository(new EntityFrameworkRepositoryContext())); 7 Message message = messageDomainService.ShowMessage("ID", new User { Name = "小菜" }); 8 Console.WriteLine(message.IsRead); 9 }
Application Layer(应用层)的协调?
Application Layer(应用层):定义软件可以完成的工作,并且指挥具有丰富含义的领域对象来解决问题。这个层所负责的任务对业务影响深远,对跟其他系统的应用层进行交互非常必要这个层要保持简练。它不包括处理业务规则或知识,只是给下一层中相互协作的领域对象协调任务、委托工作。在这个层次中不反映业务情况的状态,但反映用户或程序的任务进度的状态。
以上是《领域驱动设计-软件核心复杂性应对之道》书中关于应用层给出的定义,应用层是很薄的一层,如果你的应用层很“厚”,那你的应用层设计就肯定出现了问题。关于 Application Layer(应用层)的应用,正如 Eric Evans 所说:不包括处理业务规则或知识,只是给下一层中相互协作的领域对象协调任务、委托工作。重点就是:不包含业务逻辑,协调任务。
如果按照自己的理解去设计应用层,很可能会像我一样把它变成业务逻辑层,所以在设计过程中一定要谨记上面两点。不包含业务逻辑很好理解,前提是要理解什么才是真正的业务逻辑(上面有说明),后面一句协调任务又是什么意思呢?在说明中后面还有一句:在这个层次中不反映业务情况的状态,但反映用户或程序的任务进度的状态。也就是工作流程的控制,比如一个生产流水线,应用层的作用就像是这个生产流水线的控制器,具体生产什么它不需要管理,它只要可以装配零件然后进行组合展示给用户,仅此而已,画了一张示意图,以便大家的理解:
另外,应用层因为要对表现层和领域层进行任务协调,这中间会涉及到数据的对象转换,也就是 DTO(数据传输对象),有关 DTO 的概念和 AutoMapper 的使用可以参考:http://www.cnblogs.com/xishuai/tag/DTO_AutoMapper,这些工作是在应用层中进行处理的,就像生产流水线,组装完产品后,需要对其进行包装才能进行展示:
1 /// 对应用层服务进行初始化。 2 /// </summary> 3 /// <remarks>包含的初始化任务有: 4 /// 1. AutoMapper框架的初始化</remarks> 5 public static void Initialize() 6 { 7 Mapper.CreateMap<UserDTO, User>(); 8 Mapper.CreateMap<MessageDTO, Message>(); 9 Mapper.CreateMap<User, UserDTO>(); 10 Mapper.CreateMap<Message, MessageDTO>() 11 .ForMember(dest => dest.Status, opt => opt.ResolveUsing<CustomResolver>()); 12 } 13 public class CustomResolver : ValueResolver<Message, string> 14 { 15 protected override string ResolveCore(Message source) 16 { 17 if (source.IsRead) 18 { 19 return "已读"; 20 } 21 else 22 { 23 return "未读"; 24 } 25 } 26 }
Unit Of Work(工作单元)工作范围及实现?
关于 Unit Of Work(工作单元)的概念可以参考:http://www.cnblogs.com/xishuai/p/3750154.html。
Unit Of Work:维护受业务事务影响的对象列表,并协调变化的写入和并发问题的解决。即管理对象的 CRUD 操作,以及相应的事务与并发问题等。Unit of Work 是用来解决领域模型存储和变更工作,而这些数据层业务并不属于领域模型本身具有的。
工作单元的概念在《企业应用架构模式》中也有说明,定义如下:维护受业务事务影响的对象列表,并协调变化的写入和并发问题的解决。概念的理解并没有什么问题,我想表达的是工作单元的工作范围及如何实现?先说下工作范围,我们看下我曾经画的一张工作单元的流程图:
从示意图中可以看出,工作单元的范围是限于 Repository 的,也就是说工作单元是无法跨 Repository 提交事务的,只能在同一个仓储内管理事务的一致性,就像我们使用的 using(MessageManagerDbContext context = new MessageManagerDbContext()) 一样,只是局限于这个 using 块,我曾在领域层的单元测试中做如下测试:
1 [Test] 2 public void DomainServiceTest() 3 { 4 IUserDomainService userDomainService = new UserDomainService( 5 new UserRepository(new EntityFrameworkRepositoryContext())); 6 IMessageDomainService messageDomainService = new MessageDomainService( 7 new MessageRepository(new EntityFrameworkRepositoryContext()), 8 new UserRepository(new EntityFrameworkRepositoryContext())); 9 List<User> users = new List<User>(); 10 users.Add(new User { Name = "小菜" }); 11 users.Add(new User { Name = "大神" }); 12 userDomainService.AddUser(users); 13 messageDomainService.DeleteMessage(null); 14 }
我在 MessageDomainService 中提交事务,因为之前 UserDomainService 已经添加了用户,但是并没有添加用户成功,工作单元中的 Committed 值为 false,其实关于工作单元范围的问题,我现在并没有明确的想法,现在是局限在仓储中,那提交的事务操作就必须放在领域服务中,也就是:messageRepository.Context.Commit();,但是又会觉得这样有些不合理,工作单元应该是贯穿整个项目的,并不一定局限在某一仓储中,而且事务的处理液应该放在应用层中,因为这是他的工作,协调工作流的处理。
如果这种思想是正确的话,实现起来确实有些难度,因为现在 ORM(对象关系映射)使用的是 EntityFramework,所以工作单元的实现是很简单的,也就是使用 SaveChanges() 方法来提交事务,我在《企业应用架构模式》中看到工作单元的实现,书中列出了一个简单的例子,还只是集合的管理,如果不使用一些 ORM 工具,实现起来就不仅仅是 SaveChanges() 一段代码的事了,太局限于技术了,确实是个问题。
这一节点的内容只是提出一些疑问,并未有解决的方式,希望后面可以探讨下。
版本发布
MessageManager 项目解决方案目录:
- GitHub 开源地址:https://github.com/yuezhongxin/MessageManager
- ASP.NET MVC 发布地址:http://www.xishuaiblog.com:8081/
- ASP.NET WebAPI 发布地址:http://www.xishuaiblog.com:8082/api/Message/GetMessagesBySendUser/小菜
注:ASP.NET WebAPI 暂只包含:获取发送放消息列表和获取接收方消息列表。
调用示例:
- GetMessagesBySendUser(获取发送方):http://www.xishuaiblog.com:8082/api/Message/GetMessagesBySendUser/用户名
- GetMessagesByReceiveUser(获取接受方):http://www.xishuaiblog.com:8082/api/Message/GetMessagesByReceiveUser/用户名
WebAPI 客户端调用可以参考 MessageManager.WebAPI.Tests 单元测试项目中的示例调用代码。
注:因为 GitHub 中对 MessageManager 项目进行了更新,如果想看上一版本,下载地址:http://pan.baidu.com/s/1gd9WmUB,可以和现有版本对比下,方便学习。
另外,《领域驱动设计.软件核心复杂性应对之道》Word 版本,下载地址:http://pan.baidu.com/s/1bnndOcR
后记
这篇博文不知不觉写两天了(周末),左手也有点不那么灵活了,如果再写下去,大家也该骂我了(看得太费劲),那就做一下总结吧:
关于领域模型的设计,我个人感觉是领域驱动设计中最难的部分,你会看到当前我在 MessageManager 项目中只有两个方法,一部分原因是业务场景太简单,另一部分原因可能是我设计的不合理,复杂性业务场景的领域模型是多个类之间协调处理,并会有一部分设计模式的加入,是相当复杂的。
需要注意的一点是,本篇以上内容并不是讲述 Domain Model(领域模型)到底如何实现?而是如何聚焦领域模型?只有聚焦在领域模型上,才能把领域模型设计的更合理,这也正是下一步需要探讨的内容。
还是那句话,真正理解和运用 DDD(领域驱动设计)的唯一方式,个人感觉还是“迭代”:不断的去实践,不断的去体会。不合理那就推倒重建,再不合理那就推倒再重建。。。
如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^
参考资料:
- http://www.cnblogs.com/dudu/archive/2011/05/25/repository_pattern.html
- http://www.cnblogs.com/1-2-3/category/109191.html
- http://www.jdon.com/ddd.html
- http://www.cnblogs.com/daxnet/archive/2009/03/31/1686984.html
- http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html
- http://www.oschina.net/question/12_21641
微信公众号:你好架构
出处:http://www.cnblogs.com/xishuai/
公众号会不定时的分享有关架构的方方面面,包含并不局限于:Microservices(微服务)、Service Mesh(服务网格)、DDD/TDD、Spring Cloud、Dubbo、Service Fabric、Linkerd、Envoy、Istio、Conduit、Kubernetes、Docker、MacOS/Linux、Java、.NET Core/ASP.NET Core、Redis、RabbitMQ、MongoDB、GitLab、CI/CD(持续集成/持续部署)、DevOps等等。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。