MasaFramework -- 领域驱动设计

时间:2022-12-05 10:10:32

概念

什么是领域驱动设计

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

痛点

基于领域驱动设计的模型有很多难点需要克服

  • 统一认知
    • 语言统一, 领域模型术语、DDD模式名称、技术专业术语、设计模式、业务术语等统一为大家都能认可且理解的名词, 避免在沟通中出现语言不统一, 从而出现高昂的沟通成本
    • 开发人员应统一认知, 清晰应用服务、领域服务职责、明确聚合根、实体、值对象的基础概念
  • 划分限界上下文、找到业务中的核心域、子域、支撑域、通用域
  • 建立聚合根、实体、值对象,明确领域服务与对象的依赖关系

Masa Framework框架提供了基础设施使得基于领域驱动设计的开发更容易实现, 但它并不能教会你什么是DDD, 这些概念知识需要我们自己去学习、理解

功能科普

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

Masa.BuildingBlocks.Ddd.Domain

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

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

MasaFramework -- 领域驱动设计

未指定主键类型的实体需要通过重写GetKeys方法来指定主键, 聚合根支持添加领域事件 (并在EventBus的Handler执行完成后执行)

小窍门: 继承以AggregateRoot结尾的类是聚合根、继承以Entity结尾的类是实体

  • Event (事件) 接口

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

MasaFramework -- 领域驱动设计

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的参数验证

入门

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

MasaFramework -- 领域驱动设计

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

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

  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>();
    });
  1. Program.cs中注册DaprStarter
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDaprStarter(options =>
    {
        options.DaprGrpcPort = 3000;
        options.DaprGrpcPort = 3001;
    });
}

如果不使用Dapr, 则可以不注册DaprStarter

  1. Dapr订阅集成事件
app.UseRouting();

app.UseCloudEvents();
app.UseEndpoints(endpoints =>
{
    endpoints.MapSubscribeHandler();
});
  1. 下单参数验证

为下单提供参数验证, 确保进入应用服务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自动触发

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

最终的项目结构:

MasaFramework -- 领域驱动设计

下单的核心逻辑来自于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 -- 领域驱动设计