概念

什么是领域驱动设计

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

痛点

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

  • 统一认知

    • 语言统一, 领域模型术语、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. Kubernetes DevOps: Gitlab

    Gitlab 官方提供了 Helm 的方式在 Kubernetes 集群中来快速安装,但是在使用的过程中发现 Helm 提供的 Chart 包中有很多其他额外的配置,所以我们这里使用自定义的方式来安装 ...

  2. 第一个Django应用 - 第四部分:表单和类视图

    一.表单form 为了接收用户的投票选择,我们需要在前端页面显示一个投票界面.让我们重写先前的polls/detail.html文件,代码如下: <h1>{{ question.quest ...

  3. Docker安装prometheus

    # 拉取镜像 docker pull prom/prometheus:v2.22.0 # 创建配置文件 mkdir -p /opt/prometheus/ cd /opt/prometheus vim ...

  4. ofd格式文件转换成pdf格式的方法

    ofd格式文件很多人还比较陌生,很多人接收到文件都不知如何打开阅读,把文件发给对方,还需要对方安装个专门的阅读软件,我们还有另一个办法,就是将OFD文件转换为PDF格式文件,然后把PDF格式文件发给对 ...

  5. CentOS 7.9 安装 redis-6.2.0

    一.CentOS 7.9 安装 redis-6.2.0 1 下载地址:https://download.redis.io/releases/redis-6.2.0.tar.gz 2 安装gcc来进行编 ...

  6. pycharm下载与使用

    pycharm下载与使用 PyCharm是一种Python IDE(Integrated Development Environment,集成开发环境),带有一整套可以帮助用户在使用Python语言开 ...

  7. 记一次 .NET 某企业OA后端服务 卡死分析

    一:背景 1.讲故事 前段时间有位朋友微信找到我,说他生产机器上的 Console 服务看起来像是卡死了,也不生成日志,对方也收不到我的httpclient请求,不知道程序出现什么情况了,特来寻求帮助 ...

  8. Oracle 同义词详解(synonym)

    Oracle 同义词详解(synonym) 一.Oracle同义词概念 Oracle 数据库中提供了同义词管理的功能.同义词是数据库方案对象的一个别名,经常用于简化对象访问和提高对象访问的安全性.在使 ...

  9. IOC常用的创建对象方式

    通过无参构造方法来创建 1.User.java public class User { private String name; public User() { System.out.println( ...

  10. 分支结构中的if-else(条件判断结构)

    一.三种结构 第一种: if(条件表达式){ 执行表达式}第二种:二选一 if(条件表达式){ 执行表达式1}else{ 执行表达式2}第三种:n选一 if(条件表达式){ 执行表达式1}else i ...