DDD 領域驅動設計-兩個實體的碰撞火花

来源:http://www.cnblogs.com/xishuai/archive/2016/06/30/ddd-design-two-entities.html
-Advertisement-
Play Games

上一篇:《 "DDD 領域驅動設計-領域模型中的用戶設計?" 》 開源地址: "https://github.com/yuezhongxin/CNBlogs.Apply.Sample" (代碼已更新) 在之前的項目開發中,只有一個 JsPermissionApply 實體(JS 許可權申請),所以,C ...


上一篇:《DDD 領域驅動設計-領域模型中的用戶設計?

開源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代碼已更新)

在之前的項目開發中,只有一個 JsPermissionApply 實體(JS 許可權申請),所以,CNBlogs.Apply.Domain 設計的有些不全面,或者稱之為不完善,因為在一些簡單的項目開發中,一般只會存在一個實體,單個實體的設計,我們可能會忽略很多的東西,從而以後會導致一些問題的產生,那如果再增加一個實體,CNBlogs.Apply.Domain 該如何設計呢?

按照實際項目開發需要,CNBlogs.Apply.Domain 需要增加一個 BlogChangeApply 實體(博客地址更改申請)。

在 BlogChangeApply 實體設計之前,我們按照之前 JsPermissionApply 實體設計過程,先大致畫一下流程圖:

流程圖很簡單,並且和之前的 JS 許可權申請和審核很相似,我們再看一下之前的 JsPermissionApply 實體設計代碼:

namespace CNBlogs.Apply.Domain
{
    public class JsPermissionApply : IAggregateRoot
    {
        private IEventBus eventBus;

        public JsPermissionApply()
        { }

        public JsPermissionApply(string reason, User user, string ip)
        {
            if (string.IsNullOrEmpty(reason))
            {
                throw new ArgumentException("申請內容不能為空");
            }
            if (reason.Length > 3000)
            {
                throw new ArgumentException("申請內容超出最大長度");
            }
            if (user == null)
            {
                throw new ArgumentException("用戶為null");
            }
            if (user.Id == 0)
            {
                throw new ArgumentException("用戶Id為0");
            }
            this.Reason = HttpUtility.HtmlEncode(reason);
            this.User = user;
            this.Ip = ip;
            this.Status = Status.Wait;
        }

        public int Id { get; private set; }

        public string Reason { get; private set; }

        public virtual User User { 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<Status> GetStatus(string userAlias)
        {
            if (await BlogService.HaveJsPermission(userAlias))
            {
                return Status.Pass;
            }
            else
            {
                if (this.Status == Status.Deny && DateTime.Now > this.ApplyTime.AddDays(3))
                {
                    return Status.None;
                }
                if (this.Status == Status.Pass)
                {
                    return Status.None;
                }
                return this.Status;
            }
        }

        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() { UserAlias = this.User.Alias });
            return true;
        }

        public bool Deny(string replyContent)
        {
            if (this.Status != Status.Wait)
            {
                return false;
            }
            this.Status = Status.Deny;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = replyContent;
            return true;
        }

        public bool Lock()
        {
            if (this.Status != Status.Wait)
            {
                return false;
            }
            this.Status = Status.Lock;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = "抱歉!您的JS許可權申請沒有被批准,並且申請已被鎖定,具體請聯繫[email protected]。";
            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.User.Id });
        }

        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.User.Id });
        }

        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.User.Id });
        }
    }
}

根據博客地址更改申請和審核的流程圖,然後再結合上面 JsPermissionApply 實體代碼,我們就可以幻想出 BlogChangeApply 的實體代碼,具體是怎樣的了,如果你實現一下,會發現和上面的代碼簡直一摸一樣,區別就在於多了一個 TargetBlogApp(目標博客地址),然後後面的 Repository 和 Application.Services 複製粘貼就行了,沒有任何的難度,這樣設計實現也沒什麼問題,但是項目中的重覆代碼簡直太多了,領域驅動設計慢慢就變成了一個腳手架,沒有任何的一點用處。

該如何解決上面的問題呢?我們需要思考下 CNBlogs.Apply.Domain 所包含的含義,CNBlogs.Apply.Domain 顧名思議是申請領域,並不是 CNBlogs.JsPermissionApply.Domain,也不是 CNBlogs.BlogChangeApply.Domain,實體的產生是根據聚合根的設計,那 CNBlogs.Apply.Domain 的聚合根是什麼呢?在之前的設計中只有 IAggregateRoot 和 IEntity,具體代碼:

namespace CNBlogs.Apply.Domain
{
    public interface IAggregateRoot : IEntity { }
}

namespace CNBlogs.Apply.Domain
{
    public interface IEntity
    {
        int Id { get; }
    }
}

現在再來看上面這種設計,完全是錯誤的,聚合根介面怎麼能繼承實體介面呢,還有一個問題,就是如果有多個實體設計,是繼承 IAggregateRoot?還是 IEntity?IEntity 在這樣的設計中,沒有任何的作用,並且閑的很多餘,IAggregateRoot 到最後也只是一個抽象的介面,CNBlogs.Apply.Domain 中並沒有具體的實現。

解決上面混亂的問題,就是抽離出 ApplyAggregateRoot(申請聚合根),然後 JsPermissionApply 和 BlogChangeApply 實體都是由它進行產生,在這之前,我們先定義一下 IAggregateRoot:

namespace CNBlogs.Apply.Domain
{
    public interface IAggregateRoot
    {
        int Id { get; }
    }
}

然後根據 JS 許可權申請/審核和博客地址更改申請/審核的流程圖,抽離出 ApplyAggregateRoot,並且繼承自 IAggregateRoot,具體實現代碼:

namespace CNBlogs.Apply.Domain
{
    public class ApplyAggregateRoot : IAggregateRoot
    {
        private IEventBus eventBus;

        public ApplyAggregateRoot()
        { }

        public ApplyAggregateRoot(string reason, User user, string ip)
        {
            if (string.IsNullOrEmpty(reason))
            {
                throw new ArgumentException("申請內容不能為空");
            }
            if (reason.Length > 3000)
            {
                throw new ArgumentException("申請內容超出最大長度");
            }
            if (user == null)
            {
                throw new ArgumentException("用戶為null");
            }
            if (user.Id == 0)
            {
                throw new ArgumentException("用戶Id為0");
            }
            this.Reason = HttpUtility.HtmlEncode(reason);
            this.User = user;
            this.Ip = ip;
            this.Status = Status.Wait;
        }

        public int Id { get; protected set; }

        public string Reason { get; protected set; }

        public virtual User User { get; protected set; }

        public Status Status { get; protected set; } = Status.Wait;

        public string Ip { get; protected set; }

        public DateTime ApplyTime { get; protected set; } = DateTime.Now;

        public string ReplyContent { get; protected set; }

        public DateTime? ApprovedTime { get; protected set; }

        public bool IsActive { get; protected set; } = true;

        protected async Task<bool> Pass<TEvent>(string replyContent, TEvent @event)
            where TEvent : IEvent
        {
            if (this.Status != Status.Wait)
            {
                return false;
            }
            this.Status = Status.Pass;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = replyContent;
            eventBus = IocContainer.Default.Resolve<IEventBus>();
            await eventBus.Publish(@event);
            return true;
        }

        public bool Deny(string replyContent)
        {
            if (this.Status != Status.Wait)
            {
                return false;
            }
            this.Status = Status.Deny;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = replyContent;
            return true;
        }

        protected bool Lock(string replyContent)
        {
            if (this.Status != Status.Wait)
            {
                return false;
            }
            this.Status = Status.Lock;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = replyContent;
            return true;
        }

        protected async Task Passed(string title)
        {
            if (this.Status != Status.Pass)
            {
                return;
            }
            await SendMessage(title);
        }

        protected async Task Denied(string title)
        {
            if (this.Status != Status.Deny)
            {
                return;
            }
            await SendMessage(title);
        }

        protected async Task Locked(string title)
        {
            if (this.Status != Status.Lock)
            {
                return;
            }
            await SendMessage(title);
        }

        private async Task SendMessage(string title)
        {
            eventBus = IocContainer.Default.Resolve<IEventBus>();
            await eventBus.Publish(new MessageSentEvent() { Title = title, Content = this.ReplyContent, RecipientId = this.User.Id });
        }
    }
}

ApplyAggregateRoot 的實現,基本上是抽離出 JsPermissionApply 和 BlogChangeApply 實體產生的重覆代碼,比如不管什麼類型的申請,都包含申請理由、申請人信息、通過或拒絕等操作,這些也就是 ApplyAggregateRoot 所體現的領域含義,我們再來看下 BlogChangeApply 實體的實現代碼:

namespace CNBlogs.Apply.Domain
{
    public class BlogChangeApply : ApplyAggregateRoot
    {
        public BlogChangeApply()
        { }

        public BlogChangeApply(string targetBlogApp, string reason, User user, string ip)
            : base(reason, user, ip)
        {
            if (string.IsNullOrEmpty(targetBlogApp))
            {
                throw new ArgumentException("博客地址不能為空");
            }
            targetBlogApp = targetBlogApp.Trim();
            if (targetBlogApp.Length < 4)
            {
                throw new ArgumentException("博客地址至少4個字元!");
            }
            if (!Regex.IsMatch(targetBlogApp, @"^([0-9a-zA-Z_-])+$"))
            {
                throw new ArgumentException("博客地址只能使用英文、數字、-連字元、_下劃線!");
            }
            this.TargetBlogApp = targetBlogApp;
        }

        public string TargetBlogApp { get; private set; }

        public Status GetStatus()
        {
            if (this.Status == Status.Deny && DateTime.Now > this.ApplyTime.AddDays(3))
            {
                return Status.None;
            }
            return this.Status;
        }

        public async Task<bool> Pass()
        {
            var replyContent = $"恭喜您!您的博客地址更改申請已通過,新的博客地址:<a href='{this.TargetBlogApp}' target='_blank'>{this.TargetBlogApp}</a>";
            return await base.Pass(replyContent, new BlogChangedEvent() { UserAlias = this.User.Alias, TargetUserAlias = this.TargetBlogApp });
        }

        public bool Lock()
        {
            var replyContent = "抱歉!您的博客地址更改申請沒有被批准,並且申請已被鎖定,具體請聯繫[email protected]。";
            return base.Lock(replyContent);
        }

        public async Task Passed()
        {
            await base.Passed("您的博客地址更改申請已批准");
        }

        public async Task Denied()
        {
            await base.Passed("您的博客地址更改申請未通過審批");
        }

        public async Task Locked()
        {
            await Denied();
        }
    }
}

BlogChangeApply 繼承自 ApplyAggregateRoot,並且單獨的 TargetBlogApp 操作,其他一些實現都是基本的參數傳遞操作,沒有具體實現,JsPermissionApply 的實體代碼就不貼了,和 BlogChangeApply 比較類似,只不過有一些不同的業務實現。

CNBlogs.Apply.Domain 改造之後,還要對應改造下 Repository,之前的代碼大家可以看下 Github,這邊我簡單說下改造的過程,首先 IRepository 的設計不變:

namespace CNBlogs.Apply.Repository.Interfaces
{
    public interface IRepository<TAggregateRoot> 
        where TAggregateRoot : class, IAggregateRoot
    {
        IQueryable<TAggregateRoot> Get(int id);

        IQueryable<TAggregateRoot> GetAll();
    }
}

IRepository 對應 BaseRepository 實現,它的作用就是抽離出所有聚合根的 Repository 操作,並不單獨包含 ApplyAggregateRoot,所以,我們還需要一個對 ApplyAggregateRoot 操作的 Repository 實現,定義如下:

namespace CNBlogs.Apply.Repository.Interfaces
{
    public interface IApplyRepository<TApplyAggregateRoot> : IRepository<TApplyAggregateRoot>
        where TApplyAggregateRoot : ApplyAggregateRoot
    {
        IQueryable<TApplyAggregateRoot> GetByUserId(int userId);

        IQueryable<TApplyAggregateRoot> GetWaiting(int userId);

        IQueryable<TApplyAggregateRoot> GetWaiting();
    }
}

大家如果熟悉之前代碼的話,會發現 IApplyRepository 的定義和 IJsPermissionApplyRepository 的定義是一摸一樣的,設計 IApplyRepository 的好處就是,對於申請實體的相同操作,我們就不需要再寫重覆代碼了,比如 IJsPermissionApplyRepository 和 IBlogChangeApplyRepository 的定義:

namespace CNBlogs.Apply.Repository.Interfaces
{
    public interface IJsPermissionApplyRepository : IApplyRepository<JsPermissionApply>
    { }
}

namespace CNBlogs.Apply.Repository.Interfaces
{
    public interface IBlogChangeApplyRepository : IApplyRepository<BlogChangeApply>
    {
        IQueryable<BlogChangeApply> GetByTargetAliasWithWait(string targetBlogApp);
    }
}

當然,除了上面的代碼改造,還有一些其他功能的添加,比如 ApplyAuthenticationService 領域服務增加了 VerfiyForBlogChange 等等,具體的一些改變,大家可以查看提交

CNBlogs.Apply.Sample 開發進行到這,對於現階段的我來說,應用領域驅動設計我是比較滿意的,雖然還有一些不完善的地方,但至少除了 CNBlogs.Apply.Domain,在其他項目中是看不到業務實現代碼的,如果業務需求發生變化,首先更改的是 CNBlogs.Apply.Domain,而不是不是其它項目,這是一個基本點。

先設計 CNBlogs.Apply.Domain 和 CNBlogs.Apply.Domain.Tests,就能完成整個的業務系統設計,其它都是一些技術實現或工作流程實現,這個路子我覺得是正確的,以後邊做邊完善並學習。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ...
  • Linux編程時候,如果我們需要調用shell命令或腳本通常使用system方法。如system("ls") 該方法返回值為0或-1,即成功或失敗。而有的時候我們想要獲取shell命令執行的結果,該怎麼辦呢? 我們可以將shell命令結果重定向到文件中,然後再讀取這個文件,如: system("ls ...
  • CentOS_配置_docker CentOS_6.5 1、CentOS_6.5在安裝docker-io之前需要首先卸載docker包(沒下載過可以省略) $ sudo yum -y remove docker 2、CentOS_6.5在安裝docker之前需要首先安裝並啟用EPEL源 $ yum ...
  • 什麼是運算符 什麼是運算符?運算符是告訴PHP做相關運算的標識符號。例如,你需要計算123乘以456等於多少,這時候就需要一個符號,告訴伺服器,你需要做乘法運算。 PHP中的運算符有哪些?PHP運算符一般分為算術運算符、賦值運算符、比較運算符、三元運算符、邏輯運算符、字元串連接運算符、錯誤控制運算符 ...
  • 恢復內容開始 輸入需要用scanner機制 代碼: 啟用scanner機制 Scanner input = new Scanner(System.in); //String x= input.next(); //String x = input.nextLine(); //int num = inp ...
  • 簡介 最小(少)原則,是安全的重要原則。最小的許可權,最小的用戶,最少的服務,最少的進程,是最安全的。 系統安全包括:文件系統保護、用戶管理安全、進程的保護以及日誌的管理。 場景 1. 確保服務最少,每個都是有用,而且許可權最小化。 2. 確保用戶最少,每個都是有用,而且許可權最小化。 3. 確保文件許可權 ...
  • 之前已經介紹過, skynet 只是一個輕量框架,不是一個開箱即用的引擎 。能不能用好它,取決於使用者是否清楚知道自己要乾什麼,如果是用 skynet 做網路游戲伺服器,那麼就必須先知道網路游戲伺服器應該如何設計。 在 skynet 發佈版中帶的 example 中,有類似 gate watchdo ...
  • 資源的表現層狀態轉化。 簡單的理解即: 1 URI對應一種"資源"。 2 客戶端與服務端傳輸資源的某種"表現層"。 3 客戶端通過HTTP協議的動詞,對資源進行操作,實現"表現層狀態轉化" 。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...