第 3 章:仓库接口契约设计原则

仓库(Repository)的接口契约是模块边界解耦的核心锚点。它定义了上层业务逻辑与数据持久化之间的“法律条文”——任何违反契约的行为都将导致依赖方的信任崩溃。本章围绕接口粒度、数据模型抽象、查询规范与返回值约定四个支柱,剖析如何在分层边界处建立稳定、可测试、可替换的契约体系。

3.1 契约设计的四个支柱

仓库接口必须同时满足四套正交约束,缺少任何一个都会在后期引入耦合或语义模糊。下表列出各支柱的决策维度及典型权衡场景:

支柱核心问题粗粒度风险细粒度风险推荐方向
接口粒度一个方法承担多少职责?方法臃肿、难以组合、多态性差方法爆炸、调用方需多次往返按聚合根+用例场景拆解
数据模型抽象接口参数/返回值使用领域对象还是持久化传输对象?透传ORM实体,领域层污染过度封装DTO,失去领域行为优先使用领域值对象,必要时才引入轻量DTO
查询规范如何表达查询条件、排序、分页?参数列表过长,可读性差Query对象滥用,序列化成本高固定组合模式 + 可选Specification
返回值约定成功/失败如何表示?异常是否透传?抛出受检异常,调用方被迫处理使用Optional隐藏null,但丢失错误详情标准化结果类型 + 领域异常
核心观点:仓库接口不是“数据访问层”的简单封装,而是领域层与基础设施层之间的信任边界。契约必须足够强壮以允许任意一方独立演化。

3.2 接口粒度:按聚合根划分的边界原则

仓库的粒度应与其所属的聚合根(Aggregate Root)的生命周期管理范围对齐。一个聚合根对应一个仓库,一个仓库方法对应一个完整的用例场景。常见粒度错误包括:

  • 把“保存”和“更新”拆成两个方法——实际用例中通常是“保存或更新”的幂等操作。
  • 为每个实体属性提供单独的查询方法——应使用规范(Specification)模式或查询对象统一表达。
  • 在仓库接口中暴露“批量删除”等危险操作——应通过领域服务协调,避免基础设施直接接触集合删除。

3.3 数据模型抽象:不泄露实现细节的封装策略

接口方法的参数和返回类型必须只包含领域层可以理解的类型,禁止直接使用ORM实体、数据库自增ID或数据表结构。推荐采用以下分层抽象:

  • 领域值对象:如 Email、Money、CustomerId,携带业务含义且不可变。
  • 领域聚合根:作为返回主体,保证完整性。
  • 轻量查询视图(View Model):仅当查询不需要领域行为且组合字段来自多个聚合时,才引入扁平化的只读DTO。
风险警示:DTO泛滥会导致领域层贫血;领域对象直接序列化会导致循环引用和延迟加载异常。必须通过显式契约限定DTO的使用范围——只存在于查询场景中,不得参与命令(Command)或事件(Event)。

3.4 查询规范:稳定可组合的接口风格

查询条件是仓库接口中最易变的部分。采用固定查询方法+可选规范的组合方式可同时满足稳定性和灵活性:

  1. 固定命名方法:如 findByOrderId(OrderId id)、findRecentOrders(int limit),适用于高频确定查询。
  2. 规范对象(Specification):通过组合谓词表达复杂过滤,但注意序列化与数据库翻译复杂度。
  3. 分页/排序统一注入:使用标准参数对象 PageRequest(page, size, sort),避免每个方法都写分页参数。

3.5 返回值约定:消除调用方对底层异常的依赖

返回值约定直接决定了调用方的错误处理模式。推荐采用以下层级:

场景返回值形式说明
查询返回单条记录,可能不存在Optional<AggregateRoot>避免null,调用方显式处理缺失
查询返回多条记录List<AggregateRoot>空集合代表无数据,非null
写操作(增删改)结果对象(Success/Failure)或void + 领域事件不抛出数据层异常,封装为领域异常
唯一约束冲突等业务级失败领域异常(如DuplicateEmailException)调用方按业务类型处理,而非底层SQL错误
实施建议:将持久化层(如JPA、MyBatis)的 DataAccessException 在仓库实现内部消化,转换为领域异常或返回失败结果。如此,替换持久化方案(例如从PostgreSQL切换到MongoDB)时,调用方代码无需修改。

3.6 契约间的关系:信任链路的起点

仓库接口契约设计支柱关系图 仓库接口契约设计支柱关系 接口粒度 方法职责边界 数据模型抽象 领域对象 vs DTO 查询规范 固定方法+Specification 返回值约定 Optional/Result/异常 调用方(领域服务) 依赖契约而非实现 信任 信任链路:四个支柱共同保障调用方与实现方之间的解耦 任何一方的破坏都会导致边界渗漏,契约评审应同时检查四个维度。

3.7 选型权衡与风险点

在实际项目中,契约设计常面临以下权衡:

  • 高度抽象 vs 学习成本:引入Specification和Result模式可以增强灵活性,但团队需要额外理解“谓词组合”和“结果模式”的概念。对于小团队或快速原型,可先采用固定命名方法+Optional,后续逐步演进。
  • 领域对象完整返回 vs 性能:完整返回聚合根可能导致不必要的数据库关联查询。可引入延迟加载策略或为查询场景单独设计轻量投影(Projection)。
  • 异常 vs 返回值:将数据层异常转换为领域异常增加了代码量,但这是保持调用方纯洁的唯一途径。禁止在仓库接口中抛出SQLException或DataAccessException。
核心风险:“接口过度泛化”——为追求所谓的“通用仓库”而设计出 find(Class<T> clazz, Map<String,Object> filters) 之类的接口,导致类型安全和可读性丧失。每个仓库接口应该是具体到聚合根类型的强类型契约。

3.8 落地步骤

  1. 识别聚合根:根据领域模型划分出生命周期管理单元,每个聚合根对应一个仓库接口。
  2. 列出核心用例:针对每个聚合根,写出业务所需的CRUD和查询场景,作为接口方法的候选列表。
  3. 定义数据模型边界:决定哪些值对象直接暴露,哪些需要DTO,建立从领域对象到持久化模型的映射策略。
  4. 选择查询模式:对固定查询使用命名方法,对复杂过滤采用Specification,统一分页/排序参数。
  5. 设计返回值策略:为每类操作(查询单条、列表、写操作)指定返回类型,并编写领域异常类别。
  6. 契约评审:召集领域团队与基础设施团队,模拟替换实现(如从MySQL切换至内存)来验证接口是否无泄漏。

3.9 验收标准

维度验收检查项通过条件
完整性所有用例场景都有对应的仓库方法覆盖率达到100%
抽象性接口参数和返回值没有ORM特定类型无 Session、EntityManager、SqlSession 等依赖
可替换性用Mock或内存实现可完成领域层单元测试构造一个轻量仓库Mock后,已有业务用例通过
一致性所有仓库遵循相同的查询规范和返回值模式代码审查无例外
异常隔离不会在仓库调用方捕获到数据层异常领域层代码不引用任何持久化框架异常