要点简述
- DDD适用于业务开发。DDD套路僵化,但能定义设计质量的底线,颇有文正公结硬寨打呆仗之妙
- DDD核心思想是分治。将复杂的、大规模的业务系统,统一语言、划分边界、分而治之
- DDD是动态演化过程。领域建模过程是不断猜想与反驳的过程,演化观点是建模过程的基本心智模式
- DDD建模的关键步骤:明确战略,用户故事,事件风暴,寻找聚合,划分限界上下文,API设计,代码实现
- DDD建模其它关键词:业务场景,边界划分,通用语言,领域对象、领域服务
- DDD常见的设计理念:六边形架构,代码分层架构,依赖倒置、适配器、防腐层。经典的六边形架构,如下图
基本概念
- DDD:Domain-Driven Design,领域驱动设计,概念有领域/子域、限界上下文、聚合、实体/值对象等
- 领域:Domain,用于划分问题空间,是特定场景下的业务范围;领域之下可以有多个子域,将复杂性分⽽治之
- 子域:子域是一个明确的专业领域、提供解决方案,分为核心、支撑、通用三类
- 核心域:决定产品和公司的核心竞争力,直接对业务产生价值
- 支撑域:完成业务的必要能力,专注于业务的某个方面、不是业务成功的主因
- 通用域:被整个业务使用、但不核心,可以外购,如权限、登陆
- 限界上下文:Bounded Context,用于划分解决方案空间,通过限界上下⽂、来明确聚合(或领域)的范围和职责
- 职责:限界上下文用来明确聚合(或领域)的范围和职责,定义了该领域内部的通用语言、模型和规则
- 原则:解决相同问题的聚合(或领域)应该被放到同一个限界上下文;如果⼀个聚合同时解决多个问题,则需要对聚合进⾏拆分,拆分后的聚合应该被划分到不同限界上下⽂中
- 形式:子域和限界上下文实现时通常是1对1的关系,但也能多对多
- 聚合:Aggregate,多个实体和值对象组成的业务规则叫聚合;聚合要封装业务的不变性,并明确边界、减少关联、做到高内聚低耦合,指导微服务实现
- 过程:找出模型(领域事件、业务名词、事件合并),内聚成聚合(角色、命令、事件)
- 聚合根:聚合里面一定有一个实体是聚合根;聚合根作用是保证内部的实体的一致性,外部操作只需要对阵聚合根
- 实体:Entity,实体是对真实业务形态的抽象,实体有唯一标识、有生命周期、且具有延续性
- 业务:实体能够反映业务的真实形态,是多个属性、操作或行为的载体
- 代码:实体的代码有属性、行为(充血模型),行为代表了大部分业务逻辑;只有属性、没有行为的成为贫血模型
- 运行:实体有唯一不变的ID,属性可修改
- 值对象:值对象主要用于描述实体特征,是一些列属性的集合。值对象没有唯一ID、没有生命周期、不可修改
- 领域服务:DomainService
- 事件风暴:Event Storming,从琐碎到聚化的业务领域建模过程
- 参与方:业务专家、产品经理、架构师、开发/测试
- 关键点:业务的实体、命令、事件,实体执行命令后产生事件
- 领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计
建模过程
DDD建模分为几个步骤,包括:产品场景、用户故事、事件风暴、寻找聚合、划分限界上下文、设计API、代码实现等。以电商场景为例,DDD建模过程如下图,
代码实现,参见代码规范章节。
数据对象
DDD中的数据对象,大致分为如下几类:
- VO(View Object)视图对象
- DTO(Data Transfer Object)数据传输对象
- BO(Business Object)业务对象
- DO (Domain Object):领域对象
- PO(Persistant Object)持久对象
- DAO(Data Access Object)数据访问对象
几种数据对象所处的分层及调用链路,大致如下。其中,BO对应领域向上游提供的API、DO对应领域向下游制定的SPI。
代码规范
以Golang语言为例,实现一个 报警通知按照BU分发 的功能。四层结构如下,
DDD四层代码样例(Golang)
①用户接口层
// pkg: httpcontroller
func Routes(r *gin.Engine) {
a := BuAlarmApi{}
group := r.Group("/api/v1/bualarm")
group.POST("/alarm", a.BuAlarm)
}
func (this *BuAlarmApi) BuAlarm(c *gin.Context) {
bu := c.DefaultPostForm("bu", "")
content := c.DefaultPostForm("content", "")
username, err := this.GetUser(c)
alarmDTO := dto.AlarmDTO{
BudgetUnit: bu,
Content: content,
Sysname: username,
}
err = app.BuAlarmApp.BuAlarm(alarmDTO)
...
c.JSON(http.StatusOK, this.Success())
}
②应用层
// pkg: app
// 规范: 应用对象以全局变量方式对外暴露,目的是解耦类实现
var BuAlarmApp = new(BuAlarmAppplication)
// 依赖注入: 每个app文件,使用init函数完成注入
func init() {
domain.BuAlarmDomainService.IMRepo = infra.IMRepo // 依赖注入: 基础设施
BuAlarmApp.domainBuAlarm = domain.BuAlarmDomainService // 依赖注入: 领域
}
type BuAlarmAppplication struct {
domainBuAlarm domain.BuAlarm
}
func (this *BuAlarmAppplication) BuAlarm(alarmDTO dto.AlarmDTO) error {
alarmBO := bo.AlarmBO{
BudgetUnit: alarmDTO.BudgetUnit,
Title: alarmDTO.Title,
Content: alarmDTO.Content,
}
err = this.domainBuAlarm.AlarmByBu(alarmBO)
...
return nil
}
③领域层(业务逻辑)
// pkg: domain
var BuAlarmDomainService = new(BuAlarmDomain)
// SPI: 基础设施接口定义
type IMRepo interface {
SendIM(im *do.IM) error
}
// DomainService: 领域服务
type BuAlarmDomain struct {
IMRepo IMRepo // IM基础设施
}
// API: 领域服务接口定义
func (this *BuAlarmDomain) AlarmByBu(alarmBO *bo.AlarmBO) (err error) {
bu := strings.TrimSpace(alarmBO.BudgetUnit)
...
imDO := do.IM{
BU: bu,
Content: alarmBO.Content,
}
err = this.IMRepo.SendIM(&imDO)
...
return nil
}
④基础设施层
// pkg: infra
// 规范: 基础设施对象以全局变量方式对外暴露,目的是解耦类实现、避免资源浪费
var IMRepo = new(IMSender)
// SP: 基础设施接口实现,IMSender实现IMRepo规定的SPI
type IMSender struct {
}
func (this *IMSender) SendIM(im *do.IM) (err error) {
SendChat(im.Tos, im.Content)
...
return nil
}
DDD四层代码样例(Java)
①用户接口层
package com.zyb.controller;
import com.zyb.application.BuAlarmApplication;
import com.zyb.application.dto.AlarmDTO;
import javax.annotation.Resource;
public class BuAlarmApi {
@Resource
private BuAlarmApplication buAlarmApplication;
public void sendEvents(String metric){
AlarmDTO alarmDTO = new AlarmDTO();
alarmDTO.setMetric(metric);
buAlarmApplication.buAlarm(alarmDTO);
}
}
②应用层
package com.zyb.application;
import com.zyb.application.dto.AlarmDTO;
import com.zyb.domain.bualarm.bo.AlarmBO;
import org.springframework.stereotype.Service;
import com.zyb.domain.bualarm.service.*;
import javax.annotation.Resource;
@Service
public class BuAlarmApplicationImpl implements BuAlarmApplication {
@Resource
private BuAlarmDomainService buAlarmDomainService;
@Override
public boolean buAlarm(AlarmDTO alarmDTO) {
AlarmBO alarmBO = new AlarmBO();
alarmBO.setMetric(alarmDTO.getMetric());
return buAlarmDomainService.alarmByBu(alarmBO);
}
}
③领域层(业务逻辑)
package com.zyb.domain.bualarm.service;
import com.zyb.domain.bualarm.bo.AlarmBO;
import com.zyb.domain.bualarm.dependency.BuChatDependency;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class BuAlarmDomainServiceImpl implements BuAlarmDomainService{
@Resource
private BuChatDependency buChatDependency;
@Override
public boolean alarmByBu(AlarmBO alarmBO) {
buChatDependency.getBuChat(alarmBO.getMetric());
return true;
}
}
④基础设施层
package com.zyb.infrastructure;
import com.zyb.domain.bualarm.dataobject.BuChatDO;
import com.zyb.domain.bualarm.dependency.BuChatDependency;
import com.zyb.infrastructure.dao.BuChatMapper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class BuChatDependencyImpl implements BuChatDependency {
@Resource
private BuChatMapper buChatMapper;
@Override
public BuChatDO getBuChat(String bu) {
return buChatMapper.getBuChat();
}
}
// DAO
package com.zyb.infrastructure.dao;
import com.zyb.domain.bualarm.dataobject.BuChatDO;
import org.springframework.stereotype.Service;
@Service
public class BuChatMapper {
public BuChatDO getBuChat(){
return null;
}
}
设计理念
- DDD架构:六边形架构,如下图
- 分层架构:模块代码实现分为四层,自上而下分别是用户接口层、应用层、领域层、基础层。如下图,
- 适配器:以领域服务为中心,向上提供API、向下约定SPI;表现上,通过适配器和外部交互,将应用服务&领域服务封装在系统内部、和外部解耦
- 依赖倒置:即SPI;由领域层依赖基础层,倒置成基础层依赖领域层;这样,其它层都依赖领域层、领域层不依赖其它层,领域层最终只受限于业务逻辑
- 充血模型:将业务逻辑封装在领域对象内,不仅仅是简单的数据容器、还包含了与业务相关的行为规则
- 表现形式:代码高内聚,属性对外不可见、只能通过方法访问和变更等
- 贫血模型:与充血模型相对应的是贫血模型,它将业务逻辑放在外部服务或管理类中、领域对象仅仅是简单的数据结构
以下为思考过程,内容待整理。
建模过程
DDD建模分为明确产品战略、梳理业务场景、划分技术领域、代码实现四个阶段,如下图。接下来以运维封线检查为例,介绍DDD建模的流程详情。
①明确产品战略:确定产品目标,明确用户、价值、功能等宏观事项。 封线检查是变更管控的核心策略之一,功能是限制所有变更的时间窗口,价值是降低变更带来的质量风险,用户是公司所有员工、特别是RD。封线检查的难点是,定义一个通用策略、使能适用于所有变更系统。
②梳理业务场景:事件风暴,各个角色坐在一起头脑风暴,全面还原业务场景、精细梳理业务需求、建立领域统一语言、得出一个业务和研发人员都能看懂的事件风暴图。 事件风暴图类似有限状态机FSM,由业务流程、命令、事件三个行组成。
- 业务流程是事件风暴的核心,是业务逻辑过程;
- 命令是业务上的输入,代表了给外界使用的功能,如提交客户订单、锁定账号。命令可以是用户UI操作、外部系统触发、内部定时任务等
- 事件是业务上的输出,是领域专家关心的、在业务上真实发生的事,如,客户订单已提交、账号被锁定(3次密码错误)。事件通过回溯历史的方式、明确业务流程中的状态变化,有主动、被动之分。
封线检查的事件风暴图,如下。通过业务讨论,封线检查域的业务流程包括封线策略的创建(含鉴权)、使用两个步骤。封线策略创建由管理员发起、属性包括服务对象、时段、等级,策略创建完成即可生效使用。使用方发起封线检查,告知被检查服务对象和时段,同过封线策略计算完成封线检查返回封线等级。
③划分技术领域:领域建模,步骤是 明确领域、抽象实体/值对象、提炼聚合、划定限界上下文。领域分为核心域、通用域、支撑域;是否将聚合独立为子域,主要看聚合是否还在其它领域提供服务。实体对应业务对象,有业务属性和业务行为;值对象是属性集合、无行为。聚合是由业务和逻辑紧密关联的实体和值对象组合而成的,是数据修改和持久化的基本单元,对外提供某些领域服务;聚合根是提供领域服务的入口对象。限界上下文用来封装上下文环境的通用语言,保证领域内的术语含义明确、没有歧义,限界就是领域的边界、上下文则是语义环境。
聚合设计是领域建模的核心内容,重点讲一下。聚合强调业务规则,明确边界、减少关联。聚合设计的内容,包括将业务术语翻译为数据ER,将业务逻辑抽象为服务、接口、参数和对象,确定对象和对象之间的关系,确定对外提供服务的对象、内部依赖的对象、外部依赖的对象,根据依赖关系进一步划分聚合边界等。聚合设计的原则,包括设计小聚合、封装业务不变性、通过唯一标识引用其他聚合、边界外使用最终一致性、通过应用层实现跨聚合服务调用等。
封线检查的领域模型,如下。封线检查对外提供策略创建、封线检查两个服务,这两个服务都围绕封线策略这个事项。策略创建由管理员实施;封线检查由使用方告知服务对象和时段,由封线策略服务根据该服务对象和时段匹配策略配置,计算封线等级。对外提供服务的不是策略配置、而是封线策略对象,封线策略对象 对内读写策略配置、进行逻辑计算,对外提供创建、检查两个领域服务 —— 封线策略对象就是聚合根,封线策略对象及其提供的服务就是封线聚合。使用方需要进行鉴权、判断他是否有权进行封线检查,封线策略对象通过RPC、调用权限域的鉴权服务(即所谓的事件),封线策略聚合加上权限策略聚合、对外提供了完整的封线检查服务。
④代码实现:代码模型映射,代码规范落地。参见代码规范章节。