DDD 领域驱动设计-如何控制业务流程?
上一篇:《DDD 领域驱动设计-如何完善 Domain Model(领域模型)?》
开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新)
需要注意的是,业务流程并不是工作流程,在领域模型中,业务流程的控制很重要,在上篇的领域模型中我们就忽略了这一点,所以在后面的实现中,出现了一些严重的问题,主要是管理员审核 JS 权限申请的业务流程。
先看一下 JsPermissionApply 实体中的 Pass 操作代码:
public async Task Pass()
{
this.Status = Status.Pass;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "恭喜您!您的JS权限申请已通过审批。";
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId });
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请已批准", Content = this.ReplyContent, RecipientId = this.UserId });
}
对应的单元测试代码:
[Fact]
public async Task ProcessApply_WithPassTest()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply);
await jsPermissionApply.Pass();
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
}
有没有发现一些问题?开通 JS 权限和消息通知,发生在 JsPermissionApply 实体对象持久化之前,这本身的设计就有问题,另外,如果 JsPermissionApply 实体对象持久化失败的话,开通 JS 权限和消息通知会正常执行,相反,开通 JS 权限和消息通知如果出现问题,JsPermissionApply 实体对象持久化也会不受影响,还有就是开通 JS 权限和消息通知放在一起也会有问题。
造成上面这些问题的原因,就是我们之前画的业务流程图太敷衍了,没有具体的进行细化设计,针对管理员审核 JS 权限申请的业务流程,我们再详细的画一下:

可以看到,管理员审核通过 JS 权限申请,JS 权限申请的状态改为“通过”,再开通 JS 权限,然后持久化 JS 权限申请,最后再消息通知用户,整个 JS 权限申请通过的业务流程顺序应该是这样的,对照上面这张图,再看之前的实现,确实牛头不对马尾。
简单总结下审核通过 JS 权限申请的业务流程顺序:
- JS 权限申请状态改为“通过”。
- 开通 JS 权限。
- 消息通知用户。
好,来看一下改进后的 JsPermissionApply 实体代码:
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))
{
throw new ArgumentException("申请内容不能为空");
}
if (reason.Length > 3000)
{
throw new ArgumentException("申请内容超出最大长度");
}
if (userId == 0)
{
throw new ArgumentException("用户Id为0");
}
this.Reason = reason;
this.UserId = userId;
this.Ip = ip;
this.Status = Status.Wait;
}
public int Id { get; private set; }
public string Reason { get; private set; }
public int UserId { get; private set; }
public Status Status { get; private set; } = Status.Wait;
public string Ip { get; private set; }
public DateTime ApplyTime { get; private set; } = DateTime.Now;
public string ReplyContent { get; private set; }
public DateTime? ApprovedTime { get; private set; }
public bool IsActive { get; private set; } = true;
public async Task<bool> Pass()
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Pass;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "恭喜您!您的JS权限申请已通过审批。";
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId });
return true;
}
public bool Deny(string replyContent)
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Deny;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = $"抱歉!您的JS权限申请没有被批准,{(string.IsNullOrEmpty(replyContent) ? "" : $"具体原因:{replyContent}<br/>")}麻烦您重新填写申请理由。";
return true;
}
public bool Lock()
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Lock;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "抱歉!您的JS权限申请没有被批准,并且申请已被锁定,具体请联系contact@cnblogs.com。";
return true;
}
public async Task Passed()
{
if (this.Status != Status.Pass)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请已批准", Content = this.ReplyContent, RecipientId = this.UserId });
}
public async Task Denied()
{
if (this.Status != Status.Deny)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请未通过审批", Content = this.ReplyContent, RecipientId = this.UserId });
}
public async Task Locked()
{
if (this.Status != Status.Lock)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请未通过审批", Content = this.ReplyContent, RecipientId = this.UserId });
}
}
}
Passed, Denied, Locked 都是过去式,表示 Pass, Deny, Lock 操作完成之后的行为,可以看到,在这些操作的内容都有 Status 状态的判断,验证的是什么状态下的 JsPermissionApply 才能执行此行为,任何不符合状态的执行都是不合法的,比如执行 Pass 的前提条件是 Status 状态为 Wait,表示只有 Status 状态为 Wait 的时候,才能执行 Pass 并修改其状态,执行 Passed 的前提前提条件是 Status 状态为 Passed,意思就像其命名 Passed 一样,无需多说。
上面最重要的是开通 JS 权限的执行,因为这是 JS 权限申请最终的执行结果,所以我们后面的操作,都必须建立在其成功的基础之上,那有人会有疑问:为什么上面的业务流程顺序不是这样的呢?当申请状态改为“通过”之后,我们才能去开通 JS 权限,这是开通 JS 权限的前提条件,这时候 JS 权限申请状态是没有被持久化的,所以,如果开通 JS 权限失败,JS 权限申请状态是不会被保存的,另外,开通 JS 权限的领域事件并没有返回值,领域事件一般没有返回值的设计,它只是去通知事件订阅者执行,并不一定需要事件订阅者返回结果给它,那我们如果判断开通 JS 权限是否执行正确呢?就是通过异常判断,如果开通 JS 权限的领域事件发生异常,后面的操作也将不会正常执行。
改进后的 JsPermissionApplyTest 单元测试代码:
namespace CNBlogs.Apply.Domain.Tests
{
public class JsPermissionApplyTest
{
private IApplyAuthenticationService _applyAuthenticationService;
private IJsPermissionApplyRepository _jsPermissionApplyRepository;
private IUnitOfWork _unitOfWork;
public JsPermissionApplyTest()
{
CNBlogs.Apply.BootStrapper.Startup.Configure();
_applyAuthenticationService = IocContainer.Default.Resolve<IApplyAuthenticationService>();
_jsPermissionApplyRepository = IocContainer.Default.Resolve<IJsPermissionApplyRepository>();
_unitOfWork = IocContainer.Default.Resolve<IUnitOfWork>();
}
[Fact]
public async Task ApplyTest()
{
var userId = 1;
var verfiyResult = await _applyAuthenticationService.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_WithPassTest()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply);
Assert.True(await jsPermissionApply.Pass());
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
await jsPermissionApply.Passed();
}
[Fact]
public async Task ProcessApply_WithDenyTest()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply);
Assert.True(jsPermissionApply.Deny("理由太简单了。"));
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
await jsPermissionApply.Denied();
}
[Fact]
public async Task ProcessApply_WithLockTest()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply);
Assert.True(jsPermissionApply.Lock());
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
await jsPermissionApply.Locked();
}
}
}
从上面代码,我们可以清晰看到业务流程的执行顺序,Assert.NotNull 和 Assert.True 就相当于应用层中的 if 判断,如果正确,则继续向下执行。
JsPermissionApply 领域模型经过三篇博文的完善,基本上符合要求了。
在解决方案中,我们可以看到只有领域层、基础设施层和领域层单元测试的项目,并没有应用层和表现层的实现,但到目前为止,我们似乎把整个系统都完成了一样,这种感觉是很美妙的,JsPermissionApply 领域模型在我手心中,任你是 Web 实现或者 WebApi 实现,又或者是其他技术框架,我都不怕,一切都是自然而然的工作,所以,关于后面的实现,你也可以交给其他人去完成,地基由我奠基,盖楼你来完成。
尽管这个系统很简单,但 DDD 确实是一种很美妙的艺术。
DDD 领域驱动设计-如何控制业务流程?的更多相关文章
- DDD 领域驱动设计-领域模型中的用户设计
上一篇:<DDD 领域驱动设计-如何控制业务流程?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新,并增加了 ...
- 浅谈我对DDD领域驱动设计的理解
从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...
- DDD 领域驱动设计-商品建模之路
最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...
- DDD 领域驱动设计-三个问题思考实体和值对象(续)
上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...
- (转载)浅谈我对DDD领域驱动设计的理解
原文地址:http://www.cnblogs.com/netfocus/p/5548025.html 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来 ...
- DDD领域驱动设计和实践(转载)
-->目录导航 一. DDD领域驱动设计介绍 1. 什么是领域驱动设计(DDD) 2. 领域驱动设计的特点 3. 如果不使用DDD? 4. 领域驱动设计的分层架构和构成要素 5. 事务脚本和领域 ...
- DDD领域驱动设计的理解
DDD领域驱动设计的理解 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能 ...
- 关于DDD领域驱动设计的理论知识收集汇总
原文:关于DDD领域驱动设计的理论知识收集汇总 最近一直在学习领域驱动设计(DDD)的理论知识,从网上搜集了一些个人认为比较有价值的东西,贴出来和大家分享一下: 我一直觉得不要盲目相信权威,比如不能一 ...
- DDD领域驱动设计-项目包结构说明-Ⅳ
基于DDD领域驱动设计的思想,在开发具体系统时,需要先建立不同的层级包.主要是梳理不同层面(应用层,领域层,基础设施层,展示层)包括的功能目录,每一个层面应该包括哪些模块.本例所讲述的分层是DDD落 ...
随机推荐
- Shell特殊变量
$ 表示当前Shell进程的ID,即pid $echo $$ 运行结果 特殊变量列表 变量 含义 $0 当前脚本的文件名 $n 传递给脚本或函数的参数.n 是一个数字,表示第几个参数.例如,第一个参数 ...
- Apache Ignite高性能分布式网格框架-初探
Apache Ignite初步认识 今年4月开始倒腾openfire,过程中经历了许多,更学到了许多.特别是在集群方面有了很多的认识,真正开始认识到集群的概念及应用方法. 在openfire中使用的集 ...
- 从0开始搭建SQL Server AlwaysOn 第二篇(配置故障转移集群)
从0开始搭建SQL Server AlwaysOn 第二篇(配置故障转移集群) 第一篇http://www.cnblogs.com/lyhabc/p/4678330.html第二篇http://www ...
- 【原创】免费申请SSL证书【用于HTTPS,即是把网站从HTTP改为HTTPS,加密传输数据,保护敏感数据】
今天公司有个网站需要改用https访问,所以就用到SSL证书.由于沃通(以前我是在这里申请的)暂停了免费的SSL证书之后,其网站推荐了新的一个网站来申请证书,所以,今天因为刚好又要申请一个证书,所以, ...
- JavaScript 常量定义
相信同学们在看见这个标题的时候就一脸懵逼了,什么?JS能常量定义?别逗我好吗?确切的说,JS当中确实没有常量(ES6中好像有了常量定义的关键字),但是深入一下我们可以发现JS很多不为人知的性质,好好利 ...
- 用FSM一键制作逐帧动画雪碧图 Vue2 + webpack
因为工作需要要将五六十张逐帧图拼成雪碧图,网上想找到一件制作工具半天没有找到,就自己用canvas写了一个. 写成之后就再没有什么机会使用了,因此希望有人使用的时候如果遇到bug了能及时反馈给我. 最 ...
- mysql开启慢查询日志及查询--windows
MySQL慢查询配置 1. 慢查询有什么用? 它能记录下所有执行超过long_query_time时间的SQL语句, 帮你找到执行慢的SQL, 方便我们对这些SQL进行优化. 2. 如何开启慢查询? ...
- 在VMware上安装CentOS -7
1.下载好VMware 2.准备好CentOS的镜像文件 3.打开VMware创建新的虚拟机 选择自定义高级后按下一步 继续下一步 选择稍后安装操作系统 客户机操作系统选择Linux,版本选择Cent ...
- 烂泥:jira7.2安装、中文及破解
. jira的主要配置文件,存放在/opt/atlassian/jira/conf/server.xml文件中,如下: vim /opt/atlassian/jira/conf/server.xml ...
- windows系统路径环境变量
当前系统盘符%systemdrive%或%HOMEDRIVE%C:\ 当前系统目录%systemroot%或%Windir%C:\WINDOWS 当前用户文件夹%UserProfile%或%HOMEP ...