十年河东,十年河西,莫欺少年穷
学无止境,精益求精
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 模式,
- 失血模型
- 贫血模型
- 充血模型
- 胀血模型
详见: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等
标签:set,聚合,MediatR,get,class,发送,Net6,public,ddd From: https://www.cnblogs.com/chenwolong/p/ddd.html