本文共 16895 字,大约阅读时间需要 56 分钟。
言归正题。
Repository(仓储)的概念可以参考:,我个人比较赞同 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 ///17 /// Message管理应用层接口实现 18 /// 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 ///28 /// 初始化一个 30 /// 用来初始化MessageServiceImpl 类型的实例。 29 ///MessageServiceImpl 类型的仓储上下文实例。 31 /// “消息”仓储实例。 32 /// “用户”仓储实例。 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 ///45 /// 通过发送方获取消息列表 46 /// 47 /// 发送方 48 ///消息列表 49 public IEnumerableGetMessagesBySendUser(UserDTO sendUserDTO) 50 { 51 //User user = userRepository.GetUserByName(sendUserDTO.Name); 52 var messages = messageRepository.GetMessagesBySendUser(Mapper.Map (sendUserDTO)); 53 if (messages == null) 54 return null; 55 var ret = new List (); 56 foreach (var message in messages) 57 { 58 ret.Add(Mapper.Map (message)); 59 } 60 return ret; 61 } 62 /// 63 /// 通过接受方获取消息列表 64 /// 65 /// 接受方 66 ///消息列表 67 public IEnumerableGetMessagesByReceiveUser(UserDTO receiveUserDTO) 68 { 69 //User user = userRepository.GetUserByName(receiveUserDTO.Name); 70 var messages = messageRepository.GetMessagesByReceiveUser(Mapper.Map (receiveUserDTO)); 71 if (messages == null) 72 return null; 73 var ret = new List (); 74 foreach (var message in messages) 75 { 76 ret.Add(Mapper.Map (message)); 77 } 78 return ret; 79 } 80 /// 81 /// 删除消息 82 /// 83 /// 84 ///85 public bool DeleteMessage(MessageDTO messageDTO) 86 { 87 messageRepository.Remove(Mapper.Map (messageDTO)); 88 return messageRepository.Context.Commit(); 89 } 90 /// 91 /// 发送消息 92 /// 93 /// 94 ///95 public bool SendMessage(MessageDTO messageDTO) 96 { 97 Message message = Mapper.Map (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 /// 104 /// 查看消息105 /// 106 /// 107 ///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);118 }119 #endregion120 }121 }
对,你已经发现了 Repository 的踪迹,Repository 应用在应用层,这样就致使应用层和基础层(我把数据持久化放在基础层了)通信,忽略了最重要的领域层,领域层在其中起到的作用最多也就是传递一个非常贫血的领域模型,然后通过 Repository 进行“CRUD”,这样的结果是,应用层不变成所谓的 BLL(常说的业务逻辑层)才怪,另外,因为业务逻辑都放在应用层了,领域模型也变得更加贫血。
以上分析可以回答上一篇中遗留的问题:应用层作为协调服务层,当遇到复杂性的业务逻辑时,到底如何实现,而不使其变成 BLL(业务逻辑层)?其实关于第一个问题(领域模型如何设计不贫血)也是可以进行解答的,这个后一节点有说明,关于这一系列问题的造成我觉得就是 Repository 设计,出现了严重和理论偏移,以致于没有把设计重点发在业务逻辑上,在此和大家说声抱歉。
关于“应用层中的业务逻辑”,比如下面这段代码:
1 ///2 /// 查看消息 3 /// 4 /// 5 ///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);16 }
对,你已经看出来了,查看消息,要根据阅读人,然后判断是否已读,如果是阅读人是收件人,并且消息是未读状态,要把此消息置为已读状态,业务逻辑没什么问题,但是却放错了位置(应用层),应该放在领域层中(领域模型),其实这都是 Repository 惹的祸,因为应用层根本没有和领域层通信,关于领域模型的设计下面节点有讲解。
看了以上的内容,是不是有点:拨开浓雾,见晴天的感觉?不知道你有没有?反正我是有,关于 Repository 我们再理解的深一点,先看一下后半句的定义:利用类似于集合的接口来访问领域对象。正如 dudu 理解的这样:Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。可以这样理解为 Repository 就像一个查询集合,只提供查询给领域层,但是我们发现在实际应用中 Repository 也提供了持久化操作,这一点确实让 Repository 有点不伦不类了,关于这一点我觉得 CQRS(Command Query Responsibility Segregation)模式可以很好的解决,翻译为命令查询的职责分离,顾名思义,就是命令(持久化)和查询职责进行分离,因为我没有对 CQRS 进行过研究,也没有看到过具体的示例,所以这边就不多说,但是我觉得这是和领域驱动设计的完美结合,后面有机会可以研究下。
说了那么多,那 Repository(仓储)职责到底是什么?可以这样回答:Repository,请服务好 Domain,而且只限服务于他(防止小三),他要什么你要给什么,为什么?因为他是你大爷,跟着他有肉吃。
领域模型是领域驱动设计的核心,这一点是毋容置疑的,那领域模型中的核心是什么?或者说实现的是什么?答案是业务逻辑,那业务逻辑又是什么?或者说什么样的“业务逻辑”才能称为真正意义上的业务逻辑,关于这个问题,在上一篇中遗留如下:
领域模型到底该怎么设计?你会看到,MessageManager 项目中的 User 和 Message 领域模型是非常贫血的,没有包含任何的业务逻辑,现在网上很多关于 DDD 示例项目多数也存在这种情况,当然项目本身没有业务,只是简单的“CRUD”操作,但是如果是一些大型项目的复杂业务逻辑,该怎么去实现?或者说,领域模 型完成什么样的业务逻辑?什么才是真正的业务逻辑?这个问题很重要,后续探讨。
什么才是真正的业务逻辑?CRUD ?持久化?还是诸如“GetUserByName、GetMessageByID”之类的查询,我个人感觉这些都不是真正意义上的业务逻辑(注意,是个人感觉),因为每个项目会有“CRUD”、持久化,并不只限于某一种业务场景,像“GetUserByName、GetMessageByID”之类的查询只是查询,了解了上面 Repository 的感觉,你会发现这些查询工作应该是 Repository 做的,他是为领域模型服务的。
说了那么多,那什么才是真正意义上的业务逻辑?我个人感觉改变领域模型状态或行为的业务逻辑,才能称为真正意义上的业务逻辑(注意,是个人感觉),比如我在 Repository 节点中说过的一个示例:读取消息,要根据当前阅读人和当前消息的状态来设置当前消息的状态,如果当前阅读人为收件人和当前消息为未读状态,就要把当前消息状态设置为已读,以前这个业务逻辑的实现是在应用层中:
1 ///2 /// 查看消息 3 /// 4 /// 5 ///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);16 }
这种实现方式就会把应用层变为所谓的 BLL(业务逻辑层)了,正确的方式实现应该在 Domain Model(领域模型)中,如下:
1 ///2 /// 阅读消息 3 /// 4 /// 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 ///2 /// 发送消息 3 /// 4 /// 5 ///6 public bool SendMessage(MessageDTO messageDTO) 7 { 8 Message message = Mapper.Map (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 ///2 /// 加载用户 3 /// 4 /// 5 /// 6 public void LoadUserName(User sendUser,User receiveUser) 7 { 8 this.FromUserID = sendUser.ID; 9 this.ToUserID = receiveUser.ID;10 }
因为简单的 CRUD 操作不会发生变化,而这些业务逻辑会经常发生变化,比如往消息中加载用户信息,可能现在加载的是 ID 值,以后可能会添加其他的用户值,比如:用户地理位置等等,这样我们只要去修改领域模型就可以了,应用层一点都不需要修改,如果还是之前的实现方式,你会发现我们是必须要修改应用层的,领域模型只是一个空壳。
关于 Domain Service(领域服务)的概念,可以参照:,netfocus 兄关于领域服务讲解的很透彻,以下摘自个人感觉精彩的部分:
另外还有一个用来说明应用层服务、领域服务、基础服务的职责分配的小示例:
应用层服务
领域层服务
基础层服务
通过上述示例,可以很清晰的理解应用层服务、领域服务、基础服务的职责,关于这些概念的理解,我相信 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 单元测试在之前的 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.Tests12 {13 [TestFixture]14 public class DomainTest15 {16 [Test]17 public void UserDomainService()18 {19 IUserDomainService userDomainService = new UserDomainService(20 new UserRepository(new EntityFrameworkRepositoryContext()));21 Listusers = new List ();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(应用层)的应用,正如 Eric Evans 所说:不包括处理业务规则或知识,只是给下一层中相互协作的领域对象协调任务、委托工作。重点就是:不包含业务逻辑,协调任务。
如果按照自己的理解去设计应用层,很可能会像我一样把它变成业务逻辑层,所以在设计过程中一定要谨记上面两点。不包含业务逻辑很好理解,前提是要理解什么才是真正的业务逻辑(上面有说明),后面一句协调任务又是什么意思呢?在说明中后面还有一句:在这个层次中不反映业务情况的状态,但反映用户或程序的任务进度的状态。也就是工作流程的控制,比如一个生产流水线,应用层的作用就像是这个生产流水线的控制器,具体生产什么它不需要管理,它只要可以装配零件然后进行组合展示给用户,仅此而已,画了一张示意图,以便大家的理解:
另外,应用层因为要对表现层和领域层进行任务协调,这中间会涉及到数据的对象转换,也就是 DTO(数据传输对象),有关 DTO 的概念和 AutoMapper 的使用可以参考:,这些工作是在应用层中进行处理的,就像生产流水线,组装完产品后,需要对其进行包装才能进行展示:
1 /// 对应用层服务进行初始化。 2 /// 3 ///包含的初始化任务有: 4 /// 1. AutoMapper框架的初始化 5 public static void Initialize() 6 { 7 Mapper.CreateMap(); 8 Mapper.CreateMap (); 9 Mapper.CreateMap ();10 Mapper.CreateMap ()11 .ForMember(dest => dest.Status, opt => opt.ResolveUsing ());12 }13 public class CustomResolver : ValueResolver 14 {15 protected override string ResolveCore(Message source)16 {17 if (source.IsRead)18 {19 return "已读";20 }21 else22 {23 return "未读";24 }25 }26 }
关于 Unit Of Work(工作单元)的概念可以参考:。
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 Listusers = new List ();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 项目解决方案目录:
注:ASP.NET WebAPI 暂只包含:获取发送放消息列表和获取接收方消息列表。
调用示例:
WebAPI 客户端调用可以参考 MessageManager.WebAPI.Tests 单元测试项目中的示例调用代码。
注:因为 GitHub 中对 MessageManager 项目进行了更新,如果想看上一版本,下载地址:,可以和现有版本对比下,方便学习。
另外,《领域驱动设计.软件核心复杂性应对之道》Word 版本,下载地址:
这篇博文不知不觉写两天了(周末),左手也有点不那么灵活了,如果再写下去,大家也该骂我了(看得太费劲),那就做一下总结吧:
关于领域模型的设计,我个人感觉是领域驱动设计中最难的部分,你会看到当前我在 MessageManager 项目中只有两个方法,一部分原因是业务场景太简单,另一部分原因可能是我设计的不合理,复杂性业务场景的领域模型是多个类之间协调处理,并会有一部分设计模式的加入,是相当复杂的。
需要注意的一点是,本篇以上内容并不是讲述 Domain Model(领域模型)到底如何实现?而是如何聚焦领域模型?只有聚焦在领域模型上,才能把领域模型设计的更合理,这也正是下一步需要探讨的内容。
还是那句话,真正理解和运用 DDD(领域驱动设计)的唯一方式,个人感觉还是“迭代”:不断的去实践,不断的去体会。不合理那就推倒重建,再不合理那就推倒再重建。。。
如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^
本文转自田园里的蟋蟀博客园博客,原文链接:http://www.cnblogs.com/xishuai/p/3800656.html,如需转载请自行联系原作者