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(方法)分开;对对象的操作在服务层实现。
- 如图
-
「充血模型」
- 定义:将领域模型的原貌直接转换为程序设计。
- 操作方法在领域对象中实现,Service层(很薄)只负责调用。
- 如图
-
优劣比较
- 充血模型:保持了领域模型的原貌,可以直接映射成程序的变更,代码修改起来比较直接。
- 充血模型:保持了对象的封装性,使得领域模型在面临多态、继承等复杂结构是,易于变更。
- 充血模型:在理论上更优雅,但在实践上比较繁复。(图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
- DDD 的仓库和工厂设计介于业务领域层与基础设施层之间:接口在业务领域层,实现在基础设施层
- 图二:classic-ddd-dataflow
- 图三:classic-ddd-implement
-
基于经典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
- 基本原则:计算机程序是由单个能够起到子程序作用的单元或对象组合而成
- 完成软件工程的三个主要目标:复用性、灵活性和扩展性。
-
主要概念
-
组件
- 数据和功能一起在计算机程序运行时形成的单元;
- 是模块和结构化的基础。
-
抽象性
- 程序有能力忽略正在处理中信息的某些方面
-
封装
- 确保组件不会以不可预期的方式改变其它组件的内部状态;
- 只有在那些提供了内部状态改变方法的组件中,才可以访问其内部状态。
-
多态性
- 引用组件所产生的结果得依据实际调用的类型
-
继承性
- 允许在现存的组件基础上创建子类组件,这统一并增强了多态性和封装性。
-