3. 领域层Repository接口设计:接口职责、方法签名、查询与命令分离、缓存标注

3.1 接口职责

领域层Repository接口是聚合根在领域模型中的持久化边界,它的核心职责是将领域对象存储与基础设施实现完全解耦。接口只描述“能做什么”,而不暴露“怎么做”。具体职责包括:

  • 数据访问抽象:定义对聚合根的增删改查操作,但不暴露数据库、ORM、缓存等具体技术。
  • 领域对象生命周期管理:确保从持久化存储中恢复的聚合根处于完整、一致的状态。
  • 事务与一致性边界:为命令方法隐式界定事务范围,一个方法通常对应一个工作单元。
  • 缓存策略标注:通过注解或元数据指明哪些查询结果可被缓存,且缓存失效规则由领域层控制。
关键原则:Repository接口中的方法签名必须使用领域层类型(聚合根、值对象、标识符),严禁出现ORM实体、DTO或基础设施类型。

3.2 方法签名设计

方法签名应当遵循“以领域语言命名、以领域类型传参”的准则。常见模式如下:

方法类别 方法签名示例 说明
查询
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId, Page page);
boolean existsByOrderNumber(OrderNumber number);
返回领域对象或Optional;无副作用;可标注缓存
命令
void save(Order order);
void delete(OrderId id);
OrderId create(Order order);
修改持久化状态;返回void或标识符;通常包裹在事务中
批量
void saveAll(List<Order> orders);
void deleteAll(List<OrderId> ids);
批量操作需谨慎定义事务边界
命名规范:查询方法使用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:查询/命令与缓存标注关系

Repository 接口设计:查询与命令分离 + 缓存标注 领域层 Repository<T> 接口 查询方法(只读·无副作用) • findById(id) → Optional<T> • findByXXX(...) → List<T> • existsBy...(...) → boolean • count(...) → long 可标注 @Cacheable / @CachePut 命令方法(有副作用·事务) • save(T aggregate) → void • delete(Id id) → void • saveAll(List<T>) → void • deleteAll(List<Id>) → void 必须标注 @CacheEvict 清除缓存 缓存标注只声明策略,具体实现(Redis/本地缓存)在基础设施层完成。 命令方法执行后必须由框架自动触发缓存失效,确保最终一致性。

3.6 选型权衡与风险点

决策点 推荐方案 替代方案 风险说明
接口粒度 每个聚合根一个独立Repository接口 通用泛型CrudRepository<T,ID> 泛型接口容易混入不适用方法,破坏领域语义
同步/异步 命令方法同步;查询方法可选异步 全部异步 命令异步可能导致事务管理复杂化;异步查询需注意线程安全
返回类型 查询返回Optional或领域对象,不返回DTO 返回DTO或Map 返回DTO会泄露基础设施细节,破坏依赖方向
事务控制 命令方法内部隐式开启事务 由调用方(应用服务)显式控制 事务扩散到领域层会耦合基础设施,推荐在应用层切面控制
风险点1:缓存标注与事务的冲突 — 如果命令方法在事务内执行了@CacheEvict但事务回滚,缓存可能已提前清除,导致脏读。解决方案:将缓存清除放到事务提交后的回调中(如Spring的@TransactionalEventListener)。
风险点2:过度抽象导致方法膨胀 — 如果为每个特殊查询都单独定义方法,Repository接口会迅速膨胀。应当优先使用规约模式(Specification)或查询对象(Query Object)来封装复杂查询条件,保持接口方法数量在10个以内。

3.7 落地步骤与验收标准

落地步骤

  1. 定义聚合根标识符:为每个聚合根创建值对象(如OrderId),作为Repository方法的参数和返回类型。
  2. 声明接口方法:按CQS原则分别列出所需查询和命令方法,确保每个方法语义单一。
  3. 标注缓存注解:在频繁调用的查询方法上添加缓存标注,并指定缓存区域名称;在对应的命令方法上添加缓存失效标注。
  4. 编写接口文档:在方法上方使用JavaDoc(或等效文档)描述预期行为、是否幂等、缓存策略、事务边界。
  5. 建立单元测试桩:使用Mock框架为Repository接口创建测试替身,验证应用服务对接口的调用符合预期。

验收标准

  • 所有Repository方法参数和返回值均为领域层类型,无基础设施依赖。
  • 查询方法不产生任何写操作(通过测试拦截或静态分析验证)。
  • 命令方法执行后,相关缓存条目被正确清除(集成测试验证)。
  • 接口方法总数不超过15个(含继承方法),避免冗余。
  • 更换ORM或数据库实现时,领域层Repository接口无需修改任何代码。
验证示例:假设将JPA实现替换为MyBatis,只需新增一个MyBatisOrderRepository实现类,领域层中所有依赖OrderRepository接口的代码无需改动。该替换过程应在一小时内完成,且通过全部领域层测试。