领域服务、应用服务与仓储

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

1. Domain Service(领域服务)与 Application Service(应用服务)的清晰边界中业务规则 vs 编排

请说明 Domain Service(领域服务)与 Application Service(应用服务)的清晰边界,业务规则与编排各自属于哪一层?

  • 领域服务与应用服务的职责差异
  • 业务规则属于领域层,编排属于应用层
  • 何时使用领域服务

Domain Service 属于领域层,承载无法自然归属到某个实体或值对象的领域业务规则,例如"转账需要校验两个账户币种并计算手续费"这类跨越多个对象的领域逻辑。Application Service 属于应用层,负责用例编排(use case orchestration):协调仓储、领域服务、事务边界、发送消息、权限校验等,但不承载领域业务规则。清晰的边界是:应用服务"编排"(谁调用谁、事务怎么开、仓储怎么用),领域服务"规则"(业务约束、领域计算)。领域服务方法应无状态、语义化,应用服务调用领域服务完成业务并管理事务。

把业务规则放应用层会导致"贫血模型 + 事务脚本",领域逻辑散落各处难以复用和测试。把编排放领域层则会让领域层依赖基础设施(仓储、事务),破坏依赖方向。边界清晰后,领域层可独立测试,应用层薄而清晰。

#
★★★

2. Command 与 Query 的分离(CQS)在 Application Service 的实现

如何在 Application Service 中实现 Command 与 Query 的分离(CQS)?

  • CQS 原则(命令改状态、查询读数据)
  • Application Service 中命令与查询方法的分隔
  • 命令与查询的返回值约定

CQS 要求一个方法要么是 Command(改变状态)要么是 Query(读取状态),不能两者兼有。在 Application Service 实现上:命令方法(如 createOrder、placeOrder)接收命令参数、执行领域操作、不返回领域数据(或只返回新聚合的 ID、不返回查询结果);查询方法(如 getOrder、findOrders)读取数据并返回只读 DTO/读模型,不改变状态。可把命令与查询分别放到不同的 Service 或不同的方法命名空间,便于理解与测试。严格的 CQRS 更进一步把读写模型分开(独立读模型、独立数据库),而 CQS 只是方法级分离。

CQS 让方法语义清晰、副作用可控,便于测试与缓存。命令方法返回 ID 而非整个聚合,避免把状态暴露;查询方法返回只读 DTO,避免把领域对象泄漏给表现层。它是不引入独立读模型情况下的一种轻量级读写分离。

#
★★★

3. 事务边界与 Application Service 的对应中@Transactional 应该在 Service 层而非 Repository 层

事务边界为什么与 Application Service 对应?@Transactional 为什么应放在 Service 层而非 Repository 层?

  • 事务边界与用例的对应
  • Service 层事务 vs Repository 层事务
  • 聚合与事务的关系

事务边界应对应"用例"(一个应用服务方法 = 一个事务),一个用例通常只修改一个聚合并协调其他操作。@Transactional 放在 Application Service 层意味着整个用例的方法执行在一个事务内,跨多个仓储、多个领域服务调用的所有变更要么全部提交要么全部回滚。若把 @Transactional 放在 Repository 层,每个仓储方法会各自开一个事务,多个仓储调用无法在同一个事务内原子提交,事务边界被切碎,无法保证用例的一致性。此外,事务只应在需要写操作时开启,纯查询方法不应随便加事务。

事务边界的本质是"业务一致性边界"。放在 Service 层让事务与用例一一对应,保证一个用例内所有写操作原子。Repository 层事务只能保证单次数据访问的原子性,无法协调跨仓储操作。所以事务注解应设在应用服务层。

@Service
public class OrderApplicationService {
    @Transactional // 事务边界在应用服务层
    public OrderId placeOrder(PlaceOrderCommand cmd) {
        Order order = orderRepository.findById(cmd.orderId());
        order.place(cmd.items());
        orderRepository.save(order); // 与上方 order 变更在同一事务内
        return order.getId();
    }
}
#
★★★

4. Application Service 的「事务内远程调用」反模式中事务中调用外部 API 或发消息的边界在哪,如何用本地事务 + Outbox 模式拆分保证一致性?

为什么"事务内远程调用"是反模式?事务中调用外部 API 或发消息的边界在哪?如何用本地事务 + Outbox 模式拆分保证一致性?

  • 事务内远程调用的危害
  • 本地事务 + Outbox 模式
  • 分布式一致性的保证

事务内远程调用(在数据库事务未提交时调用外部 API 或发送消息)是反模式,原因:外部调用不可控(可能慢、失败、超时),占用数据库事务时间造成锁与连接资源浪费;外部调用返回后本地事务才提交,若外部已生效但本地回滚,会造成状态不一致;远程调用失败时事务已无法回滚,被迫沿长事务或补偿。正确边界是:把"本地事务"与"外部副作用"分离。用事务性发件箱(Transactional Outbox)模式:业务操作与事件写入同一数据库事务(业务表 + outbox 表同库同事务),事务提交后由独立 relay(轮询 or 订阅 binlog)把 outbox 中的事件发布到消息中间件/外部 API。这样业务数据与"待发送事件"原子一致,外部投递由 relay 负责,保证不丢失、不重复(配合幂等)。

Outbox 把"外部副作用"从本地事务中拆出,本地事务只保证业务数据 + outbox 记录原子写入,外部投递异步进行,解决了"本地事务与远程调用的一致性"问题。它避免了分布式事务(2PC)的复杂与脆弱,是分布式系统中保证最终一致性的标准模式。

#
★★

5. Repository 接口属于领域层,实现属于基础设施层的依赖倒置

为什么 Repository 接口应属于领域层、实现属于基础设施层?这体现了什么原则?

  • 依赖倒置原则(DIP)
  • Repository 接口与实现的归属
  • 领域层不依赖基础设施

依据依赖倒置原则(DIP),抽象应属于高层模块,实现属于低层模块。Repository 接口定义的是领域层所需要的"聚合存取"语义(如 findById、save),属于领域层的需求,因此接口声明在领域层;而具体实现(JPA、MyBatis、JDBC 等)属于基础设施层,实现领域层接口。领域层只依赖 Repository 接口(抽象),不依赖具体实现,基础设施层依赖领域层接口。这样依赖方向是"领域层 <- 基础设施层",领域层与持久化技术解耦,可独立测试(用 mock 或内存实现)。

依赖倒置让核心业务逻辑不依赖任何技术框架。若 Repository 接口定义在基础设施层,领域层就得依赖基础设施,违反依赖方向。把接口放领域层,实现放基础设施层,通过依赖注入(IoC)在运行时装配,实现领域层与 ORM 的彻底解耦。

#
★★

6. Repository 模式(Fowler P of EAA)的核心中内存对象与持久化的解耦

请解释 Fowler《Patterns of Enterprise Application Architecture》中 Repository 模式的核心:内存对象与持久化的解耦?

  • Repository 模式的初衷
  • 领域对象与持久化机制的解耦
  • 集合语义

Repository 模式的核心是让领域对象的获取与持久化对领域层透明,领域层用"集合语义"(如接口可表现 findById、add、remove)来存取聚合,而不关心底层是数据库表、ORM 还是缓存。Repository 把"内存中的领域对象"与"持久化存储"解耦:领域层只面向 Repository 接口,不触碰 SQL/JPA;持久化细节(对象-关系映射、查询、连接管理)全部封装在 Repository 实现中。这样领域模型不依赖持久化技术,可被内存实现替换用于测试,也便于切换存储技术。

Repository 是领域层与基础设施层之间的"防腐接口",它把集合语义暴露给领域层,把实现隐藏在后面。核心价值是"领域层不知道数据存在哪里、怎么存",从而保持领域模型纯净、可测试、技术无关。

#
★★

7. Specification Pattern(Evans)与 Repository.findBy(Specification) 的协作

请解释 Specification Pattern(Evans)以及它与 Repository.findBy(Specification) 的协作方式?

  • Specification 模式的定义
  • 业务规则作为可复用对象
  • 与 Repository 的协作

Specification(规格)模式把业务规则(如"订单金额大于 X 且未取消")封装成可复用的谓词对象,包含 isSatisfiedBy 方法判断某对象是否满足规格,并支持 and/or/not 组合。它与 Repository 协作时,Repository 提供 findBy(Specification) 方法,把规范作为查询条件传给仓储,由实现把规格翻译成 SQL/JPA 查询。这样业务规则在领域层定义一次,既可用于内存过滤(isSatisfiedBy),也可用于数据库查询(翻译成查询条件),避免规则散落、重复实现。

Specification 把"查询条件的业务含义"显式建模,使查询语义可复用、可组合、可测试。它让领域层表达"业务规则"而不依赖具体查询语言,Repository 负责把规格翻译成持久化查询,二者配合实现了"业务规则驱动查询"。

#
★★

8. Application Service 与 Adapter(Controller、Job、CLI)的协作模式

Application Service 与 Adapter(Controller、Job、CLI)如何协作?

  • Adapter 与 Application Service 的分工
  • 输入输出适配
  • 依赖方向

Adapter(Controller、Job、CLI、RMQ 消费者等)是应用层的外层门面,负责把外部输入(HTTP 请求、命令行参数、消息)转成应用服务可用的命令/DTO,调用 Application Service,并把结果转成外部格式(JSON、响应体)。Application Service 是核心用例逻辑,不感知 HTTP/CLI 等具体协议。二者协作时,Adapter 只做"翻译与适配",不包含业务逻辑;一个 Application Service 可被多个 Adapter 复用(同一个用例被 HTTP、Job、CLI 调用)。依赖方向是 Adapter -> Application Service -> 领域层,Adapter 与领域层解耦。

这种协作让"用例"与"入站协议"解耦,同一业务逻辑可被不同客户端复用,避免每个入口重复实现业务。Adapter 薄、Service 承载业务,是六边形/端口适配器架构的体现,也便于测试(直接测 Service 而非 Controller)。

#
★★

9. Application Service 的"工作单元"(Unit of Work)模式

Application Service 中的"工作单元"(Unit of Work)模式是什么?它如何工作?

  • Unit of Work 的定义
  • 跟踪变更与统一提交
  • 与事务的配合

工作单元(Unit of Work)维护一组在业务事务期间被修改的业务对象,跟踪这些对象的变更,并在事务结束时统一把变更持久化到数据库,保证"要么全部要么全不"。它把"变更跟踪"与"提交时机"集中管理:Application Service 在事务内加载聚合、执行命令改变聚合,Unit of Work 记录脏对象,事务提交时统一 flush 到数据库。典型实现是 JPA 的 EntityManager / Hibernate 的 Session,它们自动把持久化上下文中的实体变更在事务提交时同步到数据库。工作单元让应用层不必逐个 save,仓库仅负责加载,变更状态由 Unit of Work 管理。

Unit of Work 与事务绑定,是"聚合作为整体持久化"的实现基础。它把"变更收集"与"持久化"分离,应用层只需关注业务逻辑,提交时统一落库,避免反复 save 的性能与一致性负担。

#
★★

10. 应用服务与领域服务的边界中用例 vs 领域逻辑?

应用服务与领域服务的边界如何划分?用例(use case)与领域逻辑(domain logic)分别属于哪一层?

  • 用例与应用服务的对应
  • 领域逻辑与领域服务的对应
  • 边界划分的准则

应用服务对应"用例"(use case),描述一个系统交互流程(如"提交订单"),负责编排:获取聚合、调用领域服务、管理事务、协调仓储、处理结果。领域服务对应"领域逻辑"(domain logic),承载无法归入单个实体/值对象的业务规则与领域计算。划分准则:问"这段逻辑是否能自然归属到某个领域对象?"能则放领域对象,涉及多个领域对象或暂无处安放则放领域服务;"这是否是系统交互流程/技术编排?"是则放应用服务。领域服务与被应用服务调用,应用服务不包含业务规则,领域服务不包含技术编排。

用例是"用户与系统的一次交互",领域逻辑是"业务自身的规则"。应用服务薄、面向用例,领域服务厚、面向业务规则。这条边界保证领域层的规则可复用、可测试,应用层只做编排与适配。

#
★★

11. Repository 查询能力膨胀的治理中当仓储方法超过合理数量且多为组合查询时,应引入 CQRS 读模型还是保留仓储,判断依据与迁移路径是什么?

Repository 查询能力膨胀如何治理?当仓储方法超过合理数量且多为组合查询时,应引入 CQRS 读模型还是保留仓储?判断依据与迁移路径是什么?

  • 仓储查询膨胀的识别
  • CQRS 读模型 vs 仓储的取舍
  • 迁移路径

仓储接口若出现大量组合查询、报表查询、跨聚合查询,说明仓储职责被滥用(仓储应服务于聚合的写操作与简单读取,而非承载复杂查询)。判断依据:若查询是"服务聚合加载"(按 ID 加载、按业务键取聚合)应保留仓储;若查询是"面向展示/报表/跨聚合组合"的复杂过滤、聚合、字段投影,应引入 CQRS 读模型(专门读模型/只读仓储/查询服务),用独立 DTO 与查询 SQL 服务读场景。迁移路径:先把复杂查询从仓储剥离到独立读模型/查询服务,让仓储收敛回聚合加载语义;再逐步为读模型建立独立表/视图/索引;需要时演进为独立读库。写路径仍走仓储,读路径走读模型,两者解耦。

仓储语义是"聚合的集合",加满组合查询会污染其职责并让接口膨胀。CQRS 读模型专为查询优化,可投影、可加索引、可去重,是"查询膨胀"的治理方向。判断核心是"查询是否为聚合加载服务",组合查询归读模型。

#

12. Repository 的分页与排序(Pageable、Sort)的边界

Repository 的分页与排序(Pageable、Sort)的边界在哪里?

  • 分页排序是否属于仓储语义
  • 查询组件与仓储的边界
  • 分页返回类型

分页与排序是查询基础设施能力,仓储作为"聚合/数据访问"接口可提供带分页排序的查询方法,但边界在于:分页排序参数(Pageable、Sort)属于查询语法的适配,不影响领域语义;仓储方法应返回分页结果(如 Page)与 DTO 或聚合,不在仓储中做业务过滤。分页排序通常用于列表查询/读模型,写操作的聚合加载(按 ID)不需要分页。若查询复杂,分页排序更适合放在读模型/查询服务,仓储保留聚合加载语义。

分页排序是基础设施层的通用能力,放在仓储接口中可以,但应避免让仓储承担业务过滤。区分"聚合加载"与"列表查询":前者用仓储按 ID,后者用分页排序查询(可独立于仓储)。

#

13. 仓储模式中集合语义与基础设施隔离?

仓储模式如何实现集合语义与基础设施隔离?

  • 仓储的集合语义
  • 基础设施隔离
  • 领域层不依赖数据库

仓储把聚合的存取抽象为"集合语义":领域层通过 findById、findBy(Specification)、save、remove 等接口操作聚合,如同操作一个内存集合,而不知道底层是数据库、ORM 还是缓存。这种抽象把领域层与基础设施隔离:领域层只依赖仓储接口,不感知 SQL、连接、事务等技术细节;实现层负责把集合操作翻译成持久化操作。仓储接口定义在领域层,实现放在基础设施层,靠依赖注入装配,从而保证领域模型纯净、可测试、可替换存储。

集合语义是仓储模式的"灵魂"——它让领域层认为聚合就存在一个集合里,消除对持久化的依赖。隔离的实现靠接口放领域层 + 实现放基础设施层 + 依赖注入,依赖方向始终从基础设施指向领域层。

#

14. 事务边界中应用服务层的事务管理?

为什么事务管理应放在应用服务层?事务边界如何设定?

  • 事务边界与用例对应
  • 应用服务层事务管理
  • 事务的粒度控制

事务管理应放在应用服务层,因为事务边界应对应用例(一个用例 = 一个事务)。应用服务方法加 @Transactional 后,方法内调用多个仓储、领域服务、Outbox 写入都在同一事务内,保证原子性。事务粒度应与用例一致:过细(每个仓储方法一个事务)无法保证跨操作原子性,过粗(跨多个用例)会拉长事务、引发锁竞争。事务边界应覆盖"需要一起成功或一起失败"的写操作;纯查询方法不应加事务(或只加只读事务)。读重写轻的场景要控制事务时长,避免事务内做耗时操作。

事务边界本质是"一致性边界",与聚合/用例对应。放在应用服务层让事务与业务用例一一对应,是事务粒度最合理的落点。事务内避免远程调用与耗时操作,避免长事务。

#

15. 领域服务 vs 领域对象的方法?

领域服务与领域对象(实体)的方法如何区分?何时逻辑放进领域服务而非实体方法?

  • 领域对象方法 vs 领域服务的分工
  • 多对象协作逻辑的归属
  • 领域服务无状态

领域对象(实体/聚合根)的方法承载"属于该对象自身状态"的业务逻辑,如 order.cancel()、account.withdraw()。领域服务承载"无法自然归属到某个领域对象、涉及多个对象协作"的领域逻辑,如转账(MoveMoney)需要同时操作两个账户,或跨越多个聚合并协调它们的规则。判断准则:逻辑是否只依赖并修改一个聚合根的状态?是则放实体方法;需要多个聚合/多个对象协作或属于领域级规则,则放领域服务。领域服务应无状态(不持有状态,仅表达领域行为),其方法按领域语义命名。

把多个对象协作的规则硬塞进某个实体方法会破坏实体内聚,造成"哪个对象都有点关系"的混乱。领域服务把这些跨对象规则集中表达,实体方法保持对自身状态的内聚操作。它不持有状态,只承载行为。

#

16. 仓储与查询中读模型的简化?

仓储与查询如何通过读模型简化?

  • 读模型的概念
  • 复杂查询与读模型的分离
  • 简化建模

读模型(Read Model)是专门为查询优化而设计的、面向展示/报表的数据结构,与写模型(聚合)分离。当查询复杂(跨聚合、报表、多表 join、字段投影)时,使用读模型而非让仓储承载复杂查询,可显著简化:读模型用 DTO/视图/独立表表达查询结果,可加索引、可缓存、可针对特定查询定制结构,避免为复杂查询加载完整聚合。写路径仍走仓储(聚合),读路径走读模型,两者解耦,各自优化。这让仓储保持聚合加载语义,查询侧获得性能与灵活性。

读模型把"读取展示需求"与"领域建模"分离,是 CQRS 思想的轻量应用。它避免为了一个复杂查询去加载和映射整个聚合,也避免仓储被组合查询撑爆。读模型可按需演化,不影响写模型。

#

17. 仓储的实现中 ORM 与原生 SQL?

仓储的实现如何选择 ORM 与原生 SQL?

  • ORM 与原生 SQL 的取舍
  • 仓储实现的技术选择
  • 复杂查询与性能

仓储实现可从 ORM(JPA/Hibernate、MyBatis)或原生 SQL(JDBC、JdbcTemplate)中选择。ORM 适合对象-关系映射顺畅、聚合结构清晰、CRUD 与关联加载为主的场景,开发效率高、自动映射、变更跟踪;但当有复杂查询、性能敏感的 join、批量操作、报表 SQL 时,ORM 生成的 SQL 可能低效或难控,此时用原生 SQL/JdbcTemplate 更合适。常见做法是"混合":聚合加载与简单持久化用 ORM,复杂查询用原生 SQL 或读模型。无论哪种,仓储实现都封装在基础设施层,领域层只依赖接口。

ORM 与原生 SQL 是互补而非互斥。ORM 提升常规持久化效率,原生 SQL 掌控复杂查询与性能。把两者都隔离在仓储实现内部,领域层不受影响,可针对具体查询选择最合适的技术。