MasaFramework -- 领域驱动设计
概念
什么是领域驱动设计
领域驱动的主要思想是, 利用确定的业务模型来指导业务与应用的设计和实现。主张开发人员与业务人员持续地沟通和模型的持续迭代,从而保证业务模型与代码的一致性,实现有效管理业务的复杂度,优化软件设计的目的
痛点
基于领域驱动设计的模型有很多难点需要克服
- 统一认知
- 语言统一, 领域模型术语、DDD模式名称、技术专业术语、设计模式、业务术语等统一为大家都能认可且理解的名词, 避免在沟通中出现语言不统一, 从而出现高昂的沟通成本
- 开发人员应统一认知, 清晰应用服务、领域服务职责、明确聚合根、实体、值对象的基础概念
- 划分限界上下文、找到业务中的核心域、子域、支撑域、通用域
- 建立聚合根、实体、值对象,明确领域服务与对象的依赖关系
Masa Framework
框架提供了基础设施使得基于领域驱动设计的开发更容易实现, 但它并不能教会你什么是DDD
, 这些概念知识需要我们自己去学习、理解
功能科普
为了方便更好的理解, 下面会先说说关于领域驱动设计的包以及功能职责
Masa.BuildingBlocks.Ddd.Domain
提供了DDD中一些接口以及实现, 它们分别是:
- Entity (实体) 接口规范、实体实现
未指定主键类型的实体需要通过重写GetKeys
方法来指定主键, 聚合根支持添加领域事件 (并在EventBus的Handler执行完成后执行)
小窍门: 继承以
AggregateRoot
结尾的类是聚合根、继承以Entity
结尾的类是实体
- Event (事件) 接口
领域事件是由聚合根或者领域服务发出的事件, 其中根据事件类型又可以分为本地事件 (DomainEvent
)、集成事件 (IntegrationDomainEvent
), 而本地事件根据读写性质不同划分为DomainCommand
、DomainQuery
IDomainEventBus
(领域事件总线)被用于发布领域事件, 支持发布本地事件
和集成事件
, 同时它还支持事件的压栈发送, 压栈发送的时间将在UnitOfWork
(工作单元) 提交后依次发送
- Repository (仓储) 接口、仓储基类实现
屏蔽业务逻辑和持久化基础设施的差异, 针对不同的存储设施, 会有不同的实现方式, 但这些不会对我们的业务产生影响, 作为开发者只需要根据实际情况使用对应的依赖包即可, 与 DAO
(数据访问对象)略有不同, DAO
是数据访问技术的抽象, 而Repository
是领域驱动设计的一部分, 我们仅会提供针对聚合根
做简单的增删改查操作, 而并非针对单个表
由于一些特殊的原因, 我们解除了对非聚合根的限制, 使得它们也可以使用
IRepository
, 但这个是错误的, 后续版本仍然会增加限制, 届时IRepository
将只允许对聚合根进行操作
- Enumeration (枚举类)
提供枚举类基类, 使用枚举类来代替使用枚举, 查看原因
- Services 服务
领域服务是领域模型的操作者, 被用来处理业务逻辑, 它是无状态的, 状态由领域对象来保存, 提供面向应用层的服务, 完成封装领域知识, 供应用层使用。与应用服务不同的是, 应用服务仅负责编排和转发, 它将要实现的功能委托给一个或多个领域对象来实现, 它本身只负责处理业务用例的执行顺序以及结果的拼装, 在应用服务中不应该包含业务逻辑
继承IDomainService
的类被标记为领域服务, 领域服务支持从DI获取, 其中提供了EventBus
(用于提供发送领域事件)
- Values: 值对象
继承ValueObject
的类被标记为值对象。值对象没有唯一标识, 任何属性的变化都视为新的值对象
在项目开发中, 我们可以通过模型映射将值对象映射存储到单独的表中也可以映射为一个json字符串存储又或者根据属性拆分为多列使用, 这些都是可以的, 但无论数据是以什么方式存储, 它们是值对象这点不会改变, 因此我们不能错误的理解为在数据库中的表一定是实体或者聚合根, 这种想法是错误的
Masa.BuildingBlocks.Data.UoW
提供工作单元接口标准, 工作单元管理者, 确保Repository
的操作可以在同一个工作单元下的一致性 (全部成功或者全部失败)
功能与对应的nuget
包
Masa.Contrib.Ddd.Domain
: 领域驱动设计Masa.Contrib.Data.EFCore.SqlServer
: 基于EFCore的实现Masa.Contrib.Ddd.Domain.Repository.EFCore
: 提供仓储的默认实现Masa.Contrib.Development.DaprStarter.AspNetCore
: 协助管理Dapr Sidecar
, 运行dapr
Masa.Contrib.Dispatcher.Events.FluentValidation
: 提供基于FluentValidation
的中间件, 为事件提供参数验证的功能 (后续与MasaBlazor对接后参数错误提示更友好, 而不是简单的Toast
)Masa.Contrib.Dispatcher.Events
: 本地事件总线实现Masa.Contrib.Dispatcher.IntegrationEvents.Dapr
: 基于dapr
的集成事件实现Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EFCore
: 为集成事件提供发件箱模式支持Masa.Contrib.Data.UoW.EFCore
: 提供工作单元实现FluentValidation.AspNetCore
: 提供基于FluentValidation
的参数验证FluentValidation.AspNetCore
: 提供基于FluentValidation
的参数验证
入门
我们先简单了解一下下单的流程, 如下图所示
其中
事务中间件
(默认提供) 与验证中间件
是公共代码, 进程内事件发布后都会执行, 但事务中间件不支持嵌套
通过Ddd设计下单设计到的代码过多, 下面代码只会展示重要部分, 不会逐步讲解, 希望大家谅解, 有不理解的加群或者评论探讨
- 安装.NET 6.0
分别创建
Assignment17.Ordering.API
(订单服务, ASP.NET Core Web项目)、Assignment17.Ordering.Domain
(订单领域, 类库)、Assignment17.Ordering.Infrastructure
(订单基础设施, 类库)注册
DomainEventBus
(领域事件总线),EventBus
(事件总线),IntegrationEventBus
(集成事件总线), 并注册Repository
(仓储),IUnitOfWork
(工作单元)
builder.Services
.AddValidatorsFromAssembly(Assembly.GetEntryAssembly())//提供基于FluentValidation的参数验证
.AddDomainEventBus(assemblies.Distinct().ToArray(), options =>
{
options
.UseIntegrationEventBus(dispatcherOptions => dispatcherOptions.UseDapr().UseEventLog<OrderingContext>())
.UseEventBus(eventBuilder => eventBuilder.UseMiddleware(typeof(ValidatorMiddleware<>)))
.UseUoW<OrderingContext>(dbContextBuilder => dbContextBuilder.UseSqlServer())
.UseRepository<OrderingContext>();
});
- 在
Program.cs
中注册DaprStarter
if (builder.Environment.IsDevelopment())
{
builder.Services.AddDaprStarter(options =>
{
options.DaprGrpcPort = 3000;
options.DaprGrpcPort = 3001;
});
}
如果不使用
Dapr
, 则可以不注册DaprStarter
- Dapr订阅集成事件
app.UseRouting();
app.UseCloudEvents();
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler();
});
- 下单参数验证
为下单提供参数验证, 确保进入应用服务Handler的请求参数是合法有效的
public class CreateOrderCommandValidator: AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(o => o.Country).NotNull().WithMessage("收件人信息有误");
RuleFor(o => o.City).NotNull().WithMessage("收件人信息有误");
RuleFor(o => o.Street).NotNull().WithMessage("收件人信息有误");
RuleFor(o => o.ZipCode).NotNull().WithMessage("收件人邮政编码信息有误");
}
}
参数验证无需手动触发, 框架会根据传入
ValidatorMiddleware
自动触发
- 下单Handler
public class OrderCommandHandler
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderCommandHandler> _logger;
public OrderCommandHandler(IOrderRepository orderRepository, ILogger<OrderCommandHandler> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
[EventHandler]
public async Task CreateOrderCommandHandler(CreateOrderCommand message, CancellationToken cancellationToken)
{
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber,
message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
await _orderRepository.AddAsync(order, cancellationToken);
}
}
- 下单时聚合根发布订单状态变更事件
public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()
{
_buyerId = buyerId;
_paymentMethodId = paymentMethodId;
_orderStatusId = OrderStatus.Submitted.Id;
_orderDate = DateTime.UtcNow;
Address = address;
AddOrderStartedDomainEvent(userId, userName, cardTypeId, cardNumber,
cardSecurityNumber, cardHolderName, cardExpiration);
}
private void AddOrderStartedDomainEvent(string userId,
string userName,
int cardTypeId,
string cardNumber,
string cardSecurityNumber,
string cardHolderName,
DateTime cardExpiration)
{
var orderStartedDomainEvent = new OrderStartedDomainEvent(this, userId, userName, cardTypeId,
cardNumber, cardSecurityNumber,
cardHolderName, cardExpiration);
this.AddDomainEvent(orderStartedDomainEvent);
}
/// <summary>
/// Event used when an order is created
/// </summary>
public record OrderStartedDomainEvent(Order Order,
string UserId,
string UserName,
int CardTypeId,
string CardNumber,
string CardSecurityNumber,
string CardHolderName,
DateTime CardExpiration) : DomainEvent;
- 订单状态变更领域事件Handler
public class BuyerHandler
{
private readonly IBuyerRepository _buyerRepository;
private readonly IIntegrationEventBus _integrationEventBus;
private readonly ILogger<BuyerHandler> _logger;
public BuyerHandler(IBuyerRepository buyerRepository,
IIntegrationEventBus integrationEventBus,
ILogger<BuyerHandler> logger)
{
_buyerRepository = buyerRepository;
_integrationEventBus = integrationEventBus;
_logger = logger;
}
[EventHandler]
public async Task ValidateOrAddBuyerAggregateWhenOrderStarted(OrderStartedDomainEvent orderStartedEvent)
{
var cardTypeId = (orderStartedEvent.CardTypeId != 0) ? orderStartedEvent.CardTypeId : 1;
var buyer = await _buyerRepository.FindAsync(orderStartedEvent.UserId);
bool buyerOriginallyExisted = buyer != null;
if (!buyerOriginallyExisted)
{
buyer = new Buyer(orderStartedEvent.UserId, orderStartedEvent.UserName);
}
buyer!.VerifyOrAddPaymentMethod(cardTypeId,
$"Payment Method on {DateTime.UtcNow}",
orderStartedEvent.CardNumber,
orderStartedEvent.CardSecurityNumber,
orderStartedEvent.CardHolderName,
orderStartedEvent.CardExpiration,
orderStartedEvent.Order.Id);
var buyerUpdated = buyerOriginallyExisted ?
_buyerRepository.Update(buyer) :
_buyerRepository.Add(buyer);
var orderStatusChangedToSubmittedIntegrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(
orderStartedEvent.Order.Id,
orderStartedEvent.Order.OrderStatus.Name,
buyer.Name);
await _integrationEventBus.PublishAsync(orderStatusChangedToSubmittedIntegrationEvent);
_logger.LogTrace("Buyer {BuyerId} and related payment method were validated or updated for orderId: {OrderId}.",
buyerUpdated.Id, orderStartedEvent.Order.Id);
}
}
- 订阅订单状态更改为已提交集成事件, 修改
Program.cs
app.MapPost("/integrationEvent/OrderStatusChangedToSubmitted",
[Topic("pubsub", nameof(OrderStatusChangedToSubmittedIntegrationEvent))]
(ILogger<Program> logger, OrderStatusChangedToSubmittedIntegrationEvent @event) =>
{
logger.LogInformation("接收到订单提交事件, {Order}", @event);
});
最终的项目结构:
下单的核心逻辑来自于eShopOnContainers, 属于简化版的下单, 通过它大家可以更快的理解如何借助Masa Framework
, 方便快捷的设计出基于领域驱动设计的业务系统
参考
本章源码
Assignment17
https://github.com/zhenlei520/MasaFramework.Practice
开源地址
MASA.Framework:https://github.com/masastack/MASA.Framework
MASA.EShop:https://github.com/masalabs/MASA.EShop
MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor
如果你对我们的 MASA Framework 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们
MasaFramework -- 领域驱动设计的更多相关文章
- 浅谈我对DDD领域驱动设计的理解
从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...
- DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?
写在前面 阅读目录: 具体业务场景 业务需求变化 "愚蠢"的应对 消息列表实现 消息详情页实现 消息发送.回复.销毁等实现 回到原点的一些思考 业务需求变化,领域模型变化了吗? 对 ...
- DDD 领域驱动设计-商品建模之路
最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(3)
上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(2)> 这篇文章主要是对 DDD.Sample 框架增加 Transa ...
- DDD 领域驱动设计-两个实体的碰撞火花
上一篇:<DDD 领域驱动设计-领域模型中的用户设计?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 在 ...
- 初探领域驱动设计(2)Repository在DDD中的应用
概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...
- [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店
一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...
- 领域驱动设计实战—基于DDDLite的权限管理OpenAuth.net
在园子里面,搜索一下“权限管理”至少能得到上千条的有效记录.记得刚开始工作的时候,写个通用的权限系统一直是自己的一个梦想.中间因为工作忙(其实就是懒!)等原因,被无限期搁置了.最近想想,自己写东西时, ...
- 我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践
写在前面 插一句:本人超爱落网-<平凡的世界>这一期,分享给大家. 阅读目录: 关于DDD 前期分析 框架搭建 代码实现 开源-发布 后记 第一次听你,清风吹送,田野短笛:第一次看你,半弯 ...
- 一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?
写在前面 阅读目录: 问题根源是什么? <领域驱动设计-软件核心复杂性应对之道>分层概念 Repository(仓储)职责所在? Domain Model(领域模型)重新设计 Domain ...
随机推荐
- Pod 的生命周期
上图展示了一个 Pod 的完整生命周期过程,其中包含 Init Container.Pod Hook.健康检查 三个主要部分,接下来我们就来分别介绍影响 Pod 生命周期的部分: 首先在介绍 Pod ...
- Fluentd直接传输日志给Elasticsearch
官方文档地址:https://docs.fluentd.org/output/elasticsearch td-agent的v3.0.1版本以后自带包含out_elasticsearch插件,不用再安 ...
- 面试突击86:SpringBoot 事务不回滚?怎么解决?
在 Spring Boot 中,造成事务不自动回滚的场景有很多,比如以下这些: 非 public 修饰的方法中的事务不自动回滚: 当 @Transactional 遇上 try/catch 事务不自动 ...
- SpringBoot课程学习(三)
一.YAML格式的基本语法 (1)格式: 大小写敏感 数据值前边必须有空格,作为分隔符 使用缩进表示层级关系 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应的 空格数目可能不同,导致层 ...
- 虚拟线程 - VirtualThread源码透视
前提 JDK19于2022-09-20发布GA版本,该版本提供了虚拟线程的预览功能.下载JDK19之后翻看了一下有关虚拟线程的一些源码,跟早些时候的Loom项目构建版本基本并没有很大出入,也跟第三方J ...
- Educational Codeforces Round 106 (Rated for Div. 2)
就ac了2题... A题一开始题意模模糊糊的似懂非懂,然后自己按样例推出了题意,简单题很容易ac了.还是自己的英语水平太菜了.... B题根据0和1的位置关系能看出来,因为0不能在1后面, 所以有00 ...
- Docker容器技术基础
Docker基础 目录 Docker基础 容器(Container) 传统虚拟化与容器的区别 Linux容器技术 Linux Namespaces CGroups LXC docker基本概念 doc ...
- Docker | 使用dockerfile生成镜像,清理docker空间
用dockerfile生成镜像并挂载数据卷 编写dockerfile文件 创建dockerfile01 文件 # 基础镜像 FROM centos VOLUME ["volume01&quo ...
- Dubbo 02: 直连式
直连式 需要用到两个相互独立的maven的web项目 项目1:o1-link-userservice-provider 作为服务的提供者 项目2:o2-link-consumer 作为使用服务的消费者 ...
- 强国杯东杯分区赛miscwp
目录 不要被迷惑 PCAP文件分析 平正开 不要被迷惑 编辑 导出http 编辑 得到flag.zip后直接爆破密码 编辑 得到编辑 然后一键解码 编辑 flag{WImuJeqSNPh ...