跟踪变化最实用的方法是领域事件和事件存储。我们为领域专家所关心的所以状态改变都创建单独的事件类型,事件的名字和属性表明发生了什么样的事件。当命令操作执行完后,系统发出这些领域事件。事件的订阅方可以接收发生在模型上的所有事件。在接收到事件后,订阅方将事件保存在事件存储中。
有点越说越乱的感觉,先暂时概括下我们设计领域模型所包含的东西:
UserAuthenticationService 所做的工作就是上面图中第一个圈和第一个方框的内容,总的概述就是验证用户是否能创建 JsPermissionApply?我之前考虑用工厂实现,但感觉还是不太妥,因为工厂是为创建复杂实体服务的,内部会有一些复杂的操作,对于一些简单的实体创建,我们直接用实体的构造函数进行创建就行,比如 JsPermissionApply 的创建,既然用工厂实现不合适,那直接将操作放在 JsPermissionApply 中会怎样呢?验证自己能否被创建?想想还是有些别扭,所以还是用 UserAuthenticationService 领域服务实现吧,况且领域服务的定义就是如此。
领域服务表示一个无状态的操作,它用于实现特定于某个领域的任务,当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务。
另外,关于实体、值对象、领域事件、领域服务和仓储接口的实现,最好在不同的项目中,如果再同一个项目中的话,可能会造成循环引用的情况,比如仓储接口引用了实体,领域服务引用了仓储接口,如果实体和领域服务实现在同一个项目,就会出现循环引用的问题。
再来总结下,我们分析系统和设计领域模型的步骤:先和领域专家探讨业务系统,经过反反复复的研究,抽离出业务系统的核心业务,然后用战术建模的方式设计领域模型,最后用代码进行实现。领域模型设计好了之后,下面就开始用代码实现了,在代码实现的时候,最好解决方案中只有领域层、基础设施层和单元测试,并且一开始设计的时候,先编写领域层的代码,然后再编写单元测试,最后进行不断的测试和完善,关于数据的持久化,现在最好不要关注,尽量用 Mock 的方式模拟数据。
我先贴一下 JsPermissionApply 实体的部分代码:
using CNBlogs.Apply.Domain.DomainEvents; using CNBlogs.Apply.Domain.ValueObjects; using System; using Microsoft.Practices.Unity; using CNBlogs.Apply.Infrastructure.IoC.Contracts; namespace CNBlogs.Apply.Domain { public class JsPermissionApply : IAggregateRoot { private IEventBus eventBus; public JsPermissionApply() { } public JsPermissionApply(string reason, int userId, string ip) { if (string.IsNullOrEmpty(reason)) { ("申请内容不能为空"); } if (reason.Length > 3000) { ("申请内容超出最大长度"); } if (userId == 0) { ("用户Id为0"); } this.Reason = reason; this.UserId = userId; this.Ip = ip; this.Status = Status.Wait; } ; } ; } ; } public Status Status { get; private set; } = Status.Wait; ; } public DateTime ApplyTime { get; private set; } = DateTime.Now; ; } public DateTime? ApprovedTime { get; private set; } ; } = true; (string replyContent, Status status) { this.ReplyContent = replyContent; this.Status = status; this.ApprovedTime = DateTime.Now; eventBus = IocContainer.Default.Resolve<IEventBus>(); if (this.Status == Status.Pass) { eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId }); eventBus.Publish(new MessageSentEvent() { Title = "系统通知", Content = "审核通过", RecipientId = this.UserId }); } else if (this.Status == Status.Deny) { eventBus.Publish(new MessageSentEvent() { Title = "系统通知", Content = "审核不通过", RecipientId = this.UserId }); } } } }
JsPermissionApplyTest 单元测试的代码:
using CNBlogs.Apply.Domain.DomainServices; using CNBlogs.Apply.Domain.ValueObjects; using CNBlogs.Apply.Infrastructure.Interfaces; using CNBlogs.Apply.Infrastructure.IoC.Contracts; using CNBlogs.Apply.Repository.Interfaces; using System; using System.Data.Entity; using System.Threading.Tasks; using Xunit; using Microsoft.Practices.Unity; namespace CNBlogs.Apply.Domain.Tests { public class JsPermissionApplyTest { private IUserAuthenticationService _userAuthenticationService; private IJsPermissionApplyRepository _jsPermissionApplyRepository; private IUnitOfWork _unitOfWork; public JsPermissionApplyTest() { CNBlogs.Apply.BootStrapper.Startup.Configure(); _userAuthenticationService = IocContainer.Default.Resolve<IUserAuthenticationService>(); _jsPermissionApplyRepository = IocContainer.Default.Resolve<IJsPermissionApplyRepository>(); _unitOfWork = IocContainer.Default.Resolve<IUnitOfWork>(); } [Fact] public async Task Apply() { var userId = 1; var verfiyResult = await _userAuthenticationService.Verfiy(userId); Console.WriteLine(verfiyResult); Assert.Empty(verfiyResult); var jsPermissionApply = new JsPermissionApply("我要申请JS权限", userId, ""); _unitOfWork.RegisterNew(jsPermissionApply); Assert.True(await _unitOfWork.CommitAsync()); } [Fact] public async Task ProcessApply() { var userId = 1; var jsPermissionApply = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync(); Assert.NotNull(jsPermissionApply); jsPermissionApply.Process("审核通过", Status.Pass); _unitOfWork.RegisterDirty(jsPermissionApply); Assert.True(await _unitOfWork.CommitAsync()); } } }
代码我就不分析了,基本上是按照我们上面的设计方案实现的,本来想在仓储层模拟数据的,但时间有限,还是使用的 EF 进行数据的持久化和访问,完整的解决方案目录:
开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample