Fork me on GitHub

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

  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 确实是一种很美妙的艺术。😏

posted @ 2016-04-22 11:12  田园里的蟋蟀  阅读(5624)  评论(2编辑  收藏  举报