软件工程之领域驱动设计

要点简述

  • DDD适用于业务开发。DDD套路僵化,但能定义设计质量的底线,颇有文正公结硬寨打呆仗之妙
  • DDD核心思想是分治。将复杂的、大规模的业务系统,统一语言、划分边界、分而治之
  • DDD是动态演化过程。领域建模过程是不断猜想与反驳的过程,演化观点是建模过程的基本心智模式
  • DDD建模的关键步骤:明确战略,用户故事,事件风暴,寻找聚合,划分限界上下文,API设计,代码实现
  • DDD建模其它关键词:业务场景,边界划分,通用语言,领域对象、领域服务
  • DDD常见的设计理念:六边形架构,代码分层架构,依赖倒置、适配器、防腐层。经典的六边形架构,如下图

page.png

基本概念

  • DDD:Domain-Driven Design,领域驱动设计,概念有领域/子域、限界上下文、聚合、实体/值对象等
  • 领域:Domain,用于划分问题空间,是特定场景下的业务范围;领域之下可以有多个子域,将复杂性分⽽治之
    • 子域:子域是一个明确的专业领域、提供解决方案,分为核心、支撑、通用三类
    • 核心域:决定产品和公司的核心竞争力,直接对业务产生价值
    • 支撑域:完成业务的必要能力,专注于业务的某个方面、不是业务成功的主因
    • 通用域:被整个业务使用、但不核心,可以外购,如权限、登陆
  • 限界上下文:Bounded Context,用于划分解决方案空间,通过限界上下⽂、来明确聚合(或领域)的范围和职责
    • 职责:限界上下文用来明确聚合(或领域)的范围和职责,定义了该领域内部的通用语言、模型和规则
    • 原则:解决相同问题的聚合(或领域)应该被放到同一个限界上下文;如果⼀个聚合同时解决多个问题,则需要对聚合进⾏拆分,拆分后的聚合应该被划分到不同限界上下⽂中
    • 形式:子域和限界上下文实现时通常是1对1的关系,但也能多对多
    • page.png
  • 聚合:Aggregate,多个实体和值对象组成的业务规则叫聚合;聚合要封装业务的不变性,并明确边界、减少关联、做到高内聚低耦合,指导微服务实现
    • 过程:找出模型(领域事件、业务名词、事件合并),内聚成聚合(角色、命令、事件)
    • 聚合根:聚合里面一定有一个实体是聚合根;聚合根作用是保证内部的实体的一致性,外部操作只需要对阵聚合根
  • 实体:Entity,实体是对真实业务形态的抽象,实体有唯一标识、有生命周期、且具有延续性
    • 业务:实体能够反映业务的真实形态,是多个属性、操作或行为的载体
    • 代码:实体的代码有属性、行为(充血模型),行为代表了大部分业务逻辑;只有属性、没有行为的成为贫血模型
    • 运行:实体有唯一不变的ID,属性可修改
  • 值对象:值对象主要用于描述实体特征,是一些列属性的集合。值对象没有唯一ID、没有生命周期、不可修改
  • 领域服务:DomainService
  • 事件风暴:Event Storming,从琐碎到聚化的业务领域建模过程
    • 参与方:业务专家、产品经理、架构师、开发/测试
    • 关键点:业务的实体、命令、事件,实体执行命令后产生事件
    • 领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计

建模过程

DDD建模分为几个步骤,包括:产品场景、用户故事、事件风暴、寻找聚合、划分限界上下文、设计API、代码实现等。以电商场景为例,DDD建模过程如下图,

page.png page.png page.png

代码实现,参见代码规范章节。

数据对象

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。

page.png

代码规范

以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架构:六边形架构,如下图
  • page.png
  • 分层架构:模块代码实现分为四层,自上而下分别是用户接口层、应用层、领域层、基础层。如下图,
  • page.png
  • 适配器:以领域服务为中心,向上提供API、向下约定SPI;表现上,通过适配器和外部交互,将应用服务&领域服务封装在系统内部、和外部解耦
  • 依赖倒置:即SPI;由领域层依赖基础层,倒置成基础层依赖领域层;这样,其它层都依赖领域层、领域层不依赖其它层,领域层最终只受限于业务逻辑
  • 充血模型:将业务逻辑封装在领域对象内,不仅仅是简单的数据容器、还包含了与业务相关的行为规则
    • 表现形式:代码高内聚,属性对外不可见、只能通过方法访问和变更等
    • 贫血模型:与充血模型相对应的是贫血模型,它将业务逻辑放在外部服务或管理类中、领域对象仅仅是简单的数据结构


以下为思考过程,内容待整理。

建模过程

DDD建模分为明确产品战略、梳理业务场景、划分技术领域、代码实现四个阶段,如下图。接下来以运维封线检查为例,介绍DDD建模的流程详情。

page.png

明确产品战略:确定产品目标,明确用户、价值、功能等宏观事项。 封线检查是变更管控的核心策略之一,功能是限制所有变更的时间窗口,价值是降低变更带来的质量风险,用户是公司所有员工、特别是RD。封线检查的难点是,定义一个通用策略、使能适用于所有变更系统。

梳理业务场景:事件风暴,各个角色坐在一起头脑风暴,全面还原业务场景、精细梳理业务需求、建立领域统一语言、得出一个业务和研发人员都能看懂的事件风暴图。 事件风暴图类似有限状态机FSM,由业务流程、命令、事件三个行组成。

  • 业务流程是事件风暴的核心,是业务逻辑过程;
  • 命令是业务上的输入,代表了给外界使用的功能,如提交客户订单、锁定账号。命令可以是用户UI操作、外部系统触发、内部定时任务等
  • 事件是业务上的输出,是领域专家关心的、在业务上真实发生的事,如,客户订单已提交、账号被锁定(3次密码错误)。事件通过回溯历史的方式、明确业务流程中的状态变化,有主动、被动之分。

封线检查的事件风暴图,如下。通过业务讨论,封线检查域的业务流程包括封线策略的创建(含鉴权)、使用两个步骤。封线策略创建由管理员发起、属性包括服务对象、时段、等级,策略创建完成即可生效使用。使用方发起封线检查,告知被检查服务对象和时段,同过封线策略计算完成封线检查返回封线等级。

page.png

划分技术领域:领域建模,步骤是 明确领域、抽象实体/值对象、提炼聚合、划定限界上下文。领域分为核心域、通用域、支撑域;是否将聚合独立为子域,主要看聚合是否还在其它领域提供服务。实体对应业务对象,有业务属性和业务行为;值对象是属性集合、无行为。聚合是由业务和逻辑紧密关联的实体和值对象组合而成的,是数据修改和持久化的基本单元,对外提供某些领域服务;聚合根是提供领域服务的入口对象。限界上下文用来封装上下文环境的通用语言,保证领域内的术语含义明确、没有歧义,限界就是领域的边界、上下文则是语义环境。

聚合设计是领域建模的核心内容,重点讲一下。聚合强调业务规则,明确边界、减少关联。聚合设计的内容,包括将业务术语翻译为数据ER,将业务逻辑抽象为服务、接口、参数和对象,确定对象和对象之间的关系,确定对外提供服务的对象、内部依赖的对象、外部依赖的对象,根据依赖关系进一步划分聚合边界等。聚合设计的原则,包括设计小聚合、封装业务不变性、通过唯一标识引用其他聚合、边界外使用最终一致性、通过应用层实现跨聚合服务调用等。

封线检查的领域模型,如下。封线检查对外提供策略创建、封线检查两个服务,这两个服务都围绕封线策略这个事项。策略创建由管理员实施;封线检查由使用方告知服务对象和时段,由封线策略服务根据该服务对象和时段匹配策略配置,计算封线等级。对外提供服务的不是策略配置、而是封线策略对象,封线策略对象 对内读写策略配置、进行逻辑计算,对外提供创建、检查两个领域服务 —— 封线策略对象就是聚合根,封线策略对象及其提供的服务就是封线聚合。使用方需要进行鉴权、判断他是否有权进行封线检查,封线策略对象通过RPC、调用权限域的鉴权服务(即所谓的事件),封线策略聚合加上权限策略聚合、对外提供了完整的封线检查服务。

page.png

代码实现:代码模型映射,代码规范落地。参见代码规范章节。



Prev     Next