DDD快速入门上手

一、What

领域驱动设计DDD(Domain Drive Design)是一种“新的”软件设计模式, 2003年由Eric Evans提出,用来解决复杂 业务系统可维护性扩展性以及可测试性无法得到保证的问题。

key value
是一种 软件设计模式(思想)
适用于 复杂、业务系统
用来解决 可维护性、拓展性、可测试性
分为 战略建模、战术落地

1.1、目标及简单解读

目标 解读
可维护性 当依赖变化时,有多少代码需要随之改变(一个应用最大的成本一般都不是来自于开发阶段,而是应用整个生命周期的总维护成本,所以代码的可维护性代表了最终成本)。
可拓展性 做新需求或改逻辑时,需要新增/修改多少代码。
可测试性 运行每个测试用例所花费的时间 * 每个需求所需要增加的测试用例数量。
战略建模、战术落地简介
分类 简介 常用方法、架构
战略建模 战略设计是指通过站在业务视⻆去分析问题,通过事件⻛暴去识别并建立起业务领域模型。根据领域实体间的业务关联形成聚合,并在各个聚合之间建立起边界。根据业务和语义边界,将一个或数个聚合分配在不同的限界上下文中。 事件⻛暴、 四色建模法
战术落地 战术设计是指站在技术的视⻆,关注领域模型的具体落地实现,设计出实体、值对象、聚合根、领域服务、应用服务化、资源库等代码与逻辑细节。 整洁架构、洋葱架构、六边形架构

1.2、关键概念

概念 concept 说明
实体 Entity 具备唯一ID,具 备业务逻辑,对 应现实世界业务 对象(比如:某 个人有唯一的身 份证号)
聚合根 Aggregate Root 聚合根属于实体对 象,聚合根具有全 局唯一ID,实体只 有在聚合根内部有 唯一ID,值对象没 有ID,值对象和实 体都属于某一个聚 合根
值对象 Value Object 不具有唯一ID,由对 象的属性,一般为内 存中的临时对象,可 以用来传递参数或对 实体进行补充描述 (比如:金额,由币 值和货币单位组成)
领域服务 Domain Service 为上层提供可操作的接口,负责对领域对象进行封装和调度,一般存在于需要调用基础设施的业务逻辑中(比如:支付服务,下单服务)。
工厂 Factory 主要用来创建聚合根,一般在复杂的聚合根构建时会用到。
仓储 Repository 封装基础设施提供查询和持久化聚合根操作。

CQRS全称Command Query Responsibility Segregation,即命令查询职责分离,顾名思义,将命令和查询分离。1

1.3、适用场景

设计方式 解读
面向过程编程(POP) 无边界,需求分解成方法(函数)
面向对象编程(OOP) 以对象为边界,需求分解成对象
领域驱动设计(DDD) 以问题域为边界,需求分解成问题域,再分解成对象

DDD是为解决复杂业务软件系统而诞生,与OOP最大的区别就是划分边界的方式不一样。

二、Why

2.1、常见代码样式

★参数校验、★数据读取存储、★业务计算、★调用外部服务、★发送消息等多种逻辑。导致如下情况:

1.可维护性差
  • 数据结构的不稳定性:AccountPO类是一个纯数据结构,映射了数据库中的一个表。这里的问题是数据库的表结构和设计是应 用的外部依赖,⻓远来看都有可能会改变,比如数据库要做Sharding,或者换一个表设计,或者改变字段名。
  • 依赖库的升级:AccountRepository依赖Jpa的实现,如果未来升级版本,可能会造成用法的不同bug。同样的,如果未来换一 个ORM体系,迁移成本也是巨大的。
  • 第三方服务依赖的不确定性:第三方服务,比如Yahoo的汇率服务未来很有可能会有变化:轻则API签名变化,重则服务不可 用需要寻找其他可替代的服务。在这些情况下改造和迁移成本都是巨大的。同时,外部依赖的限流、熔断等方案都需要随之改 变。
  • 第三方服务API的接口变化:YahooForexService.getExchangeRate返回的结果是小数点还是百分比?入参是(source, target)还是(target, source)?谁能保证未来接口不会改变?如果改变了,核心的金额计算逻辑必须跟着改,否则会造成资 损。
  • 中间件更换:今天我们用Kafka发消息,明天如果要上阿里云用RocketMQ该怎么办?后天如果消息的序列化方式从String改为 Binary该怎么办?如果需要消息分片该怎么改?
2.可拓展性差
  • 数据来源被固定、数据格式不兼容:原有的AccountPO是从本地获取的,而跨行转账的数据可能需要从一个第三方 服务获取,而服务之间数据格式不太可能是兼容的,导致从数据校验、数据读写、到异常处理、金额计算等逻辑都要 重写。
  • 业务逻辑无法复用:数据格式校验的问题会导致核心业务逻辑无法复用。每个用例都是特殊逻辑的后果是最终会造成 大量的if-else语句,而这种分支多的逻辑会让分析代码非常困难,容易错过边界情况,造成bug。
  • 逻辑和数据存储的相互依赖:当业务逻辑增加变得越来越复杂时,新加入的逻辑很有可能需要对数据库schema或消 息格式做变更。而变更了数据格式后会导致原有的其他逻辑需要一起跟着动。在最极端的场景下,一个新功能的增加 会导致所有原有功能的重构,成本巨大。
3.可测试性差
  • 设施搭建困难:当代码中强依赖了数据库、第三方服务、中间件等外部依赖之后,想要完整跑通一个测试用例需要确保所 有依赖都能跑起来,这个在项目早期是及其困难的。在项目后期也会由于各种系统的不稳定性而导致测试无法通过。
  • 运行耗时⻓:大多数的外部依赖调用都是I/O密集型,如跨网络调用、磁盘调用等,而这种I/O调用在测试时需要耗时很 久。另一个经常依赖的是笨重的框架如Spring,启动Spring容器通常需要很久。当一个测试用例需要花超过10秒钟才能跑 通时,绝大部分开发都不会很频繁的测试。
  • 耦合度高:假如一段脚本中有A、B、C三个子步骤,而每个步骤有N个可能的状态,当多个子步骤耦合度高时,为了完整 覆盖所有用例,最多需要有N * N * N个测试用例。当耦合的子步骤越多时,需要的测试用例呈指数级增⻓。

2.2、常见代码样式存在问题

针对常见代码样式存在问题的解读
  • 单一性原则(Single Responsibility Principle):单一性原则要求一个对象/类应该只有一个变更的原因。但是在这个案例里,代 码可能会因为任意一个外部依赖或计算逻辑的改变而改变。
  • 依赖反转原则(Dependency Inversion Principle):依赖反转原则要求在代码中依赖抽象,而不是具体的实现。在这个案例里 外部依赖都是具体的实现,比如YahooForexService对应的是依赖了Yahoo提供的具体服务。同样的KafkaTemplate、Jpa的DAO 实现都属于具体实现。
  • 开放封闭原则(Open Closed Principle):开放封闭原则指开放扩展,但是封闭修改。在这个案例里的金额计算属于可能会被修 改的代码,这个时候该逻辑应该需要被包装成为不可修改的计算类,新功能通过计算类的拓展实现。

三、How

3.1、重构步骤

步骤 内容
抽象数据存储层 避免了其他业务逻辑代码和数据库的直接耦合,避免了当数据库字段变化时,大量业务逻辑也跟着变的问题
抽象第三方服务 增加防腐层ACL:防止内部代码受外部依赖影响
抽象中间件 让业务代码不再依赖中间件的实现逻辑。
封装业务逻辑 通过Entity、Value Object和Domain Service封装所有的业务逻辑

3.2、DDD代码样式总结

DDD代码样式总结
  • 业务逻辑清晰,数据存储和业务逻辑完全分隔。
  • Entity、Value Object、Domain Service 都是独立的对象,没有任何外部依赖, 但是却包含了所有核心业务逻辑,可以单独完整测试。
  • 原有的TransferService不再包括任何计 算逻辑,仅仅作为组件编排,所有逻辑 均delegate到其他组件。这种仅包含 Orchestration(编排)的服务叫做 Application Service(应用服务)。

四、总结

DDD不是一个什么特殊的架构,而是任何传统代码经过合理的重构之后最终一定会抵达的终点。

  • 依赖分离提升可维护性、代码复用提升可拓展性、模块单一性原则提升可测试性。
  • POM moudle解决模块间依赖关系。

整洁架构2

六边形架构

三种微服务架构模型的对比和分析

0%