分布式应用服务的拆分
思路:将业务专家头脑中的业务信息转化成领域模型,再由领域模型落地为架构设计。
领域驱动设计是什么?
领域驱动设计为什么能帮助拆分应用?
领域驱动设计怎么帮助拆分应用?
起因与概念
业务需求如何落地成代码?
传统模式下,甲方的业务专家与乙方的业务分析师讨论确定需求文档,架构师根据需求文档进行架构,程序员根据架构要求实现功能。
需求改变时,需要先修改需求文档,再按需微调架构,然后编写代码——以功能为导向的设计方式。
业务专家的需求通过需求文档和架构文档才能落地,业务专家的信息流入程序员手里时损耗严重。
需要一个通用的描述方式使得需求分析团队和技术团队能够共同描述需求——领域驱动设计(Domain-Driven Design, DDD)。
DDD核心:通过领域驱动设计提供的方法论定义领域模型,从而确定业务和应用的边界,保证业务与代码的一致性。
业务和代码的一致性:业务专家与技术团队能站在同一个层面,对同一个事物具有相同的理解。
拆分思路
甲方的业务专家十分了解业务需求,但是不懂具体实现,乙方的技术团队刚好相反。
因此需要一门通用的描述语言使得业务专家和技术团队能够站在同一个层面看待事物。
DDD拆分应用的思路:分析业务需求,形成领域知识,抽取领域模型,搭建软件架构,分层编写代码。
领域知识是对业务的一般性描述:实体、流程、命令、事件、……
领域模型:领域、子域、领域对象(聚合、聚合根、实体、值对象、……)、领域事件、……
软件架构分层:用户接口层、应用层、领域层、基础层
从业务需求触发,分析出领域知识,抽取出领域模型,构建出应用架构。
问题:
领域模型的基本概念
需求-知识-模型的形成过程
领域模型如何落地成代码架构
模型结构
通用语言
核心作用是向业务专家和技术团队传达一致的信息,方式可以是自然语言、文档、图表等。
在一定范围内(限界上下文),业务专家和技术团队使用相同的名词(通用术语)描述同一个事物,而不是使用自己专业内的名词,因为对方可能出现理解偏差。例如电商系统中的物品,在交易系统中叫“商品”,在出库系统中叫做“库存”,在物流系统中叫做“货物”,它们实际上都是指的一个东西,但是出现在了不同的上下文中。
通用术语可以直接在代码中用于实体、命令和事件的命令,推荐这么做,保持一致性
使用通用语言定义业务过程,并且代码的实现逻辑应该与该过程一致(需要代码审查)。代码的实现往往有多种方式,应该选择与过程描述一致的方式。
领域、子域和限界上下文
所谓的“业务边界”实际上是一个“业务范围”,例如电商平台作为一个综合性平台,本身就是一个领域。
业务边界理解成边界围起来的范围更好理解,领域是指架构需要实现的业务范围。
领域可以继续被细分,形成子领域(子域),一般我们只分一次,子域内部使用通用语言描述同一个事物,不存在二义性。
子域按照重要性分为:
核心域:包含核心业务,例如商品系统和订单系统
支撑域:支持核心业务,例如库存系统
通用域:技术层面具有通用性,例如消息通知、日志等
限界上下文:实际上就是事物所处的环境,例如商品生产阶段、销售阶段、运输阶段,根据环境来划分子域!
考虑到通用性,领域/子域应该可以从两个角度来看:
业务角度:称之为子域,让业务专家理解,核心业务、支撑业务、通用业务分别是哪些
技术角度:称之为限界上下文,让技术团队理解,不同的子域对应了不同的服务
在分布式架构中,一个子域就是一个服务!
实体对象和值对象
子域(限界上下文)中包含很多要素,比如领域对象,实体是一个领域对象。
实体
唯一性:但是不同上下文中实体的唯一性可能表现形式不同(代表ID的列不同)
可变性:实体在不同的上下文中状态可能不一样
行为(动作)导致实体状态发生变化,实体状态变化时触发事件,事件将不同的实体关联起来。
值对象
与实体一样,复杂的值可以作为对象存储,在上下文具有一些特殊的性质:
唯一性:没有必要重复创建内容相同但是内存地址不同的值对象
集合性:复杂值才又有必要对象化,复杂值一般包含多个属性
稳定性:对外是只读的,因为一个值对象可能被多个其他对象引用,外部对象可以随意修改的话就乱套了
可判别:同类的值对象之间是可以比较的,相等性比较或者大小比较
实体和值对象的关系
在具体建表时,一个实体对应一张表!
实体如果需要引用值对象,可以
将值对象的字段展开到实体层面,优点是比较只管,缺点是需要很多额外的字段
将值对象序列化成JSON字符串保存到单个字段中,缺点是读写值对象时需要额外的序列化/反序列化操作
上述实现方式并没有真正将值存储成对象。
对象化需要建表,如果模型设计上一个值不会被多个对象引用,可以不用建表。
一般来说,如果值对象需要被多次引用,我们会让实体对象持有值对象的引用,这样比较直观,ORM 中在实体对象上定义多对多关联。
如果一个值对象本身只会被一个实体对象持有,就没有必要单独为值建个表,直接存实体数据表中就可以了!
聚合和聚合根
聚合就是对领域内的实体和值对象进行组装以完成业务封装。
聚合类似于一个组织,组织的老大叫做聚合根。
聚合根是聚合内的一个实体,负责
与其他聚合之间的沟通
实现聚合内部的实体和值对象的协同工作
保证聚合内行为的的事务性和数据一致性
事务性:一个行为要么彻底成功,要么彻底失败,不能部分成功
一致性:部分成功时需要回滚已经成功的部分,标记为彻底失败
一个聚合实际上就是一个模型/数据表。
领域事件
聚合的业务是独立的:一个聚合执行一个动作时不应该直接调用其他聚合,它应该只维护子集内部的状态和数据。
聚合之间的协同需要通过领域事件完成:一个聚合完成一个动作后,产生一个领域事件发往消息中心,其它监听了这个事件的聚合会自动处理该事件。
领域事件使聚合之间解耦,聚合专注于处理自己内部的事务和数据——事务独立性和数据一致性。
小结
领域:我们需要关注并且实施的业务范围
子域:从业务角度对领域进行划分
限界上下文:从技术角度对领域进行划分,理论上与子域一一对应,同时也对应一个服务
一个上下文中包含多个领域对象,领域对象有实体和值对象
几个领域对象形成聚合,聚合有聚合根
聚合根负责对外协同,对内管理
聚合间的协同具体通过领域事件实现,需要配和消息中心
领域事件是聚合之间的协同,与上下文无关:同一个上下文内的聚合之间,或者不同上下文的聚合之间。
分析业务需求形成应用服务
三步拆分法:知识、模型、架构。
知识和模型属于领域驱动设计的内容,架构是领域驱动设计的技术方案落地。
建模思路:
分析业务流程:哪些业务场景,每个场景的参与者、命令和事件有哪些
形成领域对象,然后进一步组装出聚合对象和聚合根
基于聚合对象划定限界上下文,即服务
分析业务流程
业务需求分为哪些场景?
每个场景的参与者、命令和事件有哪些?
业务流程就是一个动宾短语:做了什么事
加上参与者刚好就是一个主谓宾短句:谁做了什么事
多个业务流程本身有先后顺序,它们可以形成一个图
事件一般是命令的正常执行结果,可能有多个:什么事物已经被怎么样了
抽取领域对象和生成聚合
将业务场景中的四个要素(参与者、业务流程、命令、事件)转化为 DDD 中的元素(实体、事件、命令)。
圆形表示实体,矩形表示命令,五边形表示事件
寻找聚合和服务边界时可以不用特意区分实体和值对象
有些实体可能出现在多个场景中,例如选修课申请,其关联的命令也可能出现在多个场景中
学生和老师更倾向于归类为通用实体
选修课实体出现在了申请场景和签到场景中,但是侧重点不太一样
申请场景中,学生更关注选修课的内容、课时、学分等
签到场景中,更关注它的授课时间、位置等信息
同一实体在不同的上下文中具体含义可能不完全一样!
然后从逻辑上生成聚合:
可以一个场景生成一个聚合
申请和审批关联性较强,也可以合并为一个聚合
学生、老师属于人员组织,单独拎出来
最终可以形成三个聚合,他们与场景并不是一一对应的:
选修课申请聚合:申请场景和审批场景,本质上都是在完成选课,是核心业务
签到聚合:可以作为支撑业务
人员组织聚合:通用业务,系统其他敌方也需要用到
确定服务边界(限界上下文)
基于聚合划分限界上下文(服务)。
聚合是服务的逻辑边界:技术人员在方法论的指导下主观的划分,不同的人可能划分方式不一样
限界上下文是服务的“物理”边界:每个服务在技术实现上是独立的,这是客观的,不同服务之间不能随意调用
一般人员组织考虑作为单独的上下文(通用域、服务),其余聚合按实际业务场景划分,具有一定的主观性。
领域驱动设计分层
上一节中从需求中分析出了领域知识,通过聚合、划界架构出了一系列逻辑独立的“微服务”。
这一节将拆分出的微服务分层连接,重新形成一个完整的应用系统。
在软件架构中,分层设计能够在全局层面为软件提供一个“骨架”,骨架搭建好之后将前面分析出的领域对象这些东西塞进去,形成有血有肉的系统。
分层的概述与原则
分层设计能够使复杂问题简单化,从全局层面简化问题。
一个好的分层骨架的特征:
高内聚:每层专注于实现自己的功能
低耦合:层与层之间通过标准接口进行通信(所以需要定义好稳定的标准接口)
可扩展:上层只需要调用下层的接口即可,不需要管下层内部如何实现,便于下层扩展
可复用:每层可以向多层提供服务,尤其是基础层的通用模块
DDD 的直接结果逻辑上很好理解
领域层是核心层,核心层不应该依赖于其他层,核心层应该专注于核心业务的实现,不应该去处理其他杂务。
所以需要依赖关系调换一下——DIP(Dependency Inversion Principle, 依赖倒置原则)。
DIP核心观点:高层模块不应该直接依赖于底层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
基础层是底层模块,它应该依赖上层提供的抽象接口,而不是直接成为上层的依赖。
这里取消了用户接口层对领域层的依赖?
分层的内容
用户接口层(表现层)
用户界面
Web 服务:接收 HTTP 请求,解释、验证、转换请求参数
远程调用:分布式系统的核心!
应用层:协同领域层的任务和工作,组装业务,例如信息安全验证、用户权限校验、事务控制、消息发送、消息订阅等
领域层:实现核心业务
包含 DDD 中的领域对象(实体、值对象、聚合、聚合根、领域服务)
操作一个或者多个领域对象,完成业务
实体无法完成的业务由领域服务完成
基础层:通过其余三层抽象出接口,基础层依赖此抽象的接口实现功能
基础层实现的数据库访问,就是面向领域层接口的
领域层调用基础层提供的接口获取数据,而不需要管基础层内部是如何从数据库中拿到数据的——依赖倒置
服务内部的分层调用和服务间的调用
一个应用可以拆分成多个服务,每个服务都包含上述四层结构。
服务内部不同层之间需要相互调用,不同服务之间也需要相互调用。
API网关调用是同步的:A需要等待B返回结果,例如下订单时需要等待库存扣减完成
领域事件是异步的:例如下订单时备份订单信息,此时A无需等待备份完成
事件方式还能实现多个服务异步同时处理同一个消息
把分层映射到代码结构
其他服务通过基础层中的 API 网关将调用请求传入用户接口层
传入的调用请求包含的数据通过 Assembler 转换成用户接口层内部的 DTO 对象,再传递给 Facade 完成调用
Facade 受到调用请求后将其转发到应用层,以命令的形式发到 Application Service
Application Service 组合领域内的 Aggregate 和 Service 完成业务逻辑
涉及到数据读写时通过 Repository 和数据库进行交互
领域层实现事件过程,处理业务时由应用层发布或者订阅事件,完成与其他服务的通信
架构分层映射到代码结构示例
org.ddd/
|- userinterface/
|- assembler/
|- dto/
|- facade/
|- application/
|- command/
|- event/
|- service/
|- domain/
|- aggregate01/
|- entity/
|- event/
|- repository/
|- service/
|- aggregate02/
|- infrastructure/
|- config/
|- util/
用户接口层
VO (View Object, 视图对象): 展示层(用户界面)提供的数据
Assembler: 数据转换器,将 VO 转换成用户接口层的内部数据 DTO
DTO (Data Transfer Object, 数据传输对象): 用户接口层内的数据
Facade: 供外部服务调用的入口,比如 API 服务的 Controller
userinterface/
|- assembler/
|- dto/
|- facade/
应用层
Command: 用户接口层发起的调用命令
Application Service: 调用和组装领域层提供的领域服务、领域对象等,粗粒度实现业务
Event: 应用层的事件只负责触发订阅或者发布,读取订阅的具体内容或者设置发布内容由领域层的事件完成!
application/
|- command/
|- event/
|- publish/
|- subscribe/
|- service/
领域层
领域层的四个要素:聚合、服务、仓库、事件。
聚合:通常只封装一个实体,及围绕该实体的方法,高内聚
实体:业务字段、业务方法,跨实体的方法应该放到服务中
领域事件:实现发送和监听事件的功能,负责聚合间的沟通,聚合中的方法或者服务都可以产生事件
领域服务:需要跨实体调用或者调用外部服务的业务
仓库:读取或者持久化数据,一般一个聚合对应一个仓库
domain/
|- <聚合A>/
|- entity/
|- event/
|- repository/
|- service/
|- <聚合A>/
基础层
工具、算法、缓存、网关等通用类。
infrastructure/
|- config/
|- utils/
评论区