百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

MASA Framework 领域驱动设计 领域驱动建模

ztj100 2024-12-25 16:50 23 浏览 0 评论

概念

什么是领域驱动设计?
领域驱动的主要思想是, 利用确定的业务模型来指导业务与应用的设计和实现。主张开发人员与业务人员持续地沟通和模型的持续迭代,从而保证业务模型与代码的一致性,实现有效管理业务的复杂度,优化软件设计的目的

痛点

基于领域驱动设计的模型有很多难点需要克服
  • 统一认知
    • 语言统一, 领域模型术语、DDD模式名称、技术专业术语、设计模式、业务术语等统一为大家都能认可且理解的名词, 避免在沟通中出现语言不统一, 从而出现高昂的沟通成本
    • 开发人员应统一认知, 清晰应用服务、领域服务职责、明确聚合根、实体、值对象的基础概念
  • 划分限界上下文、找到业务中的核心域、子域、支撑域、通用域
  • 建立聚合根、实体、值对象,明确领域服务与对象的依赖关系
Masa Framework框架提供了基础设施使得基于领域驱动设计的开发更容易实现, 但它并不能教会你什么是DDD, 这些概念知识需要我们自己去学习、理解

功能科普

为了方便更好的理解, 下面会先说说关于领域驱动设计的包以及功能职责

Masa.BuildingBlocks.Ddd.Domain

提供了DDD中一些接口以及实现, 它们分别是:

  • Entity (实体) 接口规范、实体实现

未指定主键类型的实体需要通过重写GetKeys方法来指定主键, 聚合根支持添加领域事件 (并在EventBus的Handler执行完成后执行)
小窍门: 继承以AggregateRoot结尾的类是聚合根、继承以Entity结尾的类是实体
  • Event (事件) 接口

领域事件是由聚合根或者领域服务发出的事件, 其中根据事件类型又可以分为本地事件 (DomainEvent)、集成事件 (IntegrationDomainEvent), 而本地事件根据读写性质不同划分为DomainCommandDomainQuery

IDomainEventBus (领域事件总线)被用于发布领域事件, 支持发布本地事件和集成事件, 同时它还支持事件的压栈发送, 压栈发送的时间将在 UnitOfWork(工作单元) 提交后依次发送
  • Repository (仓储) 接口、仓储基类实现

屏蔽业务逻辑和持久化基础设施的差异, 针对不同的存储设施, 会有不同的实现方式, 但这些不会对我们的业务产生影响, 作为开发者只需要根据实际情况使用对应的依赖包即可, 与 DAO (数据访问对象)略有不同, DAO是数据访问技术的抽象, 而Repository是领域驱动设计的一部分, 我们仅会提供针对聚合根做简单的增删改查操作, 而并非针对单个表
由于一些特殊的原因, 我们解除了对非聚合根的限制, 使得它们也可以使用IRepository, 但这个是错误的, 后续版本仍然会增加限制, 届时IRepository将只允许对聚合根进行操作
  • Enumeration (枚举类)

提供枚举类基类, 使用枚举类来代替使用枚举, 查看原因
(https://learn.microsoft.com/zh-cn/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types)
  • 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 (https://github.com/FluentValidation/FluentValidation)的中间件, 为事件提供参数验证的功能 (后续与MasaBlazor (https://www.masastack.com/blazor)对接后参数错误提示更友好, 而不是简单的Toast)
  • Masa.Contrib.Dispatcher.Events: 本地事件总线实现
  • Masa.Contrib.Dispatcher.IntegrationEvents.Dapr: 基于dapr (https://docs.dapr.io/zh-hans/)的集成事件实现
  • Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EFCore: 为集成事件提供发件箱模式支持
  • Masa.Contrib.Data.UoW.EFCore: 提供工作单元实现
  • FluentValidation.AspNetCore: 提供基于FluentValidation的参数验证
  • FluentValidation.AspNetCore: 提供基于FluentValidation的参数验证

入门

我们先简单了解一下下单的流程, 如下图所示

其中事务中间件 (默认提供) 与验证中间件是公共代码, 进程内事件发布后都会执行, 但事务中间件不支持嵌套

通过Ddd设计下单设计到的代码过多, 下面代码只会展示重要部分, 不会逐步讲解, 希望大家谅解, 有不理解的加群或者评论探讨

  • 安装.NET 6.0

1.分别创建Assignment17.Ordering.API (订单服务, ASP.NET Core Web项目)、Assignment17.Ordering.Domain (订单领域, 类库)、Assignment17.Ordering.Infrastructure (订单基础设施, 类库)
2.注册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>();
});
3.在Program.cs中注册DaprStarter
if (builder.Environment.IsDevelopment())
{
builder.Services.AddDaprStarter(options =>
{
options.DaprGrpcPort = 3000;
options.DaprGrpcPort = 3001;
});
}
如果不使用Dapr, 则可以不注册DaprStarter

4.Dapr订阅集成事件

app.UseRouting();

app.UseCloudEvents();
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler();
});

5.下单参数验证

为下单提供参数验证, 确保进入应用服务Handler的请求参数是合法有效的

public class CreateOrderCommandValidator: AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(o => o.Country).Not().WithMessage("收件人信息有误");
RuleFor(o => o.City).Not().WithMessage("收件人信息有误");
RuleFor(o => o.Street).Not().WithMessage("收件人信息有误");
RuleFor(o => o.ZipCode).Not().WithMessage("收件人邮政编码信息有误");
}
}
参数验证无需手动触发, 框架会根据传入ValidatorMiddleware自动触发

6.下单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);
}
}

7.下单时聚合根发布订单状态变更事件

public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
string cardHolderName, DateTime cardExpiration, int? buyerId = , int? paymentMethodId = ) : 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;

8.订单状态变更领域事件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 != ;

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);
}
}
9.订阅订单状态更改为已提交集成事件, 修改Program.cs
app.MapPost("/integrationEvent/OrderStatusChangedToSubmitted",
[Topic("pubsub", nameof(OrderStatusChangedToSubmittedIntegrationEvent))]
(ILogger<Program> logger, OrderStatusChangedToSubmittedIntegrationEvent @event) =>
{
logger.LogInformation("接收到订单提交事件, {Order}", @event);
});

最终的项目结构:

下单的核心逻辑来自于eShopOnContainers (https://github.com/dotnet-architecture/eShopOnContainers), 属于简化版的下单, 通过它大家可以更快的理解如何借助Masa Framework, 方便快捷的设计出基于领域驱动设计的业务系统

参考

  • MASA Framework - DDD设计(1)
  • MASA Framework - DDD设计(2)
  • DDD 概念参考
  • DAO与Repository有什么区别

本章源码

Assignment17

https://github.com/zhenlei520/MasaFramework.Practice

开源地址

MASA.Framework:https://github.com/masastack/MASA.Framework


如果你对我们的 MASA Framework 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们


相关推荐

sharding-jdbc实现`分库分表`与`读写分离`

一、前言本文将基于以下环境整合...

三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么

在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...

MySQL8行级锁_mysql如何加行级锁

MySQL8行级锁版本:8.0.34基本概念...

mysql使用小技巧_mysql使用入门

1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...

MySQL/MariaDB中如何支持全部的Unicode?

永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...

聊聊 MySQL Server 可执行注释,你懂了吗?

前言MySQLServer当前支持如下3种注释风格:...

MySQL系列-源码编译安装(v5.7.34)

一、系统环境要求...

MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了

对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...

MySQL字符问题_mysql中字符串的位置

中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...

深圳尚学堂:mysql基本sql语句大全(三)

数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...

MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?

大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...

一文讲清怎么利用Python Django实现Excel数据表的导入导出功能

摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...

用DataX实现两个MySQL实例间的数据同步

DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...

MySQL数据库知识_mysql数据库基础知识

MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...

如何为MySQL中的JSON字段设置索引

背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...

取消回复欢迎 发表评论: