上一篇:《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. ABP文档 - 目录

    ABP框架 概览 介绍 多层结构 模块系统 启动配置 多租户 集成OWIN 共同结构 依赖注入 会话 缓存 日志 设置管理 时间 领域层 实体 值对象(新) 仓储 领域服务 工作单元 领域事件(Eve ...

  2. SQL Server 常用内置函数(built-in)持续整理

    本文用于收集在运维中经常使用的系统内置函数,持续整理中 一,常用Metadata函数 1,查看数据库的ID和Name db_id(‘DB Name’),db_name('DB ID') 2,查看对象的 ...

  3. iOS开发之Alamofire源码深度解析

    今天博客中的Alamofire源码的版本是以现在最新的3.4版本为例.上篇博客系统的对NSURLSession相关的东西进行了详细的解析,详情请看<详解NSURLSession>,为了就是 ...

  4. Xamarin+Prism开发详解三:Visual studio 2017 RC初体验

    Visual studio 2017 RC出来一段时间了,最近有时间就想安装试试,随带分享一下安装使用体验. 1,卸载visual studio 2015 虽然可以同时安装visual studio ...

  5. 2Sum

    用哈希表(unordered_map)使得时间复杂度从O(n*n)降到O(n),空间复杂度从O(1)增到O(n):一边找一边插入哈希表 注意 在C++11以前要使用unordered_map需要 #i ...

  6. spring的BeanFactory加载过程

    ApplicationContext spring = new ClassPathXmlApplicationContext("classpath*:spring/applicationCo ...

  7. 关于Genymotion下载比较慢的解决办法

    Genymotion号称Android模拟器中运行最快的,但是服务器在国外,Android镜像下载起来那个速度就不想说了. Add new device后下载速度太慢了,容易失败 先登录,然后add, ...

  8. excel常用技巧

    复制表格时,如果要加上行标和列标.页面布局->工作表选项:标题,勾上打印->复制下拉框->复制为图片加上打印样式 一行长拆成几行短或几行短变成一行长的文本拆分,可以通过:填充-> ...

  9. 挑子学习笔记:两步聚类算法(TwoStep Cluster Algorithm)——改进的BIRCH算法

    转载请标明出处:http://www.cnblogs.com/tiaozistudy/p/twostep_cluster_algorithm.html 两步聚类算法是在SPSS Modeler中使用的 ...

  10. 【python之路3】if 语句

    1.if语句用法(if....else....) #!/usr/bin/env python # -*- coding:utf-8 -*- my_name = raw_input("plea ...