DDD 战术模式与领域驱动设计(聚合/界限上下文/Repository)

共 30 题
📑 题目列表 30 题
#
★★★

1. 为何两个上下文间不应直接共享数据库(应为防腐层)

请解释为何两个界限上下文(Bounded Context)间不应直接共享数据库,而应通过防腐层(Anti-Corruption Layer)交互?

  • 界限上下文(BC)的自治与边界
  • 共享数据库的耦合与模型侵扰
  • 防腐层(ACL)的作用
  • 界限上下文:DDD 中每个上下文有独立的领域模型与通用语言,边界是自治的。直接共享数据库会破坏这种自治。
  • 为什么不直接共享数据库:① 两个上下文的模型不同(可能同名不同义),共享表会让一个上下文的模型耦合到另一个的 schema,导致模型被侵扰、无法独立演化;② 共享库导致强耦合,一个上下文的表结构变更影响另一个;③ 违背"上下文边界",使边界形同虚设。
  • 防腐层(ACL):用 ACL 隔离——一个上下文需要另一个上下文的数据时,通过 ACL 转换(把对方的模型/语义转换成自己的模型),不直接访问对方数据库。ACL 是适配器,负责翻译、隔离、防止对方模型污染自身。
  • 正确做法:每个上下文拥有自己的数据库/模式,跨上下文交互通过接口/事件/ACL,不直接共享库。

共享数据库会让两个上下文模型耦合、无法独立演化,破坏边界自治。防腐层(ACL)转换模型、隔离边界,是跨上下文交互的正确方式。

#
★★★

2. 共享内核(Shared Kernel)的风险与适用限制

请解释共享内核(Shared Kernel)的风险与适用限制?

  • 共享内核的定义(共享的领域模型部分)
  • 风险(耦合、演变困难)
  • 适用限制
  • 共享内核(Shared Kernel):两个界限上下文共享一部分领域模型(如共享的实体、值对象、契约),即"共享的领域核心"。它比共享数据库轻,但仍是共享。
  • 风险:① 共享模型会产生耦合——一个上下文修改共享部分会影响另一个,需协调;② 共享部分演变困难(双方都依赖,改一处牵动全局);③ 共享内核可能被滥用,退化成"共享数据库/共享模型"。
  • 适用限制:适用于关系紧密、团队协作密切的上下文(如同一团队负责的两个上下文),且共享部分小且稳定(不频繁变更)。共享内核要求双方遵守契约、共同维护、定期沟通。若共享部分大或变化频繁,应改用防腐层/发布语言(Published Language)隔离。
  • 结论:共享内核是"小范围、稳定、紧密协作"时的选择,否则风险大于收益。

共享内核把部分领域模型共享以复用,但带来耦合与演变困难。仅适用于"小、稳定、紧密协作"的上下文,否则用防腐层隔离。

#
★★★

3. 如何避免"贫血模型",把行为放回实体而非仅 getter/setter

请解释如何避免"贫血模型",说明如何把行为放回实体而非仅 getter/setter?

  • 贫血模型的问题
  • 把业务行为放入实体
  • 保持不可变与校验
  • 贫血模型:实体只有 getter/setter 和字段,业务逻辑全部放在 Service 中,实体是"数据携带者",丢失了领域行为。这导致领域逻辑散落在服务层、难以内聚、复用。
  • 避免贫血模型:把业务逻辑放回实体/值对象——实体应包含与自身状态相关的业务行为(如订单的 pay()cancel()changeStatus()),方法与状态内聚,业务规则(校验、状态迁移)在实体内部实现。
  • 做法:① 实体方法表达业务操作(方法名用业务语言,如 Order.pay()),内部维护状态并校验;② 用能力方法(如 addMoney)而非裸 setter;③ 校验不变量在实体内部;④ 值对象封装自包含逻辑。
  • 注意:并非所有逻辑都进实体——跨聚合/跨实体的协调逻辑放领域服务,应用服务编排用例。贫血模型是三本建模(事务脚本)的产物,rich model 是 DDD 的核心。

贫血模型把行为剥离到 Service,实体只剩数据。避免方法是把与状态相关的业务行为放回实体,让实体自洽、内聚,领域逻辑集中在模型内。

#
★★★

4. 实体(Entity)与值对象(Value Object)在身份与可变性上的区别

请解释实体(Entity)与值对象(Value Object)在身份与可变性上的区别?

  • 实体的身份(ID)与可变性
  • 值对象的等值性与不可变性
  • 选择标准
  • 实体(Entity):有身份(ID),身份贯穿生命周期,即使属性变化仍是同一实体。实体可变(状态可变化),如订单(订单号不变,状态可改)。实体用 ID 比较相等。
  • 值对象(Value Object):无身份,由属性值定义,两个值对象属性相同即相等(如金额、地址、颜色)。值对象不可变(创建后不可改,修改需创建新对象)。
  • 区别:实体"有身份、可变、按 ID 比较";值对象"无身份、不可变、按值比较"。
  • 选择标准:需要追踪身份、生命周期长、会变化 → 实体;仅描述属性、无身份、可整体替换 → 值对象。值对象不可变带来并发安全与可共享。

身份与可变性是核心区别:实体有 ID 可变,值对象按值相等不可变。建模时按"是否需要身份与变化"选择实体或值对象。

#
★★

5. Specification 模式如何把查询/业务规则对象化并可组合

请解释 Specification 模式,说明如何把查询/业务规则对象化并可组合?

  • Specification 封装业务规则
  • 可组合(and/or/not)
  • 用于查询与规则校验
  • Specification 模式:把业务规则/查询条件封装成规范对象(Specification),每个规范实现"是否满足"的判断(isSatisfiedBy),把规则从散落的 if 条件中提取为可复用的对象。
  • 可组合:规范支持组合操作——and(且)、or(或)、not(非),把简单规范组合成复杂规则,如 isActive.and(isVip)。组合让规则可复用、可清晰表达。
  • 用途:① 规则校验(判断对象是否满足规则);② 查询(把规范翻译成查询条件,如数据库查询、内存过滤);③ 组合业务规则清晰、可测试。
  • 实现:规范接口 boolean isSatisfiedBy(T),合取/析取/取反规范组合子。

Specification 把业务规则对象化并支持 and/or/not 组合,让复杂规则可复用、可测试、可翻译为查询。是 DDD 中处理复杂规则与查询的常用战术模式。

interface Spec<T> { boolean isSatisfiedBy(T t); }
class AndSpec<T> implements Spec<T> {
    Spec<T> a, b; public boolean isSatisfiedBy(T t) { return a.isSatisfiedBy(t) && b.isSatisfiedBy(t); }
}
Spec<Order> vipActive = new AndSpec<>(isActive, isVip); // 组合
#
★★

6. 值对象的不可变性如何简化并发与缓存

请解释值对象的不可变性如何简化并发与缓存?

  • 不可变值对象的并发安全
  • 可自由共享缓存
  • 无副作用
  • 不可变性:值对象创建后不可修改(字段 final、无 setter),修改需创建新对象。
  • 简化并发:不可变对象天然线程安全——没有共享可变状态,多线程读同一不可变对象无竞态,无需加锁,可在并发环境中自由共享。
  • 简化缓存:不可变对象可安全共享与缓存——同一个不可变实例可被多个对象/线程引用,无需深拷贝,可放心放入缓存(不会因外部修改而失效),缓存命中率高。
  • 无副作用:不可变对象作为参数传递不担心中途被修改,降低心智负担。
  • 结论:不可变值对象(如金额、地址)在并发与缓存场景下天然安全、高效,是 DDD 推荐建模方式。

不可变值对象没有共享可变状态,天然线程安全;可被安全共享与缓存,无需拷贝。这简化了并发与缓存,是值对象不可变的价值。

#
★★

7. 工厂(Factory)与构造函数在复杂聚合创建中的职责

请解释工厂(Factory)与构造函数在复杂聚合创建中的职责?

  • 构造函数创建简单对象
  • 工厂创建复杂聚合(组装、校验、不变式)
  • 工厂的职责
  • 构造函数(Constructor):创建简单对象,接收参数并初始化字段。适合创建过程简单、参数少、无复杂组装的对象。
  • 工厂(Factory):创建复杂聚合(Aggregate),因为聚合含多个实体/值对象、需要组装子对象、校验不变式、生成 ID、设置初始化状态。工厂把"创建过程"抽象出来,避免构造函数承载复杂逻辑。
  • 工厂职责:① 组装聚合(创建聚合根 + 子实体/值对象);② 校验不变式(构造后满足聚合不变量);③ 生成唯一 ID;④ 设置初始状态;⑤ 封装创建细节,调用方不关心内部组装。
  • 选择:创建简单用构造函数;创建复杂聚合(含子对象、校验、组装)用工厂(领域工厂或工厂方法)。

构造函数适合简单创建,工厂负责复杂聚合的组装、校验、ID 生成与初始状态。工厂把复杂创建过程从构造函数中分离,保持聚合创建的一致性与完整性。

#
★★

8. 上下文映射(Context Map)的合作关系(ACL/OHS/PL)类型

请解释上下文映射(Context Map)的合作关系类型,如防腐层(ACL)、开放主机服务(OHS)、发布语言(PL)?

  • 上下文映射的意义
  • ACL/OHS/PL 等合作关系
  • 各关系适用场景
  • 上下文映射(Context Map):描述界限上下文之间的关系,明确如何协作、如何隔离,是 DDD 战略设计的关键。
  • 合作关系类型:
    • 防腐层(ACL,Anti-Corruption Layer):下游上下文通过 ACL 转换上游模型,防止上游模型污染下游,隔离耦合。
    • 开放主机服务(OHS,Open Host Service):上游对外提供协议化的接口(服务),供下游调用,定义清晰的集成契约。
    • 发布语言(PL,Published Language):上游发布正式的共享语言/模型(如规范的消息格式、Data Contract),供多个下游使用,作为共享契约。
    • 其他:共享内核(Shared Kernel)、客户-供应商(Customer/Supplier)、上司-下属(Conformist)、零散集成(Separate Ways)、伙伴(Partnership)。
  • ACL + OHS + PL 常组合:上游用 OHS 暴露服务 + PL 定义契约,下游用 ACL 转换,形成规范的上下文协作。

上下文映射描述上下文间关系。ACL 隔离转换、OHS 开放服务、PL 发布语言,三者组合规范跨上下文集成,是 DDD 战略设计的核心工具。

#
★★

9. 为何"大聚合"会损害性能,应让聚合尽量小且内聚

请解释为何"大聚合"会损害性能,以及应让聚合尽量小且内聚?

  • 大聚合的危害(事务范围大、锁竞争、加载开销)
  • 小聚合的收益
  • 内聚与边界
  • 大聚合:聚合包含过多实体/对象,聚合根管理大量子对象。
  • 为什么损害性能:① 聚合是事务一致性边界,修改聚合时需加载整个聚合并加锁,大聚合加载大量对象、锁竞争严重、事务范围大,降低吞吐;② 分布式/微服务下大聚合跨子域,边界不清;③ 并发修改大聚合冲突率高。
  • 小聚合:聚合尽量小且内聚,一个聚合只包含必需的对象(聚合根 + 维持不变式必需的子对象),减少事务范围与锁竞争,提升并发与性能。
  • 内聚:聚合内对象围绕一个不变式内聚,聚合之间通过 ID 引用(不直接引用对象),保持边界清晰。
  • 结论:聚合是事务边界,应小且内聚,减少加载与锁开销;大聚合是性能与一致性的隐患。

聚合是事务一致性与锁的边界,大聚合导致加载多、锁竞争、事务大,损害性能。聚合应小且内聚,聚合同用 ID 引用保持边界。

#
★★

10. 为何把领域模型与框架注解(如 JPA @Entity)解耦更利于测试

请解释为何把领域模型与框架注解(如 JPA @Entity)解耦更利于测试?

  • 领域模型与框架解耦
  • 独立测试(无数据库/容器)
  • 依赖倒置
  • 领域模型与框架解耦:领域模型(实体/值对象)不依赖 JPA 注解、ORM 等框架,把"业务"与"持久化"分离。领域模型用纯 Java/Pojo 表达业务。
  • 为什么利于测试:① 领域模型不依赖框架,单元测试无需加载 Spring 容器、无需真实数据库,直接 new 领域对象测试业务逻辑,快、稳定;② 业务逻辑与持久化解耦,测试聚焦业务,不关心 ORM 映射;③ 用依赖注入注入 mock 仓库,隔离外部依赖。
  • 依赖倒置:领域定义接口(Repository),基础设施(JPA 实现)实现接口,领域层不依赖 JPA,测试时用 mock 替换。
  • 收益:领域模型可独立测试、可移植(换框架/ORM 不影响领域)、业务逻辑清晰。
  • 结论:解耦领域模型与框架注解,让领域逻辑可独立、快速、稳定地测试,是"测试友好"与"可替换"的关键。

领域模型不依赖 JPA/框架,测试无需数据库与容器,可快速单测业务逻辑;配合依赖注入 mock 仓库,隔离外部依赖,提升可测试性。

#
★★

11. 为何跨聚合的引用应通过 ID 而非对象引用以保持边界

请解释为何跨聚合的引用应通过 ID 而非对象引用,以保持聚合边界?

  • 聚合边界与一致性
  • 通过 ID 引用解耦
  • 避免跨聚合对象图
  • 聚合边界:聚合是事务一致性边界,聚合内部对象通过聚合根维护不变式。跨聚合引用若用对象引用,会破坏边界。
  • 为什么通过 ID 引用:① 聚合并不随引用一起加载,通过 ID 引用可延迟加载(需要时再查),避免跨聚合的复杂对象图与加载开销;② 通过 ID 引用避免"一个聚合直接修改另一个聚合状态",保持聚合自治与边界;③ 聚合间通过 ID 交互,边界清晰,利于独立演化与分布式。
  • 边界保持:跨聚合访问另一个聚合的数据,通过 ID 从 Repository 获取或通过服务/事件,不直接对象引用。聚合根管理自身聚合,跨聚合用 ID 引用。
  • 结论:通过 ID 引用保持聚合边界,避免跨聚合对象图与耦合,聚合自治、独立演化。

跨聚合用 ID 引用避免对象图耦合与直接状态修改,保持聚合边界与自治。聚合根管理自身,跨聚合通过 ID 获取或事件交互。

#
★★

12. 应用服务(Application Service)与领域服务的分层职责

请解释应用服务(Application Service)与领域服务(Domain Service)的分层职责?

  • 应用服务的职责(编排、事务、DTO)
  • 领域服务的职责(跨聚合领域逻辑)
  • 两者的区别
  • 应用服务(Application Service):位于应用层,负责编排用例——处理请求、调用领域服务/聚合、管理事务边界、DTO 转换、权限/安全。应用服务不包含业务规则,只编排领域操作。
  • 领域服务(Domain Service):位于领域层,承载不属于单个实体/聚合的领域逻辑——跨聚合、跨实体、或需要多个领域对象协作的业务规则。领域服务是纯领域逻辑,无事务/IO。
  • 区别:应用服务是"用例编排 + 事务 + 外部适配",关注流程;领域服务是"领域业务规则",关注业务。应用服务调用领域服务与聚合,领域服务实现跨聚合业务规则。
  • 边界:应用服务薄(只编排),领域服务/聚合厚(业务逻辑)。应用服务不写业务规则,领域服务不处理 IO/事务。

应用服务编排用例、管事务、转换 DTO;领域服务承载跨聚合的领域业务规则。应用服务薄、领域服务厚,各司其职。

#
★★

13. 聚合(Aggregate)与聚合根(Aggregate Root)的一致性边界

请解释聚合(Aggregate)与聚合根(Aggregate Root)的一致性边界?

  • 聚合与聚合根
  • 一致性边界(事务边界)
  • 通过聚合根访问聚合
  • 聚合(Aggregate):一组内聚相关对象的集合,作为一个整体进行一致性管理。聚合内部对象围绕一个业务不变式内聚。
  • 聚合根(Aggregate Root):聚合的入口/根对象,对外唯一可访问的实体。外部只能通过聚合根访问聚合内对象,不能直接访问聚合内其他对象。
  • 一致性边界:聚合是事务一致性边界——修改聚合内对象时,整个聚合作为一个事务单元,一次提交保证聚合内状态一致;聚合之间则最终一致(通过 ID 引用/事件)。
  • 规则:① 通过聚合根访问聚合,保持封装;② 变更聚合内对象必须通过聚合根的方法;③ 聚合根持有一致性,保证聚合内不变式;④ 跨聚合通过 ID 引用或事件,不直接改变其他聚合内部状态。
  • 结论:聚合根是聚合的入口,聚合是事务一致性边界,保证聚合内一致、聚合间最终一致。

聚合是一致性边界,聚合根是唯一入口,外部经聚合根访问,保证聚合内事务一致、聚合间最终一致。这是聚合建模的核心。

#
★★

14. 领域事件在上下文间通过消息总线传播的最终一致

请解释领域事件如何在上下文间通过消息总线传播,实现最终一致?

  • 领域事件(Domain Event)
  • 消息总线传播
  • 最终一致
  • 领域事件(Domain Event):描述聚合/领域发生的有意义变化(如"订单已支付"),是领域层的事实记录,事件名用过去时。
  • 跨上下文传播:一个上下文发布领域事件,通过消息总线(消息队列/事件总线)传播给其他上下文,其他上下文监听事件并更新自己的状态,实现解耦协作。
  • 最终一致:因为事件是异步传播,发布方与接收方状态存在短暂不一致,最终通过事件处理达到一致。发布方不直接调用接收方,通过事件解耦。
  • 关键:事件带唯一 ID、幂等消费(重复事件不产生副作用)、事件要有版本;用 Outbox 保证事件可靠发布;接收方最终一致。
  • 结论:领域事件通过消息总线跨上下文传播,让上下文解耦、异步协作,实现最终一致(短暂不一致后一致)。

领域事件描述领域变化,通过消息总线异步传播到其他上下文,实现解耦与最终一致。事件发布可靠(Outbox)、消费幂等、可版本化。

#
★★

15. 领域服务(Domain Service)何时该承载不属于实体的逻辑

请解释领域服务(Domain Service)何时该承载不属于实体的逻辑?

  • 领域服务承载跨实体/跨聚合逻辑
  • 不适合放实体的逻辑
  • 与贫血/应用服务区分
  • 领域服务承载的逻辑:不属于单个实体/聚合、需要多个领域对象协作、或没有清晰归属的领域业务规则。当逻辑跨多个聚合/实体,或"放在哪个实体都不合适"时,用领域服务承载。
  • 典型场景:跨聚合的转账(涉及多个账户聚合)、跨实体的计算(运费 = 基于商品与配送规则)、需要读多个聚合的规则判断。
  • 判断标准:若逻辑属于"一个聚合内部状态"就放聚合/实体;若逻辑需要协作多个聚合/实体、或依赖外部领域能力,则放领域服务。领域服务是纯领域逻辑,无事务/IO。
  • 区分:不要用领域服务承载"用例编排"(那是应用服务),也不要因"偷懒"把实体逻辑全塞进领域服务(那会退化成贫血模型)。领域服务只在"实体无法自含"时承载。

领域服务承载跨聚合/跨实体、无单一归属的领域逻辑;实体能自含的逻辑放实体。区分应用服务(编排)与领域服务(领域规则),避免退化。

#

16. 传统分层架构(接口/应用/领域/基础设施)的依赖方向

请解释传统分层架构(接口/应用/领域/基础设施)的依赖方向?

  • 四层分层
  • 依赖方向(上层依赖下层)
  • 与依赖倒置的关系
  • 四层:接口层(Interface/UI)、应用层(Application)、领域层(Domain)、基础设施层(Infrastructure)。
  • 依赖方向:传统分层上层依赖下层——接口层依赖应用层,应用层依赖领域层,领域层/基础设施层是最底层。依赖自顶向下。
  • 问题:传统分层中,领域层可能依赖基础设施层(如依赖数据库),导致领域被基础设施耦合。
  • 改进:用依赖倒置(DIP)反转——领域层定义接口(Repository),基础设施层实现接口,应用层/领域层依赖接口而非具体实现,依赖方向指向领域层(核心)。接口层依赖应用层,应用层依赖领域层,领域层不依赖基础设施。
  • 结论:传统分层上层依赖下层,但领域层应通过依赖倒置不依赖基础设施(定义接口,基础设施实现),使领域成为核心。

传统分层依赖自顶向下;但领域层应依赖接口而非基础设施,通过依赖倒置让领域不依赖数据库/框架,使依赖方向指向核心领域层。

#

17. 依赖倒置(DIP)如何使领域层不依赖具体数据库/框架

请解释依赖倒置(DIP)如何使领域层不依赖具体数据库/框架?

  • DIP 原则
  • 领域定义接口、基础设施实现
  • 依赖注入
  • 依赖倒置(DIP):高层模块(领域层)不依赖低层模块(基础设施),两者都依赖抽象(接口);抽象不依赖细节,细节依赖抽象。
  • 如何实现:领域层定义端口(接口,如 Repository 接口),基础设施层实现这些接口(JPA 实现、具体数据库驱动)。领域层只依赖接口,不依赖具体数据库/框架。
  • 依赖注入:运行时通过依赖注入(DI)把基础设施实现注入领域层需要的接口,领域层可见的是接口,具体实现可替换。
  • 效果:领域层不依赖具体数据库/框架——换数据库/换框架只改基础设施实现,领域层不变;领域层可独立测试(mock 接口)。
  • 结论:DIP 让领域层依赖抽象接口,基础设施实现接口并通过 DI 注入,从而领域层不依赖具体数据库/框架,可替换、可测试。

DIP 通过"领域定义接口 + 基础设施实现 + DI 注入"反转依赖,领域层只依赖抽象,数据库/框架成为可替换实现,这是 DDD 的核心。

#

18. 六边形(端口与适配器)如何反转依赖让领域居核心

请解释六边形(端口与适配器)如何反转依赖让领域居核心?

  • 领域在六边形中心
  • 端口与适配器
  • 依赖反转
  • 六边形架构:领域逻辑在中心(六边形内部),外部(数据库、UI、消息)通过端口与适配器连接。
  • 端口(port):领域定义的接口——驱动端口(inbound,领域暴露给外部,如用例接口)与被驱动端口(outbound,领域需要外部能力,如 Repository 接口)。
  • 适配器(adapter):端口的实现——驱动适配器(HTTP 控制器、CLI)调用驱动端口,被驱动适配器(数据库实现、消息实现)实现被驱动端口。
  • 依赖反转:领域只依赖端口(接口),不依赖适配器;适配器依赖领域定义的端口。依赖方向从"领域依赖外部"反转成"外部依赖领域定义的端口",领域居核心。
  • 效果:领域不依赖数据库/框架/UI,外部细节可替换,领域可独立测试(mock 端口)。

六边形用"端口定义契约 + 适配器实现 + 依赖注入"反转依赖,领域只依赖端口,适配器依赖领域端口,外部依赖领域,领域居核心。

#

19. 如何识别"伪 DDD",仅分层命名却仍是事务脚本

请解释如何识别"伪 DDD",说明仅分层命名却仍是事务脚本的问题?

  • 伪 DDD 的特征(贫血模型 + 事务脚本)
  • 仅分层命名而无领域建模
  • 判断标准
  • 伪 DDD:只做"分层命名"(Service/Domain/Repository 目录结构),但实体仍是贫血模型(只有 getter/setter),业务逻辑全在 Service 中(事务脚本),没有真正的领域建模。
  • 特征:① 实体贫血(无行为,只有数据);② 业务逻辑在 Service 的 if/switch 中(事务脚本);③ 领域层只是"包名"没有领域模型;④ 没有聚合、值对象、领域事件、不变式等 DDD 战术元素。
  • 为什么是伪 DDD:DDD 的核心是"用领域模型承载业务逻辑、让模型表达业务",而事务脚本把业务逻辑放在操作层,领域模型形同虚设。仅分层命名没有吸收 DDD 的建模思想。
  • 判断标准:看"业务规则放哪"——若业务规则在 Service 判断、实体无行为,则仍是事务脚本(伪 DDD);若业务规则在实体/聚合/领域服务内聚,才是真 DDD。
  • 结论:伪 DDD 是"分层命名的贫血模型 + 事务脚本",识别靠"业务逻辑是否在领域模型中"。

伪 DDD 只有分层目录,实体贫血、逻辑在 Service(事务脚本),缺少领域建模。识别标准是"业务规则是否在实体/聚合内"。

#

20. CQRS 与 DDD 的契合中写侧聚合+读侧专门模型

请解释 CQRS 与 DDD 的契合,说明写侧聚合与读侧专门模型的配合?

  • CQRS 读写分离
  • 写侧聚合 + 读侧专门模型
  • 与 DDD 的结合
  • CQRS 契合 DDD:DDD 的写侧(命令)用聚合承载业务逻辑,CQRS 把读侧分离出来用专门读模型,两者天然契合。
  • 写侧聚合:命令操作写侧,用 DDD 聚合承载业务规则、维护一致性(通过领域事件/聚合方法)。写侧关注业务正确性。
  • 读侧专门模型:查询操作读侧,用专门优化的读模型(宽表、物化视图、投影),不经过聚合,针对查询优化,避免读拖累写侧。
  • 契合点:写侧用聚合(DDD 战术),读侧用投影/读模型(CQRS),写读通过事件连接(领域事件 → 投影更新读模型)。写侧保证一致性,读侧优化查询,可独立扩展。
  • 结论:CQRS 让写侧用聚合(DDD)、读侧用专门模型,两者通过事件解耦,兼顾业务一致性与查询性能。

CQRS 与 DDD 契合:写侧用聚合承载业务逻辑,读侧用专门读模型优化查询,领域事件连接读写。写侧重一致、读侧重性能,可独立扩展。

#

21. Clean Architecture 同心圆与 DDD 层的对应关系

请解释 Clean Architecture 的同心圆与 DDD 层的对应关系?

  • Clean Architecture 同心圆分层
  • 与 DDD 层的对应
  • 领域居核心
  • Clean Architecture 同心圆:由内到外是实体(Entities)→ 用例(Use Cases)→ 接口适配器(Interface Adapters)→ 框架(Frameworks)。依赖只能向内,内层不依赖外层。
  • 与 DDD 的对应:
    • 实体(Entities)→ DDD 的实体/值对象/聚合(领域层核心)。
    • 用例(Use Cases)→ DDD 的应用服务 / 用例层(编排业务)。
    • 接口适配器(Interface Adapters)→ DDD 的 Repository 接口实现、DTO 转换、控制器(适配器)。
    • 框架(Frameworks)→ 基础设施层(数据库、Web 框架、消息)。
  • 对应关系:Clean 的实体/用例对应 DDD 的领域层+应用层,清洗的接口适配器/框架对应 DDD 的适配器/基础设施。都强调"领域在核心、外部可替换"。
  • 结论:Clean Architecture 的同心圆与 DDD 分层对应,领域/用例居核心,基础设施/框架在外层可替换,依赖向内。

Clean 的实体/用例对应 DDD 领域层+应用层,接口适配器/框架对应 DDD 适配器/基础设施。都让领域居核心、外部可替换、依赖向内。

#

22. Repository 如何为聚合提供"集合式"持久化抽象

请解释 Repository 如何为聚合提供"集合式"持久化抽象?

  • Repository 的集合式语义
  • 屏蔽持久化细节
  • 聚合为导向
  • Repository 模式:为聚合提供"集合式"持久化抽象——把持久化抽象成内存集合(Collection)操作,领域层像操作集合一样读写聚合(add、remove、findById),无需关心底层数据库。
  • 集合式语义:Repository 提供 save(aggregate)findById(id)remove(aggregate) 等方法,语义像操作集合,领域层不受 SQL/ORM 细节影响。
  • 聚合为导向:Repository 以聚合为单位(聚合根为粒度),一次加载/保存整个聚合,保证聚合一致性。Repository 只操作聚合根,聚合内对象通过聚合根管理。
  • 屏蔽细节:Repository 接口在领域层定义,实现(JPA/MyBatis)在基础设施层,领域层不依赖 SQL/ORM,只依赖接口。换存储只改实现。
  • 结论:Repository 把持久化抽象成"集合式操作聚合",领域层像操作集合一样读写聚合,屏蔽 SQL/ORM 细节,以聚合为粒度。

Repository 提供集合式语义(save/findById/remove),以聚合为粒度,屏蔽持久化细节,领域层只依赖接口。这是 DDD 的持久化抽象。

#

23. Repository 的实现如何在事件溯源下变为事件存储

请解释 Repository 的实现如何在事件溯源(Event Sourcing)下变为事件存储?

  • 事件溯源下 Repository 存事件而非状态
  • 保存 append 事件、加载重放
  • Repository 接口不变
  • 事件溯源(ES):聚合状态由事件流重放得到,不直接存状态。
  • Repository 在 ES 下的实现:Repository 接口不变(仍 save/findById),但实现变为"事件存储"——save 时把聚合产生的事件追加(append)到事件存储,而不是更新状态行;findById 时从事件存储重放该聚合的所有事件,重建聚合状态。
  • 实现:Repository 保存事件(append-only),加载时重放事件(replay)构建聚合。事件存储是真相来源,聚合状态是投影。
  • 收益:Repository 接口对领域层不变(集合式语义),实现换成事件存储,领域层无需感知 ES。配合快照优化重放。
  • 结论:ES 下 Repository 的 save 变为 append 事件、findById 变为重放事件重建聚合,接口不变,实现从"状态存储"变为"事件存储"。

ES 下 Repository 保存事件(append)、加载时重放事件重建聚合,接口仍是集合式语义,实现从状态存储变为事件存储,领域层无感知。

#

24. 为何 Repository 不应泄露 ORM/SQL 细节给领域层

请解释为何 Repository 不应泄露 ORM/SQL 细节给领域层?

  • Repository 屏蔽持久化细节
  • 泄露 ORM/SQL 会耦合领域
  • 封装与可替换
  • Repository 的目的:为领域层提供持久化抽象,屏蔽 ORM/SQL 细节,让领域层只依赖接口(集合式语义)。
  • 为什么不应泄露:若 Repository 接口暴露 ORM/SQL 细节(如查询对象、EntityManager、SQL 条件、ORM 注解),领域层就会耦合具体持久化技术,导致:① 换数据库/ORM 要改领域层;② 领域层无法独立测试(需真实数据库);③ 领域模型被持久化细节污染。
  • 正确做法:Repository 接口只暴露领域语义(save(Order)、findById(id)、findByCustomerId(cid)),把 ORM/SQL 封装在实现中;实现(JPA/MyBatis)在基础设施层,领域层只依赖接口。
  • 收益:领域层不依赖持久化技术,可独立测试、可替换存储,模型干净。
  • 结论:Repository 封装 ORM/SQL 细节,接口只暴露领域语义,避免领域层耦合持久化技术,保证可测试与可替换。

Repository 接口只暴露领域语义,ORM/SQL 封在实现中,避免领域层耦合持久化技术。泄露细节会破坏可测试性与可替换性。

#

25. 在一个组织中推行 DDD 时最常见的组织/沟通障碍

请解释在一个组织中推行 DDD 时最常见的组织/沟通障碍?

  • 领域专家与开发协作
  • 通用语言(Ubiquitous Language)
  • 边界划分与团队协作
  • 推行 DDD 的障碍主要是组织与沟通层面:
  • 通用语言(Ubiquitous Language):领域专家与开发往往用不同术语,难以建立统一通用语言,导致需求理解偏差。需要专家与开发持续对齐,建立共同词汇。
  • 领域专家参与:DDD 需要领域专家深度参与建模,但专家忙、沟通成本高,缺少专家导致建模失真。
  • 边界划分:划分界限上下文(BC)需要业务理解与团队协作,边界划分不准导致模型混乱、协作困难。
  • 团队协作:多个团队间共享上下文、协调边界、共享内核等需频繁沟通,跨团队协作困难。
  • 文化/认知:团队习惯事务脚本(贫血模型),对 DDD 建模不熟,抵触变化;管理层缺乏对 DDD 价值的理解。
  • 结论:DDD 的挑战多在"人"——通用语言、专家参与、边界划分、团队协作、认知转变比技术更难。

DDD 推行障碍主要在组织与沟通:通用语言、领域专家参与、边界划分、团队协作与认知转变。技术建模相对简单,人与协作最难。

#

26. 在微服务中每个服务是否都应有独立领域模型与边界

请解释在微服务中每个服务是否都应有独立领域模型与边界?

  • 微服务与界限上下文对应
  • 独立领域模型与数据库
  • 差异化的边界
  • 每个服务应有独立领域模型与边界:微服务的边界通常对应一个界限上下文(BC),每个服务有自己的领域模型、通用语言、数据库(数据边界),独立自治。
  • 为什么独立:服务边界即上下文边界,独立领域模型让服务自治、独立演化、独立部署;独立数据库避免跨服务共享数据耦合。
  • 但并非"每个服务都强行独立":边界划分应基于业务/子域,而不是"每个小功能一个服务"。一个界限上下文可对应一个服务,也可能一个服务包含多个内聚的子域(但边界清晰)。过度拆分会产生分布式单体。
  • 原则:服务边界 = 业务边界(BC),每个服务有独立领域模型与数据边界,但不为拆分而拆分,按业务子域划分。
  • 结论:每个服务应有独立领域模型与边界(对应 BC、独立数据),但边界按业务子域划分,不强行细拆。

微服务边界对应界限上下文,每个服务独立领域模型与数据库。但按业务子域划分边界,不强行细拆,避免分布式单体。

#

27. 如何用不变式(invariant)定义在聚合内必须始终成立规则

请解释如何用不变式(invariant)定义在聚合内必须始终成立的规则?

  • 不变式的定义
  • 在聚合内维护不变式
  • 通过聚合根方法保证
  • 不变式(invariant):聚合内必须始终成立的业务规则,无论何时都不允许违背,是聚合一致性边界的体现。
  • 定义:不变式是聚合内的业务约束,如"订单总金额必须大于 0""库存不能为负""订单状态转换必须合法"。
  • 在聚合内维护:聚合根及其子对象通过方法保证不变式——每次状态变更(通过聚合根方法)都校验不变式,不满足则拒绝(抛异常)。不变式成为聚合的"内部一致性保证"。
  • 实现:聚合根方法内先校验(如扣库存前检查库存充足),变更后保持不变式;构造时校验;用值对象/领域规则封装校验逻辑。
  • 结论:不变式定义聚合内必须成立规则,通过聚合根方法在每次变更时校验,保证聚合内状态始终一致。不变式是聚合的边界判断依据。

不变式是聚合内必须始终成立的业务规则,通过聚合根方法在变更时校验、不满足则拒绝。不变式既是规则也是聚合边界依据。

#

28. 如何用事件风暴(Event Storming)快速梳理领域模型

请解释如何用事件风暴(Event Storming)快速梳理领域模型?

  • 事件风暴的流程
  • 用领域事件驱动建模
  • 识别聚合与边界
  • 事件风暴(Event Storming):一种协作式建模工作坊,领域专家与开发一起,用领域事件作为主线,快速梳理业务与领域模型。
  • 流程:① 领域专家与开发在墙上贴领域事件(橙色便签,过去时,如"订单已支付"),按时间顺序梳理业务流程;② 从事件反推命令/触发者(蓝色便签,如"支付订单");③ 识别聚合(围绕事件/命令,把相关对象聚合成聚合);④ 识别读模型/外部系统;⑤ 划分界限上下文(按事件/业务的边界分组)。
  • 特点:事件驱动、协作、快速,把领域知识可视化,帮助识别领域模型、聚合、边界。
  • 收益:快速对齐领域理解,建立通用语言,识别聚合与上下文边界,为 DDD 建模提供基础。
  • 结论:事件风暴用领域事件作为主线,通过协作让专家与开发快速梳理业务流程、识别聚合与边界,是 DDD 建模的快速入门方法。

事件风暴以领域事件为主线,协作梳理业务流程,反推命令与聚合,划分界限上下文。是快速建立领域模型与边界的方法。

#

29. 界限上下文(Bounded Context)如何划分自治的领域边界

请解释界限上下文(Bounded Context)如何划分自治的领域边界?

  • 界限上下文的定义
  • 按业务子域/通用语言划分
  • 自治与边界
  • 界限上下文(Bounded Context):领域模型有明确边界,一个上下文内模型、通用语言、业务规则一致,上下文之间模型独立、边界清晰。
  • 划分依据:按业务子域(核心域/支撑域/通用域)与通用语言划分——同一套业务语义和术语的边界构成一个上下文。不同上下文可以有同名但不同义的模型(如"订单"在销售域与物流域含义不同)。
  • 自治:每个上下文有独立的模型、数据库、代码边界,独立演化、独立部署,上下文间通过接口/事件/ACL 协作,不直接共享模型。
  • 边界:上下文是"业务边界",也是"技术边界"(数据库、服务边界)。划分时关注业务内聚、团队归属、演进独立性。
  • 结论:界限上下文按业务子域与通用语言划分,每个上下文自治(独立模型/数据库/语言),通过契约协作,是 DDD 战略设计的核心。

界限上下文按业务子域与通用语言划分,上下文内模型/语言一致,上下文间独立自治、通过契约协作。边界的划分决定模型自治与演进。

#

30. 领域事件(Domain Event)如何在聚合内显式表达状态变化

请解释领域事件(Domain Event)如何在聚合内显式表达状态变化?

  • 领域事件表达状态变化
  • 聚合内发布事件
  • 事实与副作用
  • 领域事件(Domain Event):描述聚合内发生的有意义的状态变化,事件名用过去时(如"订单已支付"),是领域事实的记录。
  • 聚合内显式表达:聚合在状态变化时显式发布领域事件——聚合方法内,在状态变更的关键点(如 Order.pay())创建一个领域事件(携带聚合 ID、变化数据、时间戳),记录发生了什么。事件成为"状态变化的事实证据"。
  • 作用:① 记录状态变化(审计、可追溯);② 触发副作用(通知其他聚合/上下文,通过事件总线);③ 解耦(聚合状态变化通知外部,不直接调用)。
  • 实现:聚合保存待发布事件,事务提交后发布(或通过领域事件收集器)。事件表达"状态已变化"这一事实。
  • 结论:领域事件在聚合状态变化时显式创建并发布,记录事实、触发副作用,让状态变化可追溯、可解耦。

领域事件在聚合方法的状态变化点显式创建,记录"状态已变化"的事实,触发副作用并解耦。它是聚合状态变化的外部可观察表达。