ddd領域驅動設計模型 及 Net6使用MediatR完成領域事件發送

十年河東,十年河西,莫欺少年窮

學無止境,精益求精

1、序言

領域驅動設計是一種解決業務複雜性的設計思想,不是一種標準規則的解決方法。 

2、ddd 領域驅動模型介紹

參考:https://www.zhihu.com/question/481820861 和  https://zhuanlan.zhihu.com/p/91525839

3、ddd 領域模型VS事務腳本

事務腳本其實就是程序員依照業務邏輯進行自然的代碼構造

比如下訂單

        public void 訂單()
        {
            保存訂單();
            發送郵件();
            增減積分();
        }

        public void 保存訂單()
        {

        }

        public void 發送郵件()
        {

        }

        public void 增減積分()
        {

        }

這種寫法,把大量的業務邏輯寫在方法內,一旦更改需求,就必須修改代碼,待業務足夠複雜時,代碼量都聚集在一個方法內,難以維護擴展。違背了設計模式的開閉原則

何爲領域模型呢?如果通過領域模型解決上述問題?可參考DDD的四種 Domain 模式,

  1. 失血模型
  2. 貧血模型
  3. 充血模型
  4. 脹血模型

詳見:https://zhuanlan.zhihu.com/p/91525839

4、ddd 實體與值對象 

值對象:沒有標識符的對象,也有多個屬性,依附於某個實體存在。

以訂單爲例,一般情況下,我們設計訂單狀態時,一般將訂單狀態字段設計爲 Int 類型,例如:0:待支付 1:已支付 2:已取消

以code first爲例,新建一個數據庫實體,如下:

    internal class OrderDto
    {
        public long uid { get; set; }
        public string? orderNo { get; set; }
        public int orderStatus { get; set; }
    }

上述實體中的orderStatus 不僅僅可以取值爲 0 、1 、2、還可以取值爲:100 、 200 、 888 等,這樣設計並不符合DDD的設計原則,那麼怎麼設計實體才符合DDD的設計原則呢?

    internal class OrderDto
    {
        public long uid { get; set; }
        public string? orderNo { get; set; }
        public OrderStatusEnum orderStatus { get; set; }
    }

    public enum OrderStatusEnum
    {
        待支付,已支付,已取消
    }

上述定義的枚舉類型即爲實體的值對象

再或者,以商家爲例

用戶要想快速的找到商家,商家就必須擁有經緯度屬性,方便用戶導航

一般情況下,我們都是這樣定義商家

  public class Shop
    {
        public long uid { get; set; }
        public string? shopName { get; set; }
        /// <summary>
        /// 緯度
        /// </summary>
        public double lat { get; set; }
        /// <summary>
        /// 經度
        /// </summary>
        public double lgt { get; set; }

        //.........其他字段
    }

按照ddd的思想,我們可以將經緯度單獨抽出來,如下

    public class Shop
    {
        public long uid { get; set; }
        public string? shopName { get; set; }
        
        public latlgt latlgt { get; set; }

        //.........其他字段
    }
    public class latlgt
    {
        public bool CheckLatlgt()
        {
            if (lat < -90 || lat > 90)
            {
                return false;
            }
            if (lgt < -180 || lat > 180)
            {
                return false;
            }
            return true;
        }
        /// <summary>
        /// 緯度
        /// </summary>
        public double lat { get; set; }
        /// <summary>
        /// 經度
        /// </summary>
        public double lgt { get; set; }

        //.........其他字段
    }

單獨抽出來的好處是重用、並且符合設計模式的單一職責模式,

5、聚合與聚合根

一個上下文內可能包含多個聚合,每個聚合都有一個根實體,叫做聚合根,一個聚合只有一個聚合根。

這裏面最重要的原則是:只有聚合根才能被外部訪問到,聚合根維護聚合的內部一致性。

以訂單和訂單詳情爲例

在code first 中,我們定義訂單和訂單詳情通常這樣定義

    internal class OrderDto
    {
        public long uid { get; set; }
        public string? orderNo { get; set; }
        public OrderStatusEnum orderStatus { get; set; }
        public List<OrderDtlDto> OrderDtls { get; set; }
    }

    public enum OrderStatusEnum
    {
        待支付,已支付,已取消
    }
  
    public class OrderDtlDto
    {
        public long uid { get; set; }
        public long orderId { get; set; }
        //..其他字段
    }

上述的訂單就是聚合根,訂單詳情屬於聚合根的從屬實體。

關於聚合和聚合根,可參考:https://zhuanlan.zhihu.com/p/146488464

6、領域服務、應用服務

以EfCore CodeFirst進行說明

領域服務是指:在同一個DbContext下,相同聚合根或不同聚合根之前的調用稱之爲領域服務,領域服務工作在同一個進程中,執行結果具有強一致性

應用服務是指:不同微服務之間的相同調用,他們之間的調用是基於網絡接口的形式,應用服務不在同一個進程內工作,執行結果不具有強一致性,屬於分佈式的範疇

上述表述是根據B站楊老師的視頻總結出來的,不完全準確,不需勿噴。

7、Net6 實現領域事件

在net6項目中引入Nuget包

MediatR.Extensions.Microsoft.DependencyInjec

註冊MediatR

builder.Services.AddMediatR(Assembly.GetAssembly(typeof(NotificationModel)));//當前程序集:Assembly.GetExecutingAssembly()

注意:註冊方法 AddMediatR 中的參數是命名空間,共MediatR掃描繼承INotification接口的類

發送方實體

在項目中新建發送方相關類,發送方相關類繼承自INotification接口

    /// <summary>
    /// 發送方內容  註冊MediatR時,掃描該類所屬命名空間
    /// </summary>
    public class NotificationModel : INotification
    {
        public string body { get; set; }
    }

發送方發送事件

在webApi中新建Action,進行事件發送

using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using swapModels.MediatrModels;

namespace swap.Controllers
{
    [AllowAnonymous]
    public class MediartController : BaseController
    {
        private readonly IMediator mediator;

        public MediartController(IMediator mediator)
        {
            this.mediator = mediator;
        }

        [HttpGet]
        public async Task<IActionResult> Test(CancellationToken cancellation=default)
        {
           await mediator.Publish<NotificationModel>(new NotificationModel() { body="hello"+DateTime.Now},cancellation);
            return Ok();
        }


    }
}
View Code

mediator 提供了兩個方法,一個是Publish,一個是Send,Publish 以廣播的形式進行事件發送,可以有多個接收方。send 只能有一個接收方,屬於點對點模式。

接收方代碼

    /// <summary>
    /// 接收方1
    /// </summary>
    public class NotificationHandler : INotificationHandler<NotificationModel>
    {
        public async Task Handle(NotificationModel notification, CancellationToken cancellationToken)
        {
            await Task.Run(() =>
            {
                Console.WriteLine("接收方1"+notification.body);
            });
        }
    }   /// <summary>
        /// 接收方2
        /// </summary>
    public class NotificationHandler2 : INotificationHandler<NotificationModel>
    {
        public async Task Handle(NotificationModel notification, CancellationToken cancellationToken)
        {
            await Task.Run(() =>
            {
                Console.WriteLine("接收方2" + notification.body);
            });
        }
    }

當運行項目,點擊swagger上Test方法時,將會有兩個接收方接收到發送事件發送的信息

 

 8、DDD集成事件的發送

集成事件屬於跨微服務之間的事件,工作在不同的線程內【微服務工作在不同服務器上】,因此使用上述的MediatR 就不能滿足需求了,我們需要藉助第三方的MQ中間件。

比如,Redis/KafKa/RabbitMQ等

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章