文章出處

上一篇:《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 確實是一種很美妙的藝術。😏


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()