关于后端:一文讲清DDD的好处与案例实践

3次阅读

共计 8874 个字符,预计需要花费 23 分钟才能阅读完成。

为什么须要 DDD

  • 简单零碎设计:零碎多,业务逻辑简单,概念不清晰,有什么适合的办法帮忙咱们理分明边界,逻辑和概念
  • 多团队协同:边界不清晰,零碎依赖简单,语言不对立导致沟通和了解艰难。有没有一种形式把业务和技术概念对立,大家用一种语言沟通。例如:航程是大家所了解的航程吗?
  • 设计与实现一致性:PRD,具体设计和代码实现天差万别。有什么办法能够把业务需要疾速转换为设计,同时还要放弃设计与代码的一致性?
  • 架构对立,可复用资产和扩展性:以后取决于开发的同学具备很好的形象能力和高编程的技能。有什么好的办法领导咱们做形象和实现。

DDD 的价值

  • 边界清晰的设计办法:通过畛域划分,辨认哪些需要应该在哪些畛域,一直拉齐团队对需要的认知,分而治之,控制规模。
  • 对立语言:团队在有边界的上下文中无意识地造成对事物进行对立的形容,造成对立的概念(模型)。
  • 业务畛域的常识积淀:通过重复论证和提炼模型,使得模型必须与业务的真实世界保持一致。促使常识 (模型) 能够很好地传递和保护。
  • 面向业务建模:畛域模型与数据模型拆散,业务复杂度和技术复杂度拆散。

DDD 架构

分层架构

  • 用户接口层:调用应用层实现具体用户申请。蕴含:controller,近程调用服务等
  • 应用层 App:尽量简略,不蕴含业务规定,而只为了下一层中的畛域对象做协调工作,调配工作,重点对畛域层做编排实现简单业务场景。蕴含:AppService,音讯解决等
  • 畛域层 Domain:负责表白业务概念和业务逻辑,畛域层是零碎的外围。蕴含:模型,值对象,域服务,事件
  • 根底层:对所有下层提供技术能力,包含:数据操作,发送音讯,生产音讯,缓存等
  • 调用关系:用户接口层 -> 应用层 -> 畛域层 -> 根底层
  • 依赖关系:用户接口层 -> 应用层 -> 畛域层 -> 根底层

六边形架构

  • 六边形架构:零碎通过适配器的形式与内部交互,将应用服务于畛域服务封装在零碎外部
  • 分层架构:它仍然是分层架构,它外围扭转的是依赖关系。
  • 畛域层依赖倒置:畛域层依赖根底层倒置成根底层依赖畛域层,这个简略的变动使得畛域层不依赖工作层,其余层都依赖畛域层,使得畛域层只表白业务逻辑且稳固。

调用链路

DDD 的基本概念

畛域模型

  • 畛域(策略):业务范围,范畴就是边界。
  • 子畛域:畛域可大可小,咱们将一个畛域进行拆解造成子畛域,子畛域还能够进行拆解。当一个畛域太大的时候须要进行细化拆解。
  • 模型(战术):基于某个业务畛域辨认出这个业务畛域的聚合,聚合根,界线上下文,实体,值对象。

1 外围域

决定产品和公司外围竞争力的子域是外围域,它是业务胜利的次要因素和公司的外围竞争力。间接对业务产生价值。

2 通用域

没有太多个性化的诉求,同时被多个子域应用的通用性能子域是通用域。例如,权限,登陆等等。间接对业务产生价值。

3 撑持域

撑持其余畛域业务,具备企业个性,但不具备通用性。间接对业务产生价值。

为什么要划分外围域、通用域和撑持域

一个业务肯定有他最重要的局部,在日常做业务判断和需要优先级判断的时候能够基于这个划分来做决策。例如:一个交易相干的需要和一个配置相干的需要排优先级,很显著交易是外围域,规定是反对域。同样咱们认为是撑持域或者通用域的在其余公司可能是外围域,例如权限对于咱们来说是通用域,然而对于业余做权限零碎的公司,这个是外围域。

限界上下文(策略)

业务的边界的划分,这个边界能够是一个畛域或者多个畛域的汇合。简单业务须要多个域编排实现一个简单业务流程。限界上下文能够作为微服务划分的办法。其本质还是高内聚低耦合,只是限界上下文只是站在更高的层面来进行划分。如何进行划分,我的办法是一个界线上下文必须反对一个残缺的业务流程,保障这个业务流程所波及的畛域都在一个限界上下文中。

实体(ENTITY)

定义:实体有惟一的标识,有生命周期且具备延续性。例如一个交易订单,从创立订单咱们会给他一个订单编号并且是惟一的这就是实体惟一标识。同时订单实体会从创立,领取,发货等过程最终走到终态这就是实体的生命周期。订单实体在这个过程中属性产生了变动,但订单还是那个订单,不会因为属性的变动而变动,这就是实体的延续性。

实体的业务状态:实体可能反映业务的实在状态,实体是从用例提取进去的。畛域模型中的实体是多个属性、操作或行为的载体。

实体的代码状态:咱们要保障实体代码状态与业务状态的一致性。那么实体的代码应该也有属性和行为,也就是咱们说的充血模型,但理论状况下咱们应用的是贫血模型。贫血模型毛病是业务逻辑扩散,更像数据库模型,充血模型可能反映业务,但过重依赖数据库操作,而且简单场景下须要编排畛域服务,会导致事务过长,影响性能。所以咱们应用充血模型,但行为外面只波及业务逻辑的内存操作。

实体的运行状态:实体有惟一 ID,当咱们在流程中对实体属性进行批改,但 ID 不会变,实体还是那个实体。

实体的数据库状态:实体在映射数据库模型时,个别是一对一,也有一对多的状况。

值对象(VALUEOBJECT)

定义:通过对象属性值来辨认的对象,它将多个相干属性组合为一个概念整体。在 DDD 中用来形容畛域的特定方面,并且是一个没有标识符的对象,叫作值对象。值对象没有惟一标识,没有生命周期,不可批改,当值对象产生扭转时只能替换(例如 String 的实现)

值对象的业务状态:值对象是形容实体的特色,大多数状况一个实体有很多属性,个别都是平铺,这些数据进行分类和聚合后可能表白一个业务含意,不便沟通而不关注细节。

值对象的代码状态:实体的繁多属性是值对象,例如:字符串,整型,枚举。多个属性的汇合也是值对象,这个时候咱们把这个汇合设计为一个 CLASS,但没有 ID。例如商品实体下的航段就是一个值对象。航段是形容商品的特色,航段不须要 ID,能够间接整体替换。商品为什么是一个实体,而不是形容订单特色,因为须要表白谁买了什么商品,所以咱们须要晓得哪一个商品,因而须要 ID 来标识唯一性。

咱们看一下上面这段代码,person 这个实体有若干个繁多属性的值对象,比方 Id、name 等属性;同时它也蕴含多个属性的值对象,比方地址 address。

值对象的运行状态:值对象创立后就不容许批改了,只能用另外一个值对象来整体替换。当咱们批改地址时,从页面传入一个新的地址对象替换调用 person 对象的地址即可。如果咱们把 address 设计成实体,必然存在 ID,那么咱们须要从页面传入的地址对象的 ID 与 person 外面的地址对像的 ID 进行比拟,如果雷同就更新,如果不同先删除数据库在新增数据。

值对象的数据库状态:有两种形式嵌入式和序列化大对象。

案例 1:以属性嵌入的形式造成的人员实体对象,地址值对象间接以属性值嵌入人员实体中。

当咱们只有一个地址的时候应用嵌入式比拟好,如果多个地址必须有序列化大对象,同时能够反对搜寻。

案例 2:以序列化大对象的形式造成的人员实体对象,地址值对象被序列化成大对象 Json 串后,嵌入人员实体中。

反对多个地址存储,不反对搜寻。

值对象的劣势和局限:

  1. 简化数据库设计,晋升数据库操作的性能(多表新增和批改,关联表查问)。
  2. 尽管简化数据库设计,然而畛域模型还是能够表白业务。
  3. 序列化的形式会使搜寻实现艰难(通过搜索引擎能够解决)。

聚合和聚合根

多个实体和值对象组成的咱们叫聚合,聚合的外部肯定的高内聚。这个聚合外面肯定有一个实体是聚合根。

聚合与畛域的关系:聚合也是范畴的划分,畛域也是范畴的划分。畛域与聚合能够是一对一,也能够是一对多的关系

聚合根的作用是保障外部的实体的一致性,对外只须要对聚合根进行操作。

限界上下文,域,聚合,实体,值对象的关系

畛域蕴含限界上下文,限界上下文蕴含子域,子域蕴含聚合,聚合蕴含实体和值对象

事件风暴

参与者

除了领域专家,事件风暴的其余参与者能够是 DDD 专家、架构师、产品经理、项目经理、开发人员和测试人员等我的项目团队成员

事件风暴筹备的资料

一块白板和一支笔。

事件风暴的关注点

在领域建模的过程中,咱们须要重点关注这类业务的语言和行为。比方某些业务动作或行为(事件)是否会触发下一个业务动作,这个动作(事件)的输出和输入是什么?是谁(实体)收回的什么动作(命令),触发了这个动作(事件)…咱们能够从这些暗藏的词汇中,剖析出畛域模型中的事件、命令和实体等畛域对象。

实体执行命令产生事件。

业务场景的剖析

通过业务场景和用例找出实体,命令,事件。

领域建模

领域建模时,咱们会依据场景剖析过程中产生的畛域对象,比方命令、事件等之间关系,找出产生命令的实体,剖析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建设畛域模型以及模型之间的依赖。畛域模型利用限界上下文向上能够领导微服务设计,通过聚合向下能够领导聚合根、实体和值对象的设计。

如何建模

  • 用例场景梳理:就是一句话需要,但咱们须要把一些含糊的概念通过对话的形式逐渐失去明确的需要,在加以提炼和形象。
  • 建模方法论:词法剖析(找名词和动词),畛域边界
  • 模型验证

协同单自动化分单案例

领域建模

需要:咱们须要把零碎自动化失败转人工订单主动调配给小二,防止人工挑单和抢单,通过主动调配晋升整体履约解决效率。

  • 产品小 A:把需要读了一遍 …….。
  • 开发小 B:那就是将履约单调配给个小二对吧?
  • 产品小 A:不对,咱们还须要依据一个规定主动分单,例如退票订单分给退票的小二
  • 开发小 B:恩,那咱们能够做一个分单规定治理。例如:新增一个退票分单规定,在外面增加一批小二工号。履约单基于本身属性去匹配分单规定并找到一个规定,而后从分单规定外面抉择一个小二工号,履约单写入小二工号即可。
  • 产品小 A:分单规定还须要有优先级,其中小二如果下班了才调配,如果上班了就不调配。
  • 开发小 B:优先级没有问题,在匹配分单规定办法外面依照优先级排序即可,不影响模型。而小二就不是简略一个工号保护在分单规定中,小二有状态了。
  • 产品小 A:分单规定外面增加小二操作太麻烦了,例如:每次新增一个规定都要去挑人,人也不肯定记得住,理论客服在治理小二的时候是依照技能组治理的。
  • 开发小 B:恩,懂了,那就是通过新增一个技能组治理模块来治理小二。而后在通过分单规定来配置 1 个技能组即可。获取一个小二工号就在技能组外面了。
  • 开发小 B:总感觉不对,因为新增一个自动化分单需要,履约单就依赖了分单规定,履约单应该是一个独立的域,分单不是履约的能力,履约单理论只须要晓得解决人是谁,至于怎么调配的他不太关怀。应该由分单规定基于履约单属性找匹配一个规定,而后基于这个规定找到一个小二。履约单与分单逻辑解耦。
  • 产品小 A:分单要轮流调配或者能者多劳调配,小二之前解决过的订单和航司优先调配。
  • 开发小 B:获取小二的逻辑越来越简单了,理论技能组才是找小二的外围,分单规定外围是通过履约单特色失去一个规定后果(技能组 ID,分单策略,特色规定)。技能组基于分单规定的后果取得小二工号。
  • 产品小 A:还漏了一个信息,就是履约单会被屡次调配的状况,每一个履约环节都可能转人工,客服须要晓得履约单被解决屡次的状况
  • 开发小 B:那用履约单无奈表白了,咱们须要新增一个概念叫协同单,协同单是为了协同履约单,通过协同推动履约单的进度。
  • 产品小 A:协同单概念很好,小二上班后,如果没有解决完,还能够转交给他人。
  • 开发小 B:恩,那只须要在协同单上减少行为即可。

畛域划分

沟通的过程就是推导和验证模型的过程,最初进行域的划分:

场景梳理

穷举所有场景,从新验证模型是否能够笼罩所有场景。

场景名称 场景动作 域服务 聚合根 办法
创立协同单 1、判断关联业务单是否非法 协同单 创立协同单 1、问题分类是否符合条件(例如: 商家用户发动自营 -> 商家的协同单)2、save 协同单 创立协同单
调配协同单 协同单 ID 调配协同单到人.1、判断协同单状态(= 待处理)2、记录操作日志 3、save 协同单 调配协同单 协同单 调配协同单
受理协同单 协同单 ID 解决协同单 协同单 受理协同单 1. 判断订单状态(= 待处理 / 验收失败)2. 更改订单状态(待处理 / 验收失败 -> 解决中)3. 记录操作日志 4.save 协同单 受理协同单
转交协同单 协同单 ID 转交协同单 协同单 转交协同单 1. 判断订单状态.(= 解决中、待处理)2. 校验转交的人是否在正确的组织上面 3. 更改协同人值对象(同一组织下的不同人,从坐席治理域中取)4. 记录操作日志 5.save 协同单 转交协同单
敞开协同单 协同单 ID 敞开协同单 协同单 敞开协同单 1. 判断订单状态(= 解决中、待处理)2. 更改订单状态(敞开)3. 记录操作日志 4.save 协同单 敞开协同单
解决协同单 协同单 ID 解决协同单 协同单 解决协同单 1. 判断订单状态(= 解决中)2. 更改订单状态(解决中 -> 待验收)3. 记录操作日志 4.save 协同单 解决协同单
驳回协同单 协同单 ID 驳回协同单 协同单 驳回协同单 1. 判断订单状态(= 待验收)2. 更改订单状态(待验收 -> 解决中)3. 记录操作日志 4.save 协同单 驳回协同单
完结协同单 协同单 ID 完结协同单 协同单 完结协同单 1. 判断订单状态(= 待验收)2. 更改订单状态(待验收 -> 已完结)3. 记录操作日志 4.save 协同单 完结协同单
回绝协同单 协同单 ID 回绝协同单 协同单 回绝协同单 1. 判断订单状态(= 解决中、待处理)2. 更改订单状态(已回绝)3. 记录操作日志 4.save 协同单 回绝协同单
催单 协同单 ID 催单 协同单 催单 1. 判断订单状态(= 解决中、待处理)2、批改催单值对象 3、记录操作日志 4、save 协同单 催单

DDD 标准

每一层都定义了相应的接口次要目标是标准代码:

  • application:CRQS 模式,ApplicationCmdService 是 command,ApplicationQueryService 是 query
  • service:是畛域服务标准,其中定义了 DomainService,利用零碎须要继承它。
  • model:是聚合根,实体,值对象的标准。

    • Aggregate 和 BaseAggregate:聚合根定义
    • Entity 和 BaseEntity:实体定义
    • Value 和 BaseValue:值对象定义
    • Param 和 BaseParam:畛域层参数定义,用作域服务,聚合根和实体的办法参数
    • Lazy:形容聚合根属性是提早加载属性,相似与 hibernate。
    • Field:实体属性,用来实现 update-tracing
/**
 * 实体属性,update-tracing
 * @param <T>
 */
public final class Field<T> implements Changeable {
    private boolean changed = false;
    private T value;
    private Field(T value){this.value = value;}
    public void setValue(T value){if(!equalsValue(value)){this.changed = true;}
        this.value = value;
    }
    @Override
    public boolean isChanged() {return changed;}
    public T getValue() {return value;}
    public boolean equalsValue(T value){if(this.value == null && value == null){return true;}
        if(this.value == null){return false;}
        if(value == null){return false;}
        return this.value.equals(value);
    }
    public static <T> Field<T> build(T value){return new Field<T>(value);
    }
}
  • repository

    • Repository:仓库定义
    • AggregateRepository:聚合根仓库, 定义聚合根罕用的存储和查询方法
  • event:事件处理
  • exception:定义了不同层用的异样

    • AggregateException:聚合根外面抛的异样
    • RepositoryException:根底层抛的异样
    • EventProcessException:事件处理抛的

工程构造

application 模块

  • CRQS 模式:commad 和 query 拆散。
  • 重点做跨域的编排工作,无业务逻辑。

domain 模块

  • 域服务, 聚合根,值对象,畛域参数,仓库定义

infrastructurre 模块

所有技术代码在这一层。mybatis,redis,mq,job,opensearch 代码都在这里实现,domain 通过依赖倒置不依赖这些技术代码和 JAR。

client 模块

对外提供服务

model 模块

内外都要用的共享对象

代码示例

application 示例

public interface CaseAppFacade extends ApplicationCmdService {
    /**
     * 接手协同单
     * @param handleCaseDto
     * @return
     */
    ResultDO<Void> handle(HandleCaseDto handleCaseDto);

}
public class CaseAppImpl implements CaseAppFacade {
    @Resource
    private CaseService caseService;// 域服务
    @Resource
    CaseAssembler caseAssembler;//DTO 转 Param
    @Override
    public ResultDO<Void> handle(HandleCaseDto handleCaseDto) {
        try {ResultDO<Void> resultDO = caseService.handle(caseAssembler.from(handleCaseDto));
            if (resultDO.isSuccess()) {pushMsg(handleCaseDto.getId());
                return ResultDO.buildSuccessResult(null);
            }
            return ResultDO.buildFailResult(resultDO.getMsg());
        } catch (Exception e) {return ResultDO.buildFailResult(e.getMessage());
        }
    }
}
  • mapstruct:VO,DTO,PARAM,DO,PO 转换十分不便,代码量大大减少。
  • CaseAppImpl.handle 调用域服务 caseService.handle。

domainService 示例

public interface CaseService extends DomainService {
    /**
     * 接手协同单
     *
     * @param handleParam
     * @return
     */
    ResultDO<Void> handle(HandleParam handleParam);
    
}
public class CaseServiceImpl implements CaseService {
    @Resource
  private CoordinationRepository coordinationRepository;

    @Override
    public ResultDO<Void> handle(HandleParam handleParam) {
        SyncLock lock = null;
        try {lock = coordinationRepository.syncLock(handleParam.getId().toString());
            if (null == lock) {return ResultDO.buildFailResult("协同单 handle 加锁失败");
            }
            CaseAggregate caseAggregate = coordinationRepository.query(handleParam.getId());
            caseAggregate.handle(handleParam.getFollowerValue());
            coordinationRepository.save(caseAggregate);
            return ResultDO.buildSuccessResult(null);
        } catch (RepositoryException | AggregateException e) {String msg = LOG.error4Tracer(OpLogConstant.traceId(handleParam.getId()), e, "协同单 handle 异样");
            return ResultDO.buildFailResult(msg);
        } finally {if (null != lock) {coordinationRepository.unlock(lock);
            }
        }
    }
}
  • 畛域层不依赖根底层的实现:coordinationRepository 只是接口,在畛域层定义好,由根底层依赖畛域层实现这个接口。
  • 业务逻辑和技术解耦:域服务这层通过调用 coordinationRepository 和聚合根将业务逻辑和技术解耦。
  • 聚合根的办法无副作用:聚合根的办法只对聚合根外部实体属性的扭转,不做长久化动作,可重复测试。
  • 模型与数据拆散:

    • 扭转模型:caseAggregate.handle(handleParam.getFollowerValue())。
    • 扭转数据:coordinationRepository.save(caseAggregate);事务是在 save 办法上。
  • 充血模型 VS 贫血模型:

    • 充血模型:表达能力强,代码高内聚,畛域内关闭,聚合根内部结构对外不可见,通过聚合根的办法拜访,适宜简单企业业务逻辑。
    • 贫血模型:业务简单之后,逻辑散落到大量办法中。
  • 标准大于技巧:DDD 架构能够防止引入一些其余概念,零碎只有域,域服务,聚合根,实体,值对象,事件来构建零碎。

    总结

    • 好的模型,能够积淀为资产,不好的模型,会逐步成为负债。
    • 性能是表象,模型才是外在。
    • 演变观点是建模过程的根本心智模式。
    • 建模过程是一直猜测与反驳的过程。

本文由 mdnice 多平台公布

正文完
 0