概念

什么是领域驱动设计

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

痛点

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

  • 统一认知

    • 语言统一, 领域模型术语、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. 使用Elasticsearch的processors来对csv格式数据进行解析

    来源数据是一个csv文件,具体内容如下图所示: 导入数据到es中 有两种办法,第一种是在kibana界面直接上传文件导入 第二种方法是使用filebeat读取文件导入 这里采用第二种办法 配置文件名: ...

  2. 使用yum方式安装的openresty参数

    nginx version: openresty/1.19.3.1 built by gcc 8.3.1 20190311 (Red Hat 8.3.1-3) (GCC) built with Ope ...

  3. k8s安装常用软件的yaml文件

    参考网址:https://www.bejson.com (网站文件部分有坑,需要擦亮眼睛) nginx k8s版本:v1.20 apiVersion: apps/v1 kind: Deployment ...

  4. vue3基础

    什么是CDN? 内容分发网络--通过相互链接的网络系统,利用最靠近用户的服务器,更快更可靠的发送给用户. vue的cdn引入 method中的函数为什么不能用this? this的主要使用是来获取da ...

  5. H5与APP的交互框架(WebViewJavascriptBridge)

    基本原理是: 把 OC 的方法注册到桥梁中,让 JS 去调用. 把 JS 的方法注册在桥梁中,让 OC 去调用.(注册自己,调用它人.) WebViewJavaScriptBridge 使用的基本步骤 ...

  6. Go的网络编程详解

    一 互联网协议介绍 1.1互联网分层模型 互联网的逻辑实现被分为好几层.每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持.用户接触到的只是最上面的那一层,根本不会感觉到下面的几层.要理解互联 ...

  7. AgileBoot - 手把手一步一步带你Run起全栈项目(SpringBoot+Vue3)

    AgileBoot是笔者在业余时间基于ruoyi改造优化的前后端全栈项目. 关于AgileBoot的详细介绍:https://www.cnblogs.com/valarchie/p/16777336. ...

  8. 从源码分析 MGR 的流控机制

    Group Replication 是一种 Shared-Nothing 的架构,每个节点都会保留一份数据. 虽然支持多点写入,但实际上系统的吞吐量是由处理能力最弱的那个节点决定的. 如果各个节点的处 ...

  9. 一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)

    这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 1.分布式架构下,Session共享有什么方案 2.简述你对RPC.RMI的理解 3.分布式id生成方案 4.分布式锁解决 ...

  10. token字段,请务加在请求地址的头部header

    如下图所示,你必须在请求的头部加上 token参数,主要原因有两个.第一点,这个是登录标志,因为接口访问用不了cookie,所以只能通过这个header请求标志判断用户是否已经登录.第二点,系统有时候 ...