仓储接口定义规范
仓储接口是领域层与基础设施层之间的契约边界,其定义规范直接决定数据层的解耦程度。本章围绕方法命名、参数约束、返回类型、异常声明与异步支持五个维度,阐述如何构建稳定、可测试、与实现无关的仓储接口。
1. 边界与分层
仓储接口位于领域层,实现位于基础设施层。领域层依赖倒置,只依赖接口抽象,不依赖任何ORM、数据库驱动或缓存客户端。接口规范必须禁止泄漏任何基础设施类型(如ResultSet、Session、SQLException)。
核心原则:接口的签名和返回值中不应出现任何不属于领域层的类型。 一旦出现
Pageable、Sort等框架类型,即构成对基础设施的隐式耦合。
2. 方法命名规范
方法名应使用统一领域语言,避免暴露数据源实现细节。推荐前缀约定:
| 操作类型 | 推荐前缀 | 示例 | 禁止示例 |
|---|---|---|---|
| 查询单条 | find | findById、findByEmail | getUserById、queryUser |
| 查询集合 | find 或 list | findByStatus、listActive | searchUsers、loadAll |
| 保存/更新 | save | save、saveAll | insert、update、upsert |
| 删除 | delete | deleteById、delete | remove、erase |
| 计数 | count | countByStatus | getCount、total |
3. 参数约束
- 参数类型:只接受领域值对象、实体ID或领域内建类型(如
EmailAddress、Money)。禁止使用Map、String裸参、基础设施DTO。 - 不可变性:所有入参应视为只读,接口实现不应修改参数对象。
- 可选性:使用
java.util.Optional显式表示可能为空或可选的参数(如Optional)。sortField - 分页与排序:如需分页,应使用领域层定义的
PageRequest值对象,而非框架Pageable。
4. 返回类型规范
| 场景 | 推荐返回类型 | 风险 |
|---|---|---|
| 查询单条可能不存在 | Optional<T> | 返回null导致NPE,调用方无法强制检查 |
| 查询多条 | List<T> 或不可变集合 | 返回null或可变集合导致副作用 |
| 保存后返回 | T 或 void | 返回void则调用方无法获取ID,需权衡 |
| 批量操作 | int影响行数 或 void | 暴露实现细节(如JPA的批处理限制) |
重要: 永远不要返回null,优先使用空集合或Optional。集合应通过Collections.unmodifiableList包装成不可变。
5. 异常声明
仓储接口应定义统一的异常基类,所有实现层异常包装到该基类下:
public abstract class RepositoryException extends RuntimeException {
// 可选:错误码、上下文信息
}
public class EntityNotFoundException extends RepositoryException { }
public class ConcurrencyConflictException extends RepositoryException { }
- 接口方法不应抛出任何基础设施异常(如
SQLException、DataAccessException)。 - 如需检查型异常,可定义领域异常并在接口throws中声明;但推荐使用运行时异常简化调用。
- 异常应携带上下文(如实体ID、操作类型),便于调用方日志记录。
6. 异步支持
异步接口设计需考虑调用方与实现方的线程模型解耦。推荐两种模式:
| 模式 | 返回类型 | 适用场景 | 风险 |
|---|---|---|---|
| Future模式 | CompletableFuture<T> | Java 8+,调用方可组合、超时控制 | 需要线程池管理,避免阻塞 |
| 响应式模式 | Mono<T> / Flux<T> | 高吞吐、背压场景 | 引入响应式框架,增加复杂度 |
// 接口定义示例
public interface UserRepository {
Optional findById(UserId id);
List findByStatus(Status status);
User save(User user);
void deleteById(UserId id);
// 异步变体
CompletableFuture> findByIdAsync(UserId id);
CompletableFuture> findByStatusAsync(Status status);
}
选型权衡: 如果团队熟悉响应式编程且项目有高并发要求,可使用
Mono/Flux;否则CompletableFuture风险更低,迁移成本小。接口应同时提供同步和异步版本,避免强制调用方改变编程模型。
7. 数据流与控制流
- 信任链路: 领域服务 → 仓储接口(契约) → 仓储实现(ORM/存储层)。接口规范确保调用方不越界访问实现细节。
- 数据流方向: 领域对象 → 接口参数 → 实现映射为数据源操作 → 返回领域对象。
- 控制流反转: 接口定义异常和异步类型,实现方必须适配,不可将底层异常透传。
风险点:
- 命名中混入数据库术语(如
findByColumnName)导致接口与表结构耦合。 - 参数使用了
Map或Object,丧失类型安全。 - 异常基类中泄漏实现细节(如
OptimisticLockException直接来自JPA)。 - 异步方法返回
Future但实现中调用get()阻塞当前线程,破坏异步意图。
8. 落地步骤与验收标准
| 步骤 | 具体任务 | 输出产物 |
|---|---|---|
| 1 | 定义仓储接口命名约定文档,所有开发同学Review | 命名规范.md |
| 2 | 定义领域异常基类及子类(至少3个:NotFound、Conflict、Generic) | RepositoryException.java |
| 3 | 选择异步返回类型(建议先从CompletableFuture开始) | 接口模板 |
| 4 | 编写第一个仓储接口(如UserRepository)并配套Mock测试 | 接口+单元测试 |
| 5 | 代码审查:检查接口中是否有基础设施依赖 | 审查checklist |
验收标准:
- 仓储接口中无任何
import来自javax.persistence、org.hibernate、org.springframework.data等基础设施包。 - 所有方法返回值不含
null,集合返回不可变。 - 异常全部继承自
RepositoryException,且不在方法签名中抛出基础设施异常。 - 单元测试可使用Mockito完全Mock接口,无需启动数据库。
9. 选型权衡总结
| 维度 | 推荐策略 | 替代方案 | 风险对比 |
|---|---|---|---|
| 方法命名 | find/save/delete 前缀 | get/insert/remove | get暗示必有值,易导致误用 |
| 参数类型 | 领域值对象 | 原始类型或String | 原始类型丢失语义,参数字段变更需改接口 |
| 返回类型 | Optional / 不可变集合 | null / 可变集合 | null引发NPE,可变集合导致调用方误改 |
| 异常 | 运行时异常 + 领域基类 | 检查型异常 / 无异常 | 检查型异常强制调用方处理,可能泄露实现细节 |
| 异步 | CompletableFuture | 响应式框架 | 响应式学习成本高,调试困难 |
通过严格定义仓储接口规范,团队可以建立清晰的信任链路:领域层只依赖契约,基础设施层只负责实现契约,双方通过接口签名隔离。这为后续替换存储技术(如从MySQL切换到DynamoDB)提供了安全的边界。