• 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等

  • 相关阅读:
    fastjson1.2.22-1.2.24 反序列化命令执行实践测试
    Spring boot JdbcTemplate sql注入测试
    java反序列化命令执行测试实践
    SpringBoot 整合mybatis SQL注入漏洞实践
    SpringBoot整合mybatis入门
    python函数默认参数为可变对象的理解
    python笔记
    python
    python面向对象
    ICMP
  • 原文地址:https://www.cnblogs.com/chenwolong/p/ddd.html
Copyright © 2020-2023  润新知