概念

什么是领域驱动设计

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

痛点

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

  • 统一认知

    • 语言统一, 领域模型术语、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 (枚举类)

提供枚举类基类, 使用枚举类来代替使用枚举, 查看原因

  • 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设计下单设计到的代码过多, 下面代码只会展示重要部分, 不会逐步讲解, 希望大家谅解, 有不理解的加群或者评论探讨

  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);
});

最终的项目结构:

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

  1. 浅谈我对DDD领域驱动设计的理解

    从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...

  2. DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?

    写在前面 阅读目录: 具体业务场景 业务需求变化 "愚蠢"的应对 消息列表实现 消息详情页实现 消息发送.回复.销毁等实现 回到原点的一些思考 业务需求变化,领域模型变化了吗? 对 ...

  3. DDD 领域驱动设计-商品建模之路

    最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...

  4. DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(3)

    上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(2)> 这篇文章主要是对 DDD.Sample 框架增加 Transa ...

  5. DDD 领域驱动设计-两个实体的碰撞火花

    上一篇:<DDD 领域驱动设计-领域模型中的用户设计?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 在 ...

  6. 初探领域驱动设计(2)Repository在DDD中的应用

    概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...

  7. [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店

    一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...

  8. 领域驱动设计实战—基于DDDLite的权限管理OpenAuth.net

    在园子里面,搜索一下“权限管理”至少能得到上千条的有效记录.记得刚开始工作的时候,写个通用的权限系统一直是自己的一个梦想.中间因为工作忙(其实就是懒!)等原因,被无限期搁置了.最近想想,自己写东西时, ...

  9. 我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践

    写在前面 插一句:本人超爱落网-<平凡的世界>这一期,分享给大家. 阅读目录: 关于DDD 前期分析 框架搭建 代码实现 开源-发布 后记 第一次听你,清风吹送,田野短笛:第一次看你,半弯 ...

  10. 一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?

    写在前面 阅读目录: 问题根源是什么? <领域驱动设计-软件核心复杂性应对之道>分层概念 Repository(仓储)职责所在? Domain Model(领域模型)重新设计 Domain ...

随机推荐

  1. ProxySQL SQL 注入引擎

    ProxySQL 2.0.9 引入了 libsqlinjection 作为识别可能的 SQL 注入攻击的机制. 启用 S​​QL 注入检测 要启用 SQL 注入检测,只需要启用变量 mysql-aut ...

  2. ProxySQL(9):ProxySQL的查询缓存功能

    文章转载自: https://www.cnblogs.com/f-ck-need-u/p/9314459.html ProxySQL支持查询缓存的功能,可以将后端返回的结果集缓存在自己的内存中,在某查 ...

  3. 如何调试 Docker

    开启 Debug 模式 在 dockerd 配置文件 daemon.json(默认位于 /etc/docker/)中添加 { "debug": true } 重启守护进程. $ s ...

  4. Readsh中文版初始设置

    B站视频教程网址:https://space.bilibili.com/630285695/video 安装成功后,打开浏览器输入http://ip:5000如果出现如下画面,即告安装成功. 初始设置 ...

  5. 实现fastdfs防盗链功能

    目录 1.背景 2.实现原理 2.1 开启防盗链 2.2 重启 nginx 2.3 Java代码生成token 1.token生成规则 2.java生成token 3.测试 3.1 带正确token访 ...

  6. Java程序员必会Synchronized底层原理剖析

    synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用. 但不可否认的是synchr ...

  7. 8.pygame-定时器

    使用定时器添加敌机 游戏启动后,每隔一秒出现一架敌方飞机 每架飞机向屏幕下方飞行,速度各不相同 没加敌机出现的水平位置也不进相同 当敌机从屏幕下方飞出,不会再飞回到屏幕中   定时器 pygame中使 ...

  8. Uniapp And Taro一些小测评

    前情 最近公司准备新开发一个小程序项目,对于使用哪一款小程序框架有一些犹豫,我有过2年左右的uniapp项目开发经验,Taro在刚刚出来的时候有尝试过,经常莫名报错需要重启,在内心是有些偏向uniap ...

  9. Jenkins发版通知企业微信机器人

    1)开始通知 在Jenkins发版过程的第一步添加下面内容,调用下面脚本实现机器人发版通知(注意脚本路径和传参) ${BUILD_USER}是Jenkins内置变量,执行发布的用户名,需要安装插件-B ...

  10. Goland环境中Go module配置

    [现象] 从go vendor切换到go module之后,import包解析有问题.如下所示: 对应的go modules也没解析出来 [原因] 有两点原因: goland中go module配置存 ...