面试追问地图
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
| 分层 | 依赖方向、职责、事务 | Manager 是否必要、跨层调用 |
| 限界上下文 | 模型和语言边界 | 如何从业务识别边界 |
| 实体/值对象 | 身份、不可变、合法性 | 值对象如何持久化 |
| 聚合根 | 一致性边界、外部访问入口 | 聚合大小、跨聚合一致性 |
| Repository | 聚合持久化抽象 | 与 DAO 区别、复杂查询 |
| 贫血/充血模型 | 规则位置、可维护性 | 巨型 Service 如何重构 |
| 领域事件 | 已发生事实、解耦 | Outbox、幂等、乱序 |
| 防腐层 | 模型转换、隔离外部概念 | 与普通 Adapter 的区别 |
| 状态机 | 合法状态和转换 | 并发更新、补偿流程 |
| DDD 取舍 | 业务复杂度和团队能力 | 何时只采用部分实践 |
DDD 题不要只背名词,应选择一个真实业务对象说明边界、不变量、事务和事件。
一、分层与职责
常见后端分层如何划分?
难度 🟡
- Controller/API:协议转换、参数校验、鉴权入口。
- Application/Service:用例编排、事务边界。
- Domain:业务规则和领域模型。
- Infrastructure:数据库、消息、外部服务实现。
分层的目标是控制依赖和变化,不是单纯创建更多包。
Controller、Service、Manager、DAO 如何分工?
- Controller 不承载核心业务。
- Service 表达业务用例和事务。
- Manager 可封装跨 DAO、第三方或通用能力,但不能成为无边界杂物层。
- DAO/Repository 负责持久化访问。
如果所有逻辑都堆在 Service,应继续按业务能力拆分,而不是只把代码搬到 Manager。
DTO、DO、VO 是否必须全部存在?
不必机械转换。需要隔离以下变化时再分离:
- 外部接口契约。
- 数据库存储结构。
- 领域模型。
- 页面展示结构。
简单 CRUD 中过多模型转换会增加维护成本。
二、DDD 基础
什么是限界上下文?
难度 🔴
限界上下文定义一个领域模型和术语有效的边界。
同一个词在不同上下文可以有不同含义,例如:
- 订单上下文关注交易状态。
- 物流上下文关注运单和配送状态。
微服务边界应优先参考业务上下文,而不是按数据库表拆分。
实体和值对象的区别?
- 实体由身份标识区分,属性变化后仍是同一个对象。
- 值对象由全部属性定义,没有独立身份,通常设计为不可变。
金额、坐标、时间区间常适合值对象,可在构造时维护合法性。
什么是聚合和聚合根?
聚合是一组需要保持一致性的领域对象,外部只能通过聚合根修改内部状态。
聚合边界决定:
- 单个事务修改范围。
- 一致性规则。
- Repository 的保存单位。
聚合过大会造成锁竞争和加载成本,过小则把强一致规则推到分布式协调。
Repository 的职责是什么?
Repository 为领域层提供类似集合的持久化抽象:
- 按业务标识获取聚合。
- 保存聚合。
- 隐藏数据库访问细节。
复杂报表查询不必强行通过聚合 Repository,可使用独立查询模型。
三、贫血模型与充血模型
贫血模型和充血模型的区别?
- 贫血模型:对象主要保存数据,业务逻辑集中在 Service。
- 充血模型:实体和值对象同时封装状态和业务规则。
充血模型有利于维护不变量,但并不意味着把所有流程和外部调用塞进实体。
如何避免“巨型 Service”?
- 将业务规则移入实体和值对象。
- 将跨实体但属于领域的规则放入 Domain Service。
- Application Service 只编排用例。
- 将不同业务用例拆为独立服务或命令处理器。
Domain Service 什么时候使用?
当一条领域规则:
- 不自然属于某个实体或值对象。
- 涉及多个领域对象。
- 仍然是纯业务规则,而不是技术调用。
数据库、MQ 和 HTTP 调用属于基础设施,不应直接放进 Domain Service。
四、事务与领域事件
事务边界应该放在哪里?
通常放在 Application Service 的一个业务用例上。
事务应:
- 覆盖必须原子完成的本地修改。
- 尽量短。
- 避免在事务中进行慢远程调用。
什么是领域事件?
领域事件表达领域中已经发生且其他部分关心的事实,例如:
OrderPaid
FenceEntered
UserRegistered事件使用过去式,包含必要业务信息,不直接暴露内部实体。
领域事件如何可靠发布?
本地事务中同时保存业务数据和 Outbox 事件,提交后由后台任务或 CDC 发布。
消费者必须幂等,并处理重复、乱序和失败补偿。
命令和事件有什么区别?
- 命令:希望某件事发生,可以失败,通常有明确接收者。
- 事件:某件事已经发生,不应被撤销,可能有多个订阅者。
五、上下文集成
什么是防腐层 ACL?
防腐层位于自身领域与外部系统之间,负责:
- 模型转换。
- 协议适配。
- 错误语义转换。
- 隔离外部系统概念。
它避免外部系统的数据结构和术语污染自身领域模型。
微服务之间如何共享模型?
不要直接共享数据库实体类。应共享稳定契约:
- API Schema。
- 事件 Schema。
- 独立客户端 SDK。
共享代码库过多会让服务重新产生编译期耦合。
如何处理跨上下文业务流程?
- 简单同步流程可由 Application Service 编排。
- 长流程可使用 Saga 或流程管理器。
- 每个上下文维护自己的数据和事务。
- 失败通过补偿而不是跨库大事务回滚。
六、设计原则与可维护性
SOLID 如何落地而不是停留在定义?
- SRP:按变化原因拆分。
- OCP:通过策略和扩展点新增能力。
- LSP:子类型不破坏父类型契约。
- ISP:提供小而聚焦的接口。
- DIP:业务依赖抽象,基础设施实现抽象。
不要为了原则创建大量只有一个实现的空洞接口,抽象应服务真实变化。
如何判断是否需要设计模式?
先识别重复变化:
- 多种可替换算法:策略模式。
- 多步骤可插拔处理:责任链。
- 创建逻辑复杂:工厂。
- 跨对象状态通知:观察者。
模式是对变化的组织方式,不是代码越复杂越高级。
如何设计状态机?
- 明确状态集合。
- 明确允许的转换。
- 转换时校验前置状态。
- 状态变化和关键副作用保持一致。
- 对非法转换返回明确错误。
数据库更新可使用 WHERE id=? AND status=? 保证并发下的状态前置条件。
七、场景题
如何建模订单取消?
需要区分:
- 用户主动取消。
- 超时自动取消。
- 支付后退款。
- 履约后不可取消。
领域模型应定义允许的状态转换;定时任务或延迟消息只负责触发命令,不能绕过订单自身规则。
如何建模地理围栏?
可拆分:
- 围栏实体:标识、状态、业务归属。
- 几何值对象:圆、多边形、坐标系。
- 生效时间值对象。
- 命中策略和通知规则。
坐标转换、空间索引属于基础设施或领域服务,围栏状态变化由聚合维护。
什么时候不适合使用复杂 DDD?
- 业务只是简单 CRUD。
- 领域规则少且变化慢。
- 团队缺乏统一建模能力。
- 项目生命周期短。
可以只采用限界上下文、值对象、明确事务边界等部分实践,不必一次引入全部战术模式。