十年河东,十年河西,莫欺少年穷

学无止境,精益求精

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等

 

原文地址:http://www.cnblogs.com/chenwolong/p/ddd.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性