加载中...

本篇要搞懂的 4 件事:一个 DDD 微服务里到底有哪几种服务(Facade / 应用 / 领域 / 基础)、这些服务在运行时怎么互相调用、严格分层 vs 松散分层差在哪、以及数据对象 PO/DO/DTO/VO 在各层之间怎么一路转换流动。

⚠️ 这一讲比较"细"——前面 07/08 讲的是静态架构图(有哪些层),13/14 讲的是代码目录长什么样(代码放哪),16 讲补的是动态运行视图:请求进来后,服务一层层怎么调、数据一层层怎么变形。三者合起来才是完整的微服务内部结构。

承接前文:07/08 确定了 DDD 分层架构(用户接口层 → 应用层 → 领域层 → 基础层),13/14 把这套分层落成了代码模型(每层放什么类、什么目录)。但光有"架子"还不够——真正跑起来时,服务怎么逐层调用、数据怎么逐层转换才是开发时天天打交道的东西。16 讲就是把这台机器拆开,给你看里面的传动结构。它分两条线讲:

服务的协作 ──→ 4 种服务 / 3 类调用 / 封装组合 / 严格 vs 松散分层
数据的协作 ──→ PO ↔ DO ↔ DTO ↔ VO 在各层的转换流动

一、微服务里有哪 4 种服务

按 DDD 分层切出来的微服务,内部的服务按层归位,各管一摊:

服务类型 所在层 干什么 类比
Facade 服务 用户接口层 接前端 Restful 请求、解析输入;把 DO 组装成 DTO 发回前端 前台接待:对外的那张脸
应用服务 应用层 服务的组合、编排、转发;管业务用例的执行顺序和结果拼装;对外提供粗粒度服务 项目经理:自己不干活,负责调度
领域服务 领域层 封装核心业务逻辑,实现需要多个实体协作的领域逻辑 核心工程师:真正干活的
基础服务 基础层 提供数据库 / 缓存等基础资源服务,主要是仓储服务,靠依赖倒置解耦 后勤仓库:存取数据

🪞 辅助理解:把一个微服务想象成一家餐厅。Facade 是前台服务员(接单、上菜、把后厨的菜摆盘成顾客看到的样子);应用服务是领班(不炒菜,负责安排"先上凉菜再上热菜"的流程);领域服务是主厨(真正掌握菜怎么做的核心手艺);基础服务是仓库管理员(从冷库取食材、把剩料入库)。顾客(前端)只跟前台打交道,根本不知道后厨怎么运作——这就是分层要的效果。

💡 注意:基础服务(仓储)既能被领域服务调,也能被应用服务调。后者是为了应付缓存、文件这类"没什么业务逻辑、纯查询"的数据,懒得绕领域层。

二、服务怎么互相调用(运行时三类场景)

微服务的服务调用:内部跨层、微服务之间、领域事件驱动三类场景

看图抓什么:从左到右就是一次请求的旅程——前端 APP → API 网关 → Facade(用户接口层)→ 应用服务(应用层)→ 领域服务(领域层)→ 仓储接口 → 仓储实现 → 数据库/文件缓存(基础层)。注意中间那条往下走的消息队列:它是异步事件的通道,连着"微服务外"的事件发布/订阅和应用服务调用。这张图把三类调用场景全画进去了。

① 微服务内跨层调用——主流程,应用服务是总指挥,有两条路径:

路径一(走领域层,有业务逻辑):
  应用服务 → 领域服务 → 组装实体+实体方法 → 仓储服务取持久化数据 → 初始化实体

路径二(跳过领域层,纯查询):
  应用服务 → 仓储服务(直接读缓存/文件,没领域逻辑,不碰数据库持久化)

② 微服务之间调用——可以直接访问对方应用服务,也可以走 API 网关。

💡 跨微服务做新增/修改时要警惕:这是分布式事务,得自己保证数据一致性(单机事务那套不灵了)。

③ 领域事件驱动——一种异步、间接的调用方式:

微服务内:聚合之间 ── 走事件总线 EventBus ── 异步处理
微服务间:服务之间 ── 走消息中间件(MQ)── 异步处理

流程:应用服务处理完业务 → 发生领域事件 → 调事件发布服务 → 发出去
     另一边:收到订阅主题 → 事件订阅服务 → 调事件处理领域服务 → 继续业务

🪞 辅助理解:同步调用像打电话——你得等对方接、等对方答完才能挂。领域事件像发微信群消息——你发完就继续干自己的,谁关心谁去接(订阅),处理多久跟你没关系。这就是"异步、解耦"的好处。

三、服务是怎么"逐级封装、组合、暴露"的

服务从领域层逐级向上封装、组合、暴露的完整视图

看图抓什么:这张和上一张布局几乎一样,但重点不是"调用箭头",而是数据对象的转换标注——用户接口层有"DTO/数据装配",领域层有"DO",基础层有"PO"。它其实是在告诉你:服务往上封装的同时,数据也在跟着变形(这是第四节的伏笔)。这里先看服务从下往上的封装逻辑:

主要服务形态 职责要点
基础层 仓储服务(接口+实现) 接口供上层调;实现完成 DO 的持久化和初始化
领域层 实体方法 + 领域服务 实体用充血模型(业务逻辑写进实体类的方法里),是原子业务单元;归不到实体上的逻辑才做成领域服务
应用层 应用服务 + 事件发布/订阅服务 组合编排领域服务(也可调外部微服务的应用服务);还能做安全认证、权限校验、数据校验、分布式事务控制
用户接口层 Facade 服务(接口+实现) 服务定向 + DO↔DTO 转换组装,是前端和微服务之间的桥

💡 两条"军规":

  1. 充血模型——尽量把业务逻辑塞进实体自己的方法里(富领域模型),实在塞不进去才做领域服务。设计实体时只管它自己的属性和行为,别管外部流程,这样领域模型才稳。
  2. 聚合之间必须经应用层——禁止聚合 A 的领域服务直接调聚合 B 的领域服务,也禁止两个聚合的数据表直接关联。要交互,走应用层。这是为了聚合之间解耦。

🪞 辅助理解(充血 vs 贫血):贫血模型像"数据是数据、逻辑是逻辑"——实体只是个装数据的盒子(一堆 get/set),所有业务逻辑堆在 service 里。充血模型是"数据和行为长在一起"——订单这个对象自己就有 订单.取消()订单.计算总价() 方法。DDD 提倡充血,因为业务规则跟着对象走,不容易散落、不容易被绕过。比如校验、约束这类规则长在对象上,比散在各处的 if 判断更难被漏掉。

四、严格分层 vs 松散分层(重点对比)

分层架构有条铁律:每层只能依赖它下方的层。但"下方"是指紧邻的下一层还是任意下层,就分出了两个流派:

松散分层架构 严格分层架构
依赖范围 可依赖任意下层 只能依赖紧邻的下一层
暴露方式 下层服务可越级直接暴露给任意上层 下层必须逐级封装后才能往上提供
优点 简单、快,无需逐级封装 隐藏核心逻辑、应用层不臃肿、可管理性好
缺点 ① 容易暴露领域层实现细节
② 服务变更时不好追谁调了它,难通知所有调用方
多写一层封装,啰嗦

松散分层——领域层的实体方法、领域服务可以越级直接暴露给应用层甚至用户接口层:

松散分层:用户接口层可越过应用层直接依赖领域层(虚线)

看图抓什么:那条虚线箭头是关键——用户接口层绕过应用层,直接捅到领域层。实线是正常的逐层依赖,虚线就是"松散"允许的越级依赖。

松散分层实例:abDomainService 越过应用层直接暴露给 abFacade

看图抓什么:看中间那根从领域层 abDomainService() 斜着直接射到用户接口层 abFacade 的箭头——它跳过了应用层。实体 A 的方法在 aAppService 里组合后暴露给 aFacade,但 abDomainService 这个领域服务被直接甩给了 abFacade任意下层都能暴露给任意上层,这就是松散的代价:领域层的核心逻辑直接漏到了最外层。

严格分层——每层只服务紧邻的上一层,下层要跨层就先封装

严格分层:每层只向紧邻上层提供服务,逐层依赖

看图抓什么:三个层之间只有笔直向上的实线,没有任何虚线、没有任何越级。用户接口层→应用层→领域层,一格一格走,谁也别想跳。

严格分层实例:实体方法必须封装成领域服务才能上浮,领域服务必须经应用服务

看图抓什么:对比松散那张实例图,这里多了一整排 DomainService(aDomainService、bDomainService、cDomainService)。原因就是严格分层的规矩——实体 A 的方法 a.f() 不能直接暴露,必须先封装成 aDomainService 才能给到 aAppServiceabDomainService 组合了 A、B 实体方法后,也只暴露给 abAppService每一级都老老实实经过封装,没有斜线越级。所以严格分层的代码会"多一层壳",但好处是核心逻辑藏得住、改动时只需通知紧邻上层。

💡 实战取舍:严格分层更"正统"、可维护性更好,但封装层数多;松散分层省事但容易把领域层暴露出去、变更难追踪。作者倾向严格分层——尤其当你的领域模型是核心资产、要长期稳定时。

五、数据对象怎么一路转换(PO/DO/DTO/VO)

DDD 里数据对象有好几种"形态",分布在不同层,同一份数据在不同层会换一身衣服

简称 全称 待在哪 是什么
PO Persistent Object(持久化对象) 基础层 数据库表结构一一映射,持久化的载体
DO Domain Object(领域对象) 领域层/应用层 运行时的实体,核心业务的载体
DTO Data Transfer Object(数据传输对象) 用户接口层、微服务之间 传输用的载体(前端↔应用层、微服务↔微服务)
VO View Object(视图对象) 前端应用 界面展示用的数据

数据对象在微服务各层的转换流动:VO↔DTO↔DO↔PO↔数据库

看图抓什么:这是全篇最该背下来的一张图。从左到右是数据的"变装链路",注意每两层之间那个菱形就是一次转换点

前端APP        用户接口层        应用层      领域层        基础层         数据库
  VO  ←DTO→  DTO ←Facade→  应用服务  ← DO →  领域服务/仓储接口 ← PO →  数据库
       (Restful           (DO-DTO转换)        (DO-PO转换)
       API JSON)
   ↑层间转换DTO-DO↑                          ↑仓储实现做DO↔PO↑

逐层说人话:

  • 基础层:先建好 DO↔PO 的映射。要存数据时仓储把 DO 转成 PO 写库;要读数据时从库里读出 PO 转回 DO 做初始化。多数情况 PO 和 DO 一一对应,但也有多对多,需要数据重组
  • 领域层:主角是 DO(实体+值对象的数据和行为载体),靠 DO↔PO 转换完成存取。
  • 应用层:主角还是 DO。但要调别的微服务时,DO 转成 DTO 出去传输;接收时反过来 DTO→DO 再处理。DTO 和 DO 若是一对多,也要重组。
  • 用户接口层:Facade 做 DO↔DTO 互转——把多个 DO 组装成 DTO 发给前端。
  • 前端应用:主角是 VO,界面拿 VO 渲染,和后端之间用 DTO 通信(走 Restful API、JSON)。

🪞 辅助理解(一份数据的变装记):"一个订单"从数据库到屏幕,要换 4 次衣服:在数据库里它是一行行的表记录(PO,按字段排列);被读进领域层变成有方法、能 .取消() 的活对象(DO);要发给前端时打包成一个传输包裹(DTO,只装前端要的字段);到了浏览器再变成页面上能显示的样子(VO)。为什么不一种格式走到底?因为每层关心的事不一样——数据库关心存储、领域层关心业务、传输关心带宽、界面关心展示。各换各的衣服,互不绑死,这样改数据库不影响前端、改界面不影响领域逻辑。

💡 这其实回答了思考题:为什么要搞这么多对象?核心是解耦 + 各司其职——让每一层只依赖自己那层的数据形态,外层怎么变都传导不到核心领域模型,保证 DO(领域模型)的稳定。

总结

16 讲讲的是 DDD 微服务的动态运行视图,两条主线:

服务线——微服务内有 4 种服务各居其位:Facade(用户接口层,对外的脸)→ 应用服务(应用层,调度编排)→ 领域服务(领域层,核心逻辑)→ 基础服务/仓储(基础层,存取数据)。运行时有三类调用:微服务内跨层(应用服务两条路径:走领域层 / 跳过领域层纯查询)、微服务之间(注意分布式事务)、领域事件驱动(异步、解耦,走 EventBus / MQ)。服务从领域层逐级向上封装组合,依赖方式分严格分层(只依赖紧邻下层、逐级封装、藏得住核心、可管理性好)和松散分层(可越级、省事但暴露细节、变更难追)。

数据线——同一份数据在各层换不同形态:PO(库里,对表)↔ DO(领域层,活对象)↔ DTO(传输包裹)↔ VO(界面展示),转换点主要在基础层(DO↔PO)、用户接口层(DO↔DTO)、前端(DTO↔VO)。这么多对象不是脱裤子放屁,而是为了层层解耦、把变化挡在外层、保住核心领域模型的稳定

一句话速记

请求进来从 Facade 一路往里到仓储,数据进来从 VO 一路变身到 PO;服务严格分层逐级封装藏住核心,数据各层各换衣服互不绑死——一切都为了"外层随便变、核心保持稳"。

思考题

结合你"会设计架构、代码靠 AI 代写"的情况,下次让 AI 帮你生成一个 DDD 微服务时,可以拿这几条去审 AI 写的代码

  • AI 生成的代码里,业务逻辑是写进了实体的方法(充血模型),还是又堆回了 service 里成了贫血模型?(后者说明它没按 DDD 来)
  • 你有没有发现 AI 经常偷懒搞松散分层——让 Controller/Facade 直接调领域服务、甚至直接调仓储?这在小项目没事,但你要核心模型稳定时该不该拦它?
  • 数据对象上,AI 是老老实实分了 PO/DO/DTO/VO,还是一个对象从数据库一路裸奔到前端(实体直接当返回值)?这个"一路裸奔"有什么坏处?想想它和用 DTO 只暴露前端需要的字段之间的区别——比如会不会把不该给前端的内部字段也带出去。
公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关