Skip to content

DDD 之实战篇

遗留系统

遗留系统成因

  • 软件业务由简单到复杂的必然结果

解决遗留系统

  • 调整程序结构,解耦、拆分,再实现新功能,才能保证软件质量。

微服务问题

  • Martin Fowler 定义微服务:“小而专”。
  • 常常 Get " 小" 而忽略 “专”,导致微服务难于维护。
  • “专”:微服务由小团队独立维护,即让每次需求变更让某个小团队独立完成。
  • 原则:微服务内高内聚,微服务间低耦合。

引入 DDD

  • 微服务起源:系统规模越来越大,维护越来越困难;需要分而治之,即拆分微服务。
  • 微服务并非完全独立,需要组织协作;因此,DDD 是用来指导开发团队、组织微服务的实践方法。
  • DDD:阻止软件退化的钥匙

运用 DDD

  • 当系统业务变得复杂时,只有将对业务的理解绘制成 领域模型,才能正确指导软件开发。
  • 依据真实世界 -> 变更领域模型 -> 指导程序变更

Clean Architecture 启示

  • 适配器的意义:将 业务代码技术框架 解耦
  • 上层业务架构师:专注业务开发,降低技术门槛
  • 底层平台架构师:低成本完成架构不断演化,跟上市场和技术更迭

DDD 低成本维护与高质量设计

DDD 杜绝软件退化之利刃

  • 软件退化的根源:变更

    • 软件设计质量最高的版本,是第一次设计版本;后来需求变更,原有设计被打乱。
    • 变更,既是挑战,又是机会。
    • 比如最初版本 Case:客户信息 -> 计算金额 -> 支付
    • 当需求变更 Case:客户会员、金额折扣、支付方式
    • 接着变更 Case:秒杀、预定、闪购、团购、返券等
  • 为什么软件会退化?会随着变更而设计质量下降?

    • 软件的 本质 ,是对真实世界的模拟。
    • 软件 业务逻辑正确与否 的唯一标准:是否与真实世界一致。

      • 一致,则 OK;
      • 不一致,则会用户被提 BUG。
    • 软件的要做成什么样,既不由我们决定,也不由客户决定;而由客观世界决定。

      • 用户为什么总改需求?因为用户也不确定客观世界的规则;只有遇到问题,才能从经验判断。
    • 难以认识的真实世界

      • 对真实世界的最初认识:通常简单、清晰、易于理解的业务逻辑,变成第一次设计的版本。
      • 软件使用中,会产生不简单、不清晰、难以理解的业务逻辑,产生需求变更。
      • 软件业务会越来越接近真实世界、越专业;同时,业务逻辑越复杂、规模越大。
    • 简单结构与复杂逻辑

      • 保证软件设计质量,需要逐步调整程序结构:由简单程序结构转变为复杂程序结构。
      • 现实情况:在原有简单结构中增加代码,处理越来越复杂的业务逻辑;必然造成软件退化。
  • 软件变化的根源:不是变更

    • 软件需求变更只是 诱因
    • 杜绝软件退化:必须每次需求变更时,适当调整原有的程序结构。
  • 开放封闭原则 OCP

    • 开放原则:对功能扩展开发,即当系统需求变更时,可以对软件功能进行扩展,满足新需求。
    • 封闭原则:对代码修改封闭,即应当在不修改原有代码的基础上,实现新的功能。
  • 过度设计

    • 面向未来的变更做出过多的 “灵活设计”,结果期望的变更没发生。
    • “灵活设计” 成了摆设,还增加了程序复杂度。
    • 人月神话:第二次设计的版本通常会有很多过度设计。
  • 保持设计质量:领域驱动

    • 微服务对设计提出很高要求,「小而专、高内聚」,否则无法发挥微服务优势;需要 DDD 来指导设计。
    • 核心思想:真实是世界是怎么样,软件就怎么设计。
    • 真实世界与软件世界

      • 事物/行为/关系
      • 对象/方法/关联
      • 真实世界 -> 领域模型 -> 软件设计
    • DDD 思路:每次变更时,先回到领域模型,基于业务进行领域模型变更。

设计原则与变更

  • 单一职责原则

    • 描述:系统中的每个元素只完成自己职责范围内的事情,别的事情别人做,我只调用。
    • 《敏捷软件开发:原则、模式与实践》:一个职责就是软件变化的一个原因。
    • 什么是高质量代码?

      • 当用户提出一个变更需求,为了实现需求而软件修改的成本越低,软件设计质量就越高。
    • 如何保证每次需求变更时,只修改一个模块就能实现新需求?

      • 需要在维护软件的过程中不断整理代码:
      • 将因同一原因而变更的代码放在一起;
      • 将因不同原因而变更的代码分开放,放在不同模块、不同类中。
  • 思考 Case:“付款” 与 “折扣” 是否同时变更?

DDD 与数据库设计

  • 早期系统软件设计流程

    • 需求文稿 => 数据库设计 => 程序设计
    • 核心:数据库设计
  • 面向对象的系统软件设计过程

    • 需求文稿 => 用例设计 => 领域模型 => 程序设计 && 数据库设计
    • 核心:领域模型设计
  • 领域对象持久化?

    • insert: 创建领域对象
    • update: 根据 key 值修改相应的领域对象
    • delete: 摧毁领域对象
    • 思想:将暂时不使用的领域对象持久化到磁盘上。
  • 领域模型中继承关系与数据库设计?

    • 方案一:父类子类属性全部放在一张表中

      • 优点:简单,整个继承关系的数据全在表中
      • 缺点:造成 “表稀疏”
    • 方案二:每个子类都对应一张表,所有表共用一个主键;所有表都包含父类字段

      • 缺点:查询所有表中的某条记录时,效率低
    • 方案三:每个子类对应一张表;父类包含所有数据,并有字段标记属于哪个子类

  • NoSQL

    • 关系型数据库:遵循第三范式,使数据库大幅度降低冗余;但需频繁使用join操作,在高并发场景性能低。
    • NoSQL 数据库:将需要 join 的查询在入库前先 join,然后写入一张分布式 “宽表”。

领域模型指导程序设计

  • 落地概念:建立领域模型之区分服务、实体与值对象

    • 当用户在系统界面操作时,会向系统发出请求;“服务”接收请求,然后执行相应方法;所有操作完成后,在将实体或值对象中的数据持久化到数据库。
    • 服务:标识在领域对象之外的操作与行为,接收用户请求和执行某些操作。
    • 实体:通过一个唯一标识字段,来区分真实世界中的每一个个体。(领域对象)
    • 值对象:代表真实世界中那些一成不变的、本质性的事物。(领域对象)
    • 可变性是实体的特点,不变性是值对象的本质。
  • 设计思路:贫血模型 vs 充血模型

    • 「贫血模型」

      • Martin Fowler 提出 "贫血模型" 概念。所谓 “贫血模型”,就是在软件设计中,有很多的POJO对象,他们除了有一堆 get/set 方法,几乎没有任何业务逻辑。
      • 实体对象(属性)与 Service(方法)分开;对对象的操作在服务层实现。
      • 如图

      x

    • 「充血模型」

      • 定义:将领域模型的原貌直接转换为程序设计。
      • 操作方法在领域对象中实现,Service层(很薄)只负责调用。
      • 如图

      x

    • 优劣比较

      • 充血模型:保持了领域模型的原貌,可以直接映射成程序的变更,代码修改起来比较直接。
      • 充血模型:保持了对象的封装性,使得领域模型在面临多态、继承等复杂结构是,易于变更。
      • 充血模型:在理论上更优雅,但在实践上比较繁复。(图1 vs 图2)
      • 充血模型:需要开发人员具有更强的OOA/OOD能力、分析业务、业务建模与设计能力
      • 充血模型:需要有较强的团队协作能力
      • 贫血模型:所有业务处理过程都交给 Service 去完成;简单直接、易于理解。
    • 「充血模型」与「贫血模型」结合

      • 采用「贫血模型」:将复杂的业务处理场景,划分为多个相对独立的步骤,然后将这些独立的步骤分配给多个 Service 串联起来。
      • 采用「充血模型」:充血模型的方法和数据在领域对象生命周期内,一起创建,一起消亡。
      • 二者结合:将需要封装的业务逻辑放到领域对象中,按照充血模型去设计(高内聚);除此之外的其他业务逻辑放到 Service,按照贫血模型设计(松耦合)。
      • 需要封装的为充血模型的

        • 在领域模型中出现了类似继承、多态的情况
        • 类型或编码转换
        • 希望在软件中能更好的表现领域对象之间的关系
        • “聚合”:真实世界里代表「整体-部分」的事物

落地聚合、仓库与工厂

  • 聚合

    • 聚合:表达的是真实世界中「整体/部分」的关系。
    • 聚合根:外部访问的唯一入口。
    • 通过聚合的设计,可以真实的反映现实世界的状况,提高软件设计质量,有效降低日后变更的成本。
  • 仓库

    • 仓库(Repository):采用领域驱动设计,通常会实现一个仓库去完成对数据库的访问。
    • CASE:订单仓库 Order

      • customerId:用户ID
      • customer:用户对象
      • OrderItems:订单明细表
  • 工厂

    • 不同于设计模式中的工厂模式
    • 装载(Load):通过主键ID去查询某条记录

      • 1) 订单仓库查询订单时,只是简单的查询订单表
      • 2) 查询到该订单后,将其封装在订单对象中,通过查询补填用户对象、订单明细对象
      • 3) 通过补填后,会得到一个用户对象、多个订单明细对象,需要将它们装配到订单对象中。
    • 工厂的职责

      • 订单仓库将任务交给订单工厂,订单工厂分别调用订单DAO、订单明细DAO、用户DAO去查询
      • 将订单明细、用户信息 set 到订单对象相应属性上
      • 订单工厂将装配好的订单对象返回订单仓库
    • 通过仓库与工厂,对原有的DAO进行了一层封装,在保存、装载、查询等操作中,加入聚合、装配等操作,并将这些操作封装起来,对上层的客户程序屏蔽。

DDD 原理:从事件风暴到微服务拆分

如何开事件风暴会议

  • 如何破局需求分析的困境?统一建模语言。

    • 困境。研发:技术方案、设计细节;客户:领域知识、业务难题;造成沟通障碍。
    • 解决。主动了解客户语言,业务领域知识,用客户的语言与之沟通;主动让客户了解研发的语言,让客户清楚研发如何设计软件。
  • 什么是事件风暴(Event Storming)?

    • 定义:一种基于工作坊的DDD实践方法;可以快速发现领域中正在发生的事件,指导领域建模及程序开发。
    • 事件:事件即事实(Event as Fact),即在业务领域中那些已经发生的事件就是事实(Fact)。
    • 实施方法:让开发人员与领域专家一起开会,目的是与领域专家一起进行领域建模。(共创)
    • 步骤

      • 1)在产品经理的引导下,与业务专家开始梳理当前业务的「领域事件」。命名时采用 过去时态(注意: 领域事件是 已发生且需要保存的重要事实 )。
      • 2)针对每一个领域事件,项目组成员围绕它进行业务分析,增加各种命令与事件,进而思考与之相关的资源、外部系统与时间。
      • 3)识别模型中涉及的「聚合」及「聚合根」

限界上下文:冲破微服务设计困局

  • 问题域与限界上下文

    • 将整体系统分成许多相对独立的业务场景,在一个一个的业务场景中进行领域分析与建模,这样的业务场景称为 「问题子域」,简称「子域」。
    • 领域驱动设计的核心思想

      • 将对软件的分析与设计还原到真实世界中。
      • 真实世界的业务与问题叫做「问题域」,业务规则与知识叫「业务领域知识」。
    • 一个复杂系统的领域驱动设计,是以子域为中心进行领域建模,绘制出一张一张的领域设计模型,称之「限界上下文」。

  • 限界上下文与微服务

    • 单一职责原则与限界上下文

      • 每个限界上下文中实现的,都是软件变化同一个原因的业务。
    • 推荐每个限界上下文对应一个微服务。(也可以不这样)

    • 很好的将每次的需求变更快速落到某个微服务中变更
    • 实现低成本维护与快速交付,快速适应市场变化,提升企业竞争力。
    • 微服务之间低耦合 -> 上下文映射(Context Map)
  • 微服务拆分最佳实践

    • 从 DDD开始「需求分析」、「领域建模」,逐渐建立起多个「问题子域」
    • 将问题子域落实到「限界上下文」,他们之间的关联形成「上下文映射」
    • 各限界上下文落实到微服务中的「贫血模型」或「充血模型」设计,从而在微服务之间根据上下文映射形成「接口」
    • 拆分原则:“小而专”、“高内聚低耦合”。

微服务拆分原则

  • 问题:微服务按照什么原则拆分,如何拆分以及会面对哪些潜在风险。
  • 微服务拆分原则

    • “小而专” 要求即微服务内高内聚、微服务间低耦合
    • 微服务内高内聚:即单一职责原则,将代码修改的范围缩小到这个微服务内,独立修改,独立发布。
    • 微服务间低耦合:在微服务实现自身业务时,非自己职责的执行过程交给其他微服务去实现,你只通过接口调用。
    • 基于限界上下文进行拆分
  • 领域事件通知机制

    • 消息队列
    • 松耦合
  • 总结

    • 按照「限界上下文」进行 微服务 的拆分,按照「上下文地图」定义各微服务之间的 接口与调用关系
    • 将「领域模型」划分到多个「问题子域」,每个子域都有一个领域模型的设计。
    • 基于「充血模型」与「贫血模型」设计各个微服务的业务领域层,即各自的 Service 、 Entity 与 Value Object
    • 按照领域模型设计各个微服务的数据库

整洁架构:敏捷交付与架构演化

突破技术改造困局

  • 底层架构更迭

    • 过去理念:架构是软件系统中稳定不变的部分。
    • 现在指导思想:整洁架构之道,好的架构源于不停的演变。
  • 老技术升级成本高之根源:「业务代码」与「底层框架」的深度耦合

    • 解决思路:适配器理念,解耦上层业务代码与底层业务框架
    • 解决方法:通过分层将业务与技术解构

      • 业务领域层:前端表示层、MVC层、Bus层、Dao层
      • 解耦:值对象
      • 各技术架构:基础平台
    • 切记:“不要嫁给框架!”

支持快速交付的技术中台战略

  • 中台:将以往业务系统中可以复用的前台与后台代码剥离个性,提取共性,形成的共用组件。
  • 打造快速交付团队

    • 烟囱式的开发团队

      • 从需求到交付的整个过程需要多个部门的交互,大量时间耗费在沟通协调中;
      • 导致烟囱式的软件开发,大量重复代码,难以维护;
      • 统一软件发布制约了交付速度;
      • 对着规模增大,技术转型越来越困难。
    • 特性开发团队

      • 每个团队负责一个特性,直接面向客户,需求、UI、应用、数据库、运维都是一个团队完成。
      • 微服务架构,独立发布,独立交付和维护。
      • 优点:交付速度快;缺点:每人都是全栈,组建一个特性团队成本昂贵。
    • 最优解:底层架构团队

      • 通过技术选型,构建技术中台,将开发中的UI、应用、数据库、大数据等技术进行封装
      • 然后以API接口的方式开发给上层业务
      • 大前端+技术中台:开发的技术门槛降低,业务开发人员更加关注业务,快速应对市场变化。
  • 技术中台特性

    • 简单易用、快速便捷的技术中台
    • 易于架构演化
    • 支持领域驱动与微服务的技术架构
  • 案例:技术中台之CQRS模式

    • Martin Fowler 《企业应用架构模式》:将系统按照职责划分为命令(写操作)和查询(读操作)两部分。
    • 所有「命令操作」采用领域驱动设计思路;所有「查询」采用事务脚本(Transaction Script)模式,即SQL查询
    • 实现

      • 在增删改时,只需要编写前端界面、Service、值对象
      • 大多数情况下的查询,只用配置MyBatis就可以完成

DDD与技术中台设计

  • 经典DDD分层架构

    • 图一:classic-ddd

    x

    • DDD 的仓库和工厂设计介于业务领域层与基础设施层之间:接口在业务领域层,实现在基础设施层
    • 图二:classic-ddd-dataflow

    x

    • 图三:classic-ddd-implement

    x

  • 基于经典DDD架构的改造

    • 通用工厂和通用仓库的设计
    • 内置聚合功能
  • 支持微服务的技术中台设计

    • 先按照领域业务建模
    • 然后基于限界上下文拆分微服务

DDD 指导微服务落地

  • DDD 核心思想

    • 过去:软件就是客户怎么提需求,软件就怎么开发
    • DDD:主动理解业务,掌握业务领域知识
  • “小步快跑”设计思想

    • 对业务理解比较粗陋时,对主要流程领域建模
    • 不断往领域模型知识加内容,遵循原则重构模型,再加新功能
    • 最后完成所有功能
  • 领域建模,即深入理解业务

  • 架构师要求

    • 能够将业务转化为技术:超强业务落地能力,即将用户业务需求落地为技术方案。
    • 能合理用技术支持业务:具博学且大视野,能将业务痛点落地成合理/最优技术方案

事件溯源与 DDD

事件溯源设计思路

  • 事件,即已发生的重要事实。
  • 根据 「事件风暴」 中分析识别的领域事件,在每次完成相应的工作以后,增加一个对「领域事件」的发布。
  • 用户下单完成后,只需要实现一个 “用户下单” 的领域事件。

事件溯源实现

  • 基于消息的领域事件发布:Spring Cloud Stream 是一个实现消息驱动的技术框架,底层支持 RabbitMQ、Kafka 等主流消息队列。
  • 事件上游负责publish() 领域事件;
  • 事件下游负责实现各自的apply() 方法执行后续操作

DDD 实践总结

步骤一、基于业务的战略规划

  • 战略设计通过限界上下文,从全局视角规划整个系统的业务模块。

步骤二、基于限界上下文的领域建模

  • 逐步细化,对每个模块展开事件风暴会议,进行领域建模。
  • 各团队独立:按照业务独立性,将业务划分为不同模块,分配给各个团队独立开发;
  • 团队间协作:单一职责,每个团队各守其职。
  • 共同制定计划:事件风暴 & PI 计划会议

    • 各团队 独立 召开事件风暴会议,与敏捷开发相结合;
    • 通过会议,分析清楚自己的领域模型;
    • 通过主题域和支持域的分析,清楚哪些团队提供什么样的接口。

步骤三、基于领域的战术设计

  • 逐步落实到每个模块的微服务设计,数据库设计,以及分布式和云端部署等
  • 通过统一语言建模,用更专业的词汇来表述业务需求;分别形成 用例模型、用例描述和领域模型
  • 领域建模的方法

    • 事件风暴法
    • 四色分析法

      • 粉色:时间原型

        • 某个时间发生的某个重要事件,
        • 领域建模的核心。
      • 绿色:参与者-地点-物品原型

        • 与事件相关的人物、地点、物品
        • 简称 PPT(Party/Place/Thing)
      • 黄色:角色原型

        • 此事件中参与者的角色
      • 蓝色:PPT原型的补充说明

    • 需求讨论中建模法

      • 与业务专家在探讨业务时
      • 一边理解业务需求,一边绘制草图
    • 原文分析法

      • 在用例建模之后,在对每个用例进行描述的基础上
      • 将用例的事件流按照名词和动词逐一进行分析

实践总结

  • 背景前提:软件复杂性提升;人的大脑受困于自身局限,越来越难以应对,从而做出许多错误的设计决策;系统越来越难以维护,将团队置于巨大风险中。
  • DDD 是对核心复杂性的应对之道,“分而治之”
  • 1)通过业务建模将业务与技术分离,设计就变得清晰,易于做出正确设计
  • 2)通过限界上下文划分,使系统负责性下降,风险变得可掌控。
  • 3)通过与规模化敏捷的结合,使团队协作变得流畅。

DDD 再思考

DDD 核心思想

  • 就是要避免业务逻辑的复杂度与技术实现的复杂度混淆在一起;确定业务逻辑与技术实现的边界,从而隔离各自的复杂度,业务逻辑并不关心技术是如何实现的。
  • 无论采用何种技术,只要业务需求不变,业务规则就不会变化。
  • 理想状态下,应该保证 业务逻辑与技术实现是「正交」的

DDD 思想 vs 传统 OOA/OOD

  • DDD 是 OOA/OOD 思想的延续;
  • 同时,创新性的提出了领域建模思想,将业务分析与技术实现分离。

补充知识

  • OOA

    • 概念

      • Object-Oriented Analysis,与结构化分析相反。
    • 原则

      • 抽象

        • 定义:抽取共同的、本质性的特征,中舍弃个别的、非本质的特征。
        • 意义

          • 1)不需要了解和描述问题域中事物的一切,只需要分析其中与系统目标有关的事物及其本质性特征;
          • 2)舍弃个体事物细节上的差异,抽取其共同特征,形成一批事物的概念。
      • 封装

        • 把对象的属性和方法结合为一个不可分的系统单位,并尽可能隐蔽对象的内部细节。
      • 继承

        • 特殊类的对象拥有的一般类的全部属性与方法。
      • 分类

        • 具有相同属性和方法的对象划分为一类。
      • 聚合

        • 把一个复杂的事物看成若干比较简单的事物的组装体,从而简化对复杂事物的描述。
      • 关联

        • 通过一个事物联想到另外的事物。
      • 消息通信

        • 要求对象之间只能通过消息进行通信,而不允许在对象之外直接地存取对象内部的属性。
      • 行为分析

        • 控制自己的视野:考虑全局时,注意其大的组成部分,暂时不详察每一部分的具体的细节;考虑某部分的细节时,则暂时撇开其余的部分。
      • 粒度控制

        • 问题域中各种行为往往相互依赖、相互交织。
    • 基本步骤

      • 第一步,确定对象和类。
      • 第二步,确定结构(structure)
      • 第三步,确定主题(subject)。
      • 第四步,确定属性(attribute)。
      • 第五步,确定方法(method)。
  • OOD

    • 定义

      • “根据需求决定所需的类、类的操作以及类之间关联的过程”。
    • OOD是一种抽象的范式。

      • 一个类可以把大量的细节隐藏起来,只露出一个简单的接口,符合人类的抽象思维。
      • 这是一个伟大的概念,因为它给我们提供了封装和复用的基础,让我们可以从问题本身的角度来看问题,而不是从机器的角度来看问题。
    • 诞生背景

      • OOD和SD (Structured Design,结构化设计)的概念几乎同时诞生,分别以不同的方式来表现数据结构和算法。
      • NATO 会议采纳了Dijkstra的思想,整个软件产业都同意goto语句的确是有害的,结构化方法、瀑布模型从70年代开始大行其道。
      • 到90年代,OOD突然风靡了整个软件行业。
    • OOD 与 SD 本质差异

      • SD的问题

        • SD中模块被组织成一个树型结构,棵树的根就是主模块。
        • 顶端模块关心规模最大的问题,负责最重要的策略;底层模块只实现最小的细节。
        • 体系结构中越靠上,概念的抽象层次就越高,也越接近问题领域。
        • 但是,由于上方的模块需要调用下方的模块,所以这些上方的模块就依赖于下方的细节。 也就是说,当实现细节变化时,抽象也会受到影响。
      • OOD的精髓

        • 我们希望 倒转这种依赖关系:我们创建的抽象不依赖于任何细节,而细节则高度依赖于上面的抽象。
        • 依赖关系的倒转正是OOD和传统SD之间根本的差异,也正是OOD思想的精华所在。
  • OOP

    • 基本原则:计算机程序是由单个能够起到子程序作用的单元或对象组合而成
    • 完成软件工程的三个主要目标:复用性、灵活性和扩展性。
    • 主要概念

      • 组件

        • 数据和功能一起在计算机程序运行时形成的单元;
        • 是模块和结构化的基础。
      • 抽象性

        • 程序有能力忽略正在处理中信息的某些方面
      • 封装

        • 确保组件不会以不可预期的方式改变其它组件的内部状态;
        • 只有在那些提供了内部状态改变方法的组件中,才可以访问其内部状态。
      • 多态性

        • 引用组件所产生的结果得依据实际调用的类型
      • 继承性

        • 允许在现存的组件基础上创建子类组件,这统一并增强了多态性和封装性。