第 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 查询规范:稳定可组合的接口风格
查询条件是仓库接口中最易变的部分。采用固定查询方法+可选规范的组合方式可同时满足稳定性和灵活性:
- 固定命名方法:如
findByOrderId(OrderId id)、findRecentOrders(int limit),适用于高频确定查询。 - 规范对象(Specification):通过组合谓词表达复杂过滤,但注意序列化与数据库翻译复杂度。
- 分页/排序统一注入:使用标准参数对象
PageRequest(page, size, sort),避免每个方法都写分页参数。
3.5 返回值约定:消除调用方对底层异常的依赖
返回值约定直接决定了调用方的错误处理模式。推荐采用以下层级:
| 场景 | 返回值形式 | 说明 |
|---|---|---|
| 查询返回单条记录,可能不存在 | Optional<AggregateRoot> | 避免null,调用方显式处理缺失 |
| 查询返回多条记录 | List<AggregateRoot> | 空集合代表无数据,非null |
| 写操作(增删改) | 结果对象(Success/Failure)或void + 领域事件 | 不抛出数据层异常,封装为领域异常 |
| 唯一约束冲突等业务级失败 | 领域异常(如DuplicateEmailException) | 调用方按业务类型处理,而非底层SQL错误 |
实施建议:将持久化层(如JPA、MyBatis)的
DataAccessException 在仓库实现内部消化,转换为领域异常或返回失败结果。如此,替换持久化方案(例如从PostgreSQL切换到MongoDB)时,调用方代码无需修改。
3.6 契约间的关系:信任链路的起点
3.7 选型权衡与风险点
在实际项目中,契约设计常面临以下权衡:
- 高度抽象 vs 学习成本:引入Specification和Result模式可以增强灵活性,但团队需要额外理解“谓词组合”和“结果模式”的概念。对于小团队或快速原型,可先采用固定命名方法+Optional,后续逐步演进。
- 领域对象完整返回 vs 性能:完整返回聚合根可能导致不必要的数据库关联查询。可引入延迟加载策略或为查询场景单独设计轻量投影(Projection)。
- 异常 vs 返回值:将数据层异常转换为领域异常增加了代码量,但这是保持调用方纯洁的唯一途径。禁止在仓库接口中抛出SQLException或DataAccessException。
核心风险:“接口过度泛化”——为追求所谓的“通用仓库”而设计出
find(Class<T> clazz, Map<String,Object> filters) 之类的接口,导致类型安全和可读性丧失。每个仓库接口应该是具体到聚合根类型的强类型契约。
3.8 落地步骤
- 识别聚合根:根据领域模型划分出生命周期管理单元,每个聚合根对应一个仓库接口。
- 列出核心用例:针对每个聚合根,写出业务所需的CRUD和查询场景,作为接口方法的候选列表。
- 定义数据模型边界:决定哪些值对象直接暴露,哪些需要DTO,建立从领域对象到持久化模型的映射策略。
- 选择查询模式:对固定查询使用命名方法,对复杂过滤采用Specification,统一分页/排序参数。
- 设计返回值策略:为每类操作(查询单条、列表、写操作)指定返回类型,并编写领域异常类别。
- 契约评审:召集领域团队与基础设施团队,模拟替换实现(如从MySQL切换至内存)来验证接口是否无泄漏。
3.9 验收标准
| 维度 | 验收检查项 | 通过条件 |
|---|---|---|
| 完整性 | 所有用例场景都有对应的仓库方法 | 覆盖率达到100% |
| 抽象性 | 接口参数和返回值没有ORM特定类型 | 无 Session、EntityManager、SqlSession 等依赖 |
| 可替换性 | 用Mock或内存实现可完成领域层单元测试 | 构造一个轻量仓库Mock后,已有业务用例通过 |
| 一致性 | 所有仓库遵循相同的查询规范和返回值模式 | 代码审查无例外 |
| 异常隔离 | 不会在仓库调用方捕获到数据层异常 | 领域层代码不引用任何持久化框架异常 |