3. 领域层Repository接口设计:接口职责、方法签名、查询与命令分离、缓存标注
3.1 接口职责
领域层Repository接口是聚合根在领域模型中的持久化边界,它的核心职责是将领域对象存储与基础设施实现完全解耦。接口只描述“能做什么”,而不暴露“怎么做”。具体职责包括:
- 数据访问抽象:定义对聚合根的增删改查操作,但不暴露数据库、ORM、缓存等具体技术。
- 领域对象生命周期管理:确保从持久化存储中恢复的聚合根处于完整、一致的状态。
- 事务与一致性边界:为命令方法隐式界定事务范围,一个方法通常对应一个工作单元。
- 缓存策略标注:通过注解或元数据指明哪些查询结果可被缓存,且缓存失效规则由领域层控制。
关键原则:Repository接口中的方法签名必须使用领域层类型(聚合根、值对象、标识符),严禁出现ORM实体、DTO或基础设施类型。
3.2 方法签名设计
方法签名应当遵循“以领域语言命名、以领域类型传参”的准则。常见模式如下:
| 方法类别 | 方法签名示例 | 说明 |
|---|---|---|
| 查询 | |
返回领域对象或Optional;无副作用;可标注缓存 |
| 命令 | |
修改持久化状态;返回void或标识符;通常包裹在事务中 |
| 批量 | |
批量操作需谨慎定义事务边界 |
命名规范:查询方法使用findBy/queryBy/getBy前缀;命令方法使用save/create/delete/update前缀。避免使用search/fetch等模糊动词。
3.3 查询与命令分离(CQS)
Repository接口应当显式遵循命令-查询分离原则:
- 查询方法:只读、无副作用、幂等。返回对象或集合,不允许修改传入参数或外部状态。
- 命令方法:有副作用(写入数据库/发送事件),返回void或标识符。必须保障事务完整。
分离后的接口结构清晰,便于单元测试(查询可mock、命令可验证)。在跨微服务场景下,查询方法可独立启用只读数据库副本或缓存,命令方法强制走主库。
3.4 缓存标注
为支持高性能查询,可在接口方法上添加缓存标注。标注形式可以是注解、接口默认方法或独立的元数据配置。示例如下:
- 注解方式:
@Cacheable(key = "#id")标记在findById方法上,声明缓存区域和键表达式。 - 缓存区域划分:按聚合根类型或查询维度定义不同缓存区域(如
order_cache、customer_cache)。 - 失效策略:命令方法(如
save、delete)应通过@CacheEvict标注,自动清除相关缓存条目。
注意:缓存标注只适用于查询方法,绝不可标注在命令方法上(会导致缓存写穿透或脏读)。此外,缓存标注的语义由基础设施层实现,领域层只负责“声明”策略。
3.5 接口设计的内联SVG:查询/命令与缓存标注关系
3.6 选型权衡与风险点
| 决策点 | 推荐方案 | 替代方案 | 风险说明 |
|---|---|---|---|
| 接口粒度 | 每个聚合根一个独立Repository接口 | 通用泛型CrudRepository<T,ID> | 泛型接口容易混入不适用方法,破坏领域语义 |
| 同步/异步 | 命令方法同步;查询方法可选异步 | 全部异步 | 命令异步可能导致事务管理复杂化;异步查询需注意线程安全 |
| 返回类型 | 查询返回Optional或领域对象,不返回DTO | 返回DTO或Map | 返回DTO会泄露基础设施细节,破坏依赖方向 |
| 事务控制 | 命令方法内部隐式开启事务 | 由调用方(应用服务)显式控制 | 事务扩散到领域层会耦合基础设施,推荐在应用层切面控制 |
风险点1:缓存标注与事务的冲突 — 如果命令方法在事务内执行了
风险点2:过度抽象导致方法膨胀 — 如果为每个特殊查询都单独定义方法,Repository接口会迅速膨胀。应当优先使用规约模式(Specification)或查询对象(Query Object)来封装复杂查询条件,保持接口方法数量在10个以内。
@CacheEvict但事务回滚,缓存可能已提前清除,导致脏读。解决方案:将缓存清除放到事务提交后的回调中(如Spring的@TransactionalEventListener)。风险点2:过度抽象导致方法膨胀 — 如果为每个特殊查询都单独定义方法,Repository接口会迅速膨胀。应当优先使用规约模式(Specification)或查询对象(Query Object)来封装复杂查询条件,保持接口方法数量在10个以内。
3.7 落地步骤与验收标准
落地步骤
- 定义聚合根标识符:为每个聚合根创建值对象(如
OrderId),作为Repository方法的参数和返回类型。 - 声明接口方法:按CQS原则分别列出所需查询和命令方法,确保每个方法语义单一。
- 标注缓存注解:在频繁调用的查询方法上添加缓存标注,并指定缓存区域名称;在对应的命令方法上添加缓存失效标注。
- 编写接口文档:在方法上方使用JavaDoc(或等效文档)描述预期行为、是否幂等、缓存策略、事务边界。
- 建立单元测试桩:使用Mock框架为Repository接口创建测试替身,验证应用服务对接口的调用符合预期。
验收标准
- 所有Repository方法参数和返回值均为领域层类型,无基础设施依赖。
- 查询方法不产生任何写操作(通过测试拦截或静态分析验证)。
- 命令方法执行后,相关缓存条目被正确清除(集成测试验证)。
- 接口方法总数不超过15个(含继承方法),避免冗余。
- 更换ORM或数据库实现时,领域层Repository接口无需修改任何代码。
验证示例:假设将JPA实现替换为MyBatis,只需新增一个
MyBatisOrderRepository实现类,领域层中所有依赖OrderRepository接口的代码无需改动。该替换过程应在一小时内完成,且通过全部领域层测试。