Spiga

ABP成长系列1:DDD

2024-04-13 15:06:58

最近有一个重写一个电商老系统的需求。这是一个运行了10来年的单体应用,随着业务的越来复杂,这套系统已经很难维护了,性能也越来越差,公司也终于启动了重写计划。

重写肯定是不会再使用单体架构了,具体是用分布式架构,还是微服务来写,其实也就是服务的颗粒度划分的问题。但无论是分布式还是微服务,系统的复杂度会是增加几十甚至上百倍。

在重写之前,我一直在想是完全从零开始搭建框架,还是使用ABP这样提供了基础设施最佳实践的框架。好在公司的业务也不是很复杂,几个人的团队已经可以着手重写的任务了。于是我计划在搭建框架前,先跟团队一直深入学习一遍ABP的底层。

ABP是模块化设计理念可以说是它的精髓,这也是之所以要学习这个框架的原因。我们的计划是使用ABP的基础核心库来打架项目基础创建,这样重写时哪些可以用ABP的基础设施来直接搭建应用,哪些要完全自建底层逻辑,哪些甚至可以套用旧代码简单重构,就会轻松很多。

说了这么多,不管是分布式还是微服务,ABP还是从零开始,首先要做的是对齐设计思想。而这绕不开的就是DDD,我们今天先来总结一下DDD的核心思想。

一、什么是领域驱动设计

1. 传统软件开发

  • 业务梳理 ==> 软件设计 ==> 开发

  • 初期-简单-迭代-复杂-冗杂-屎山代码-牵一发而动全身-重构系统-演进

    示例:订单服务(查询订单、创建订单、订单评价、支付……)

  • 项目流程:讨论需求==>数据库建模==>项目开发==>迭代数据库的设计==>上线

2. 领域驱动开发

  • 根据领域知识一步步驱动软件设计的过程就是领域驱动设计(DDD,Domain-Driven Design)
  • 领域驱动设计(DDD)是一种处理高度复杂领域的设计思想
  • 设计思想:通过分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性
  • 项目流程:领域模型=>数据库设计

3. 战略设计和战术设计

领域驱动设计包括战略设计和战术设计两部分:

  • 战略设计:从业务视角出发=》业务领域模型=》边界=》通用语言的限界上下文
  • 战术设计:从技术视角出发=》技术实现=》实体、聚合根、值对象、领域服务……

二、领域驱动设计的基础概念

1. 领域和子域

  • 领域(业务问题域)具体指一种特定的范围或区域,就是用来确定范围的,范围就是边界
  • 细分-限定-领域模型-代码实现
  • 领域越大业务范围就越大
  • 领域可以进一步划分为子领域(子域 更小的)
  • 子域对应一个更小的范围

我们来看一张经典的图

给桃树建立领域知识的步骤:

  1. 确定研究对象
  2. 对研究对象进行细分
  3. 对子对象再次进行细分
  4. 细分到最小单元

再来看一个电商项目的领域划分的例子:

  • 电商领域可以分成:商品子域、销售子域、订单子域、物流子域
  • 物流子域又可以分成:拣货子域、发货子域、派送子域

领域建模的过程和方法其核心思想就是将问题域逐步分解

降低业务理解和系统实现的复杂度

2. 核心域、通用域和支撑域

  • 核心域:产品核心业务功能
  • 通用域:被多个子域使用的通用功能(认证、授权、日志)
  • 支撑域:既不包含业务核心功能,也不包含通用功能

电商平台中:销售子域是核心域,物流子域是支撑销售业务的,所以它的支撑域

植物研究领域中,需要根据不同情况来划分核心域

  • 如果是在果园:果实就是核心域。为了让果实更饱满,果农可能会裁剪一些枝和叶
  • 如果是在公园:花就是核心域

3. 通用语言

  • 通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言
  • 通用语言中
    • 术语、用例场景:反映代码
    • 名词:领域对象命名(商品、订单)=》实体对象
    • 动词:动作、事件(商品已下单、订单已付款)=>领域事件、命令
  • 通用语音是一种语义环境

4. 限界上下文

  • 作用:用来确定语义所在的领域边界
  • 目的:为了避免同样的概念或语义,在不同的上下文环境中产生歧义
  • 分开理解:限界 ==> 领域的边界;上下文 ==> 语义环境
  • 合起来理解:限界上下文就是用来细分领域,从而定义通用语言所在的边界
  • 电商领域中,同样商品这个名词:
    • 对于销售子域,商品的概念就是商品,关心的是价格、特性
    • 对于物流子域,商品的概念变成了货物,关心的不再是价格了,而是重量、尺寸、数量

5. 实体和值对象

实体和值对象都是领域模型中的领域对象

实体≠实体类:领域驱动设计中要求实体是唯一的且可持续变化的

  • 唯一性:由唯一的身份标识来决定的;可持续变化:反映了实体本身的状态和行为
  • 实体的多种形态
    • 业务形态===战略设计、领域模型基础单元、操作、行为
    • 代码形态=实体类,属性、方法实现业务逻辑,充血模型,多个实体(领域服务)
    • 运行形态===实体对象,唯一ID
    • 数据形态===领域模型(表格)=》实体对象、行为=》数据模型

值对象≠值类型对象

  • 值对象=属性集合(拥有若干个用于描述目的、具有整体概念和不可修改的属性)
  • 值对象就是将一个值,用对象的方式进行表述,来表达一个具体的固定不变的概念
  • 值:数字(1、2、3)、字符串(“Hello”)、地址(北京朝阳区XX街道)
  • 对象:具体的事务
  • 可以是单一属性;也可以是属性集合

值对象嵌入实体的两种方式:

  • 属性嵌入:适用于单一属性的值对象,或只有一条记录的多属性值对象的实体
  • 序列化大对象:适用于一条或多条记录的多属性值对象的实体

实体与值对象的关系:

  • 值对象依附于实体,是实体属性的一部分
  • 实体着重唯一性和延续性,不在意属性的变化
  • 值对象着重描述性,对属性的变化很敏感
  • 地址,在物流子域是实体,在客户子域是客户实体的值对象

6. 聚合和聚合根

  • 聚合:将相关联的领域对象进行显式分组,来表达整体的概念

  • 聚合由业务和逻辑紧密关联的实体和值对象组合而成的,是数据修改和持久化的基本单元

  • 聚合有一个聚合根和上下文边界,聚合之间的边界是松耦合的

  • 个体(实体、值对象)组成=》社团、机构、公司,组织里的一员

  • 聚合(组织):实体+值对象,聚合根(负责人、实体ID、聚合的管理者、负责内部协调)

  • 聚合根:聚合根也称为根实体,它不仅是实体,还是聚合的管理者

  • 聚合根在聚合内部负责协调实体和值对象,按照固定的业务规则,协同完成共同的业务逻辑

  • 聚合根还是聚合对外的接口,接受外部任务和请求,在上下文内实现聚合之间的业务协同

  • 聚合根、实体、值对象的关系

    • 聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期

    • 关于实体,只要ID相同,就认为是同一个实体。而值对象要求所有的属性都相同。

    • 一个聚合只有一个聚合根

    • 实体具有聚合内唯一性标识,状态可变,依附于聚合根,其生命周期由聚合根管理

    • 值对象无标识,不可变,无生命周期

7. 领域服务与应用服务

  • 跨多个实体的业务逻辑通过领域服务来实现
  • 跨多个聚合的业务逻辑通过应用服务来实现
  • 一个聚合:A和B两个实体(领域服务)
  • 聚合C和聚合D中的两个领域服务(应用服务)
  • 应用服务用来表述应用行为,而领域服务用来表述领域行为
  • 应用行为描述了一个具体操作,从开始到结束的每一个环节
  • 领域行为则是对应用行为的细化,用来处理具体的某一个环节

比如:购物(用户购物车=>结算=应用行为,用户例程;计算金额、支付、订单、物流)

8. 如何设计聚合与聚合根

  • 聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界
  • 聚合的主要目的是用来封装业务和保证聚合内领域对象的数据一致性。

订单=必须具有唯一订单编号、订单日期、冗余商品的基本信息(名称、价格、折扣)、至少有一个商品

订单支付成功后=》更新已支付状态(订单子域)=》扣减库存(库存子域)

事务{更新订单状态();扣减库存();……}

示例:订单支付成功后,订单状态要更新为已支付状态,并且现有库存要根据订单中商品实际销售数量进行扣减。

  • 传统方式:通过事务 ==> 更新订单状态 & 扣减库存

  • DDD方式:从领域不变性的角度来看,我们应该维护各自子域内业务规则的不变性

9. 领域对象

一个领域模型会包含多个聚合,一个聚合包含多个领域对象,每个领域对象都有自己的领域类型

领域对象从哪里来?

  • 实体:大多数情况下,领域模型的业务实体与数据库实体是一一对应的
  • 聚合根:找出聚合根
  • 值对象:根据需要将某些实体的某些属性或属性集设计为值对象
  • 领域事件:如果领域模型中领域事件会触发下一步的业务操作,我们就需要设计领域事件
  • 领域服务:如果一个业务动作或行为跨多个实体,我们就需要设计领域服务
  • 仓储:如果需要数据持久化,那么每一个聚合应该有一个与之匹配的仓储

10. 领域事件

领域事件:用来表示领域中发生的事件

举例:当用户在购物车点击结算时,生成待付款订单,若支付成功,则更新订单状态为已支付,扣减库存,并推送捡货通知信息到捡货中心

领域事件的两个目的:解耦和拆分事务

如何使用领域事件来解耦?

  • 事件是由事件源和事件处理组合而成的。
  • 通过事件源辨别事件的来源,事件处理来表示事件导致的下一步操作,常见的实现方式就是事件总线
  • 事件总线是一种集中式事件处理机制,允许不同的组件之间,进行彼此通信而又不需要相互依赖,达到一种解耦的目的。
  • 最终一致性是一种设计方法,可以通过将某些操作的执行,延迟到稍后的时间,来提高应用程序的可扩展性和性能。

三、分层架构

1. 经典四层架构

  1. 用户接口层:负责向用户展现内容
  2. 应用层:主要面向用例和流程相关的操作
  3. 领域层:实现系统核心业务逻辑
  4. 基础层:向其他层提供通用的技术能力

2. 洋葱架构,又称整洁架构

3. 六边形架构

4. 三种架构的对比