上一篇:《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 权限申请的业务流程顺序:

  1. JS 权限申请状态改为“通过”。
  2. 开通 JS 权限。
  3. 消息通知用户。

好,来看一下改进后的 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.NotNullAssert.True 就相当于应用层中的 if 判断,如果正确,则继续向下执行。


JsPermissionApply 领域模型经过三篇博文的完善,基本上符合要求了。

在解决方案中,我们可以看到只有领域层、基础设施层和领域层单元测试的项目,并没有应用层和表现层的实现,但到目前为止,我们似乎把整个系统都完成了一样,这种感觉是很美妙的,JsPermissionApply 领域模型在我手心中,任你是 Web 实现或者 WebApi 实现,又或者是其他技术框架,我都不怕,一切都是自然而然的工作,所以,关于后面的实现,你也可以交给其他人去完成,地基由我奠基,盖楼你来完成。

尽管这个系统很简单,但 DDD 确实是一种很美妙的艺术。

DDD 领域驱动设计-如何控制业务流程?的更多相关文章

  1. DDD 领域驱动设计-领域模型中的用户设计

    上一篇:<DDD 领域驱动设计-如何控制业务流程?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新,并增加了 ...

  2. 浅谈我对DDD领域驱动设计的理解

    从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...

  3. DDD 领域驱动设计-商品建模之路

    最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...

  4. DDD 领域驱动设计-三个问题思考实体和值对象(续)

    上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...

  5. (转载)浅谈我对DDD领域驱动设计的理解

    原文地址:http://www.cnblogs.com/netfocus/p/5548025.html 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来 ...

  6. DDD领域驱动设计和实践(转载)

    -->目录导航 一. DDD领域驱动设计介绍 1. 什么是领域驱动设计(DDD) 2. 领域驱动设计的特点 3. 如果不使用DDD? 4. 领域驱动设计的分层架构和构成要素 5. 事务脚本和领域 ...

  7. DDD领域驱动设计的理解

    DDD领域驱动设计的理解 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能 ...

  8. 关于DDD领域驱动设计的理论知识收集汇总

    原文:关于DDD领域驱动设计的理论知识收集汇总 最近一直在学习领域驱动设计(DDD)的理论知识,从网上搜集了一些个人认为比较有价值的东西,贴出来和大家分享一下: 我一直觉得不要盲目相信权威,比如不能一 ...

  9. DDD领域驱动设计-项目包结构说明-Ⅳ

     基于DDD领域驱动设计的思想,在开发具体系统时,需要先建立不同的层级包.主要是梳理不同层面(应用层,领域层,基础设施层,展示层)包括的功能目录,每一个层面应该包括哪些模块.本例所讲述的分层是DDD落 ...

随机推荐

  1. 干货分享:让你分分钟学会 JS 闭包

    闭包,是 Javascript 比较重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,很难从定义去理解它.因此,本文不会对闭包的概念进行大篇幅描述 ...

  2. 关于微软HttpClient使用,避免踩坑

    最近公司对于WebApi的场景使用也越来越加大了,随之而来就是Api的客户端工具我们使用哪个?我们最常用的估计就是HttpClient,在微软类库中命名空间地址:System.Net.Http,是一个 ...

  3. nodejs进阶(2)—函数模块调用

    函数调用 1. 文件内普通函数调用 创建一个js文件命名为2_callFunction.js,其中定义一个函数fun1,向返回对象输出了一段字符串“你好,我是fun1”. //------------ ...

  4. 深入理解 Java G1 垃圾收集器--转

    原文地址:http://blog.jobbole.com/109170/?utm_source=hao.jobbole.com&utm_medium=relatedArticle 本文首先简单 ...

  5. 开源一款简单清爽的日历组件,JavaScript版的

    源码会在最后给出地址,需要的朋友自己去下载.最近项目需要做一个日程安排的功能,就是点击日历的某一天弹出一个录入页面,填完信息后保存当天的日程安排.有日程的日期会有不同的标记(比如加一个背景色啥的).网 ...

  6. Java定时任务的常用实现

    Java的定时任务有以下几种常用的实现方式: 1)Timer 2)ScheduledThreadPoolExecutor 3)Spring中集成Cron Quartz 接下来依次介绍这几类具体实现的方 ...

  7. 深入理解 JavaScript,以及 Linux 下的开发调试工具

    前言 JavaScript 是我接触到的第二门编程语言,第一门是 C 语言.然后才是 C++.Java 还有其它一些什么.所以我对 JavaScript 是非常有感情的,毕竟使用它有十多年了.早就想写 ...

  8. Java中常用集合操作

    一.Map 名值对存储的. 常用派生类HashMap类 添加: put(key,value)往集合里添加数据 删除: clear()删除所有 remove(key)清除单个,根据k来找 获取: siz ...

  9. windows下mongodb配置

    打开cmd(windows键+r输入cmd)命令行,进入D:\mongodb\bin目录(如图先输入d:进入d盘然后输入cd d:\mongodb\bin), 输入如下的命令启动mongodb服务: ...

  10. MyEclipse对Maven的安装

    好记性不如烂笔头,记录一下. 操作系统:windows 7 MyEclipse2015 JDK1.7 maven的下载链接,点这里下载apache-maven-3.0.4-bin.tar.gz. 下载 ...