基于DDD的模块化单体应用(一)

本文翻译自https://github.com/kgrzybek/modular-monolith-with-ddd,项目采用MIT宽松许可

1. 领域

知识、影响或活动的领域,用户应用程序的主题区域是软件的域。

DDD中将系统分为UI层、应用层、领域层和基础设施层:
在这里插入图片描述

上层模块不应该依赖于下层模块,两者都应该依赖于抽象;
抽象不应该依赖于实现,实现应该依赖于抽象;

应用层是很薄的一层,因为它只负责接收UI层传来的参数和路由到对应的领域模型,它不负责处理具体的业务逻辑。系统的业务逻辑放在了领域层中,所以,领域层在系统架构中占据了很大的面积。下面以会议系统举例讲解。

1.1 描述

会议(Meetings)

  • 主要的业务实体包括会议(Meeting)、会议组(Meeting Group)和会议成员(Member)。会议成员可以创建会议组,或者是会议组的一部分,或者能够参加会议。
  • 会议组成员可以是会议组的组织者(Organizer)或者是普通成员。
  • 只有会议组的组织者可以创建新的会议。
  • 会议包括参与者(Attendees)和不参与者(not attendees)以及候选成员。
  • 会议可以有与会限制条件,如果达到限制条件,成员只能在等待列表签到。
  • 会与参与者可以携带宾客到会议,会议宾客数量是会议的一个属性,会议也可以禁止携带宾客。
  • 会议参与者可以有两种角色:普通会议参与者和主办者。一个会议至少包括一个主办者,主办者就有特殊权限,包括更改会议信息和修改参与者列表。

管理(Administration)

创建新的会议组需要成员先提出创建会议组申请,申请会发送到管理员进行审核,管理员可以接受或者拒绝申请,接受申请后则会议组自动创建。

支付(Payments)

会议组的组织者需要支付会议组费用,会议组织者也可以向会议参与者及宾客收取费用。

用户(Users)

  • 每个管理员、会议成员及支付者都是用户,想要成为用户,需要进行注册和审核。
  • 每个用户可以拥有一个到多个角色。
  • 每个角色包含一系列权限。权限定义了用户是否能够执行某个特定的行为。

1.2 概念模型(Conceptual Model)

概念模型用于信息世界建模,是现实世界到信息世界的第一层抽象,是数据库设计人员进行数据库设计的有力工具,也是数据库设计人员与用户之间进行交流的语言。因此,概念模型应该有较强的语义表达能力,另一方面它还应该简单、清晰、易于用户理解。最常用的就是E-R模型图。
在这里插入图片描述

1.3 事件风暴(Event Storming)

概念模型聚焦在域的结构及域间的关系,更重要的是域中的行为和事件。有很多方法可以展示行为和事件,一种非常流行的轻量级技术叫做Event Storming。

  • 用户注册过程:
    在这里插入图片描述
  • 会议组创建:
    在这里插入图片描述
  • 会议组织:
    在这里插入图片描述

2.架构

2.1 顶层视图

在这里插入图片描述
模块描述:

  • API-REST API应用程序,主要职责:1.获取请求,2.认证和授权,3.委托工作到指定模块发送命令和查询,4.返回响应;
  • 用户访问-用户认证、授权和注册;
  • 会议-实现会议边界上下文:创建会议组和会议;
  • 管理-实现管理边界上下文:实现管理任务比如用户组申请审核;
  • 支付-实现支付边界上下文:实现跟支付相关的所有功能;
  • 内存消息总线-异步订阅/发布事件,使用事件集成所有模块;

关键点:

  • API不包括任何程序逻辑;
  • API与模块通信使用一个小的接口来发送查询和命令;
  • API用到的模块都有一个自己的接口;
  • 模块间的通信只通过事件总线异步通信,不使用RPC;
  • 每个模块拥有自己的数据,使用单独的数据库,不允许数据共享;
  • 模块不应依赖其他模块,模块只依赖其他模块的集成事件程序集;
  • 各个模块拥有自己的组合根(Composition Root),意味着每个模块都有自己独立的控制反转模块;
  • API作为宿主需要初始化各个模块,每个模块都有一个初始化方法;
  • 每个模块都是高度封装的,只有需要的类型和成员是公共的,剩下的都是内部和私有的;

2.2 模块级视图

在这里插入图片描述
每个模块包含以下几个子模块(程序集):

  • 应用:子模块的主要部分,负责初始化、处理所有请求、内部命令及集成事件;
  • 领域:领域驱动设计术语中在指定边界上下文中的领域模型;
  • 基础设施:基础代码实现,如EF配置和映射;
  • 集成事件:发布到消息总线上的集成事件契约,只有这个程序集可以跟其他模块共享;
    在这里插入图片描述

2.3 API和模块通信

API与模块通信仅发生在两个地方:模块初始化和请求处理。

模块初始化

API在Startup类里调用各模块的静态的Initialize方法。所有该模块用到的配置需要作为参数传递到该方法。

public static void Initialize(
    string connectionString,
    IExecutionContextAccessor executionContextAccessor,
    ILogger logger,
    EmailsConfiguration emailsConfiguration)
{
    var moduleLogger = logger.ForContext("Module", "Meetings");

    ConfigureCompositionRoot(connectionString, executionContextAccessor, moduleLogger, emailsConfiguration);

    QuartzStartup.Initialize(moduleLogger);

    EventsBusStartup.Initialize(moduleLogger);
}

请求处理

各模块具有统一的接口暴露给API。该接口包含三个函数:执行带返回值的命令、执行不带返回值的命令和查询。

public interface IMeetingsModule
{
    Task<TResult> ExecuteCommandAsync<TResult>(ICommand<TResult> command);

    Task ExecuteCommandAsync(ICommand command);

    Task<TResult> ExecuteQueryAsync<TResult>(IQuery<TResult> query);
}

注意:一些人认为处理命令不应该返回结果,这个个好的方式但是有时不实用,尤其是你创建了一个资源并且想马上获得资源ID的时候。有时命令和查询的边界是很模糊的,举个例子认证命令(AuthenticateCommand)返回了一个token但是它并不是一个查询。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值