4. 分层架构中的 Repository 定位:领域层、持久化层、应用层间的职责隔离
4.1 业务边界与分层动机
在经典分层架构(应用层 → 领域层 → 持久化层)中,Repository 承担着“领域对象仓储”的语义,而非“数据访问封装”。其核心目的是将领域模型的生命周期管理与底层存储细节彻底分离,使领域层完全不感知数据库、ORM 或任何持久化技术。这种隔离为领域逻辑的可测试性、可演化性提供了基础。
| 层次 | 职责 | 依赖方向 | 关注点 |
|---|---|---|---|
| 应用层 | 编排业务用例、控制事务、调用领域服务 | 依赖领域层接口(Repository、Service) | 请求/响应 DTO、用例流程、权限 |
| 领域层 | 定义核心业务逻辑、实体、值对象、聚合、Repository 接口 | 零依赖(除领域原语) | 业务规则、不变条件、领域事件 |
| 持久化层 | 实现 Repository 接口,完成数据库读写、ORM 映射 | 依赖领域层接口,可依赖基础设施(数据库驱动) | SQL/NoSQL 优化、连接管理、性能 |
关键原则:领域层中的 Repository 接口仅描述“存入”和“取出”聚合的行为,不出现任何持久化框架特有注解、接口继承或连接对象。持久化层实现必须遵守该接口契约,但可以将 ORM 细节完全封装在实现类内部。
4.2 数据流、控制流与信任链路
理解分层间的控制流向是正确解耦的前提:
- 控制流(依赖方向):应用层 → 领域层接口(编译时);持久化层 → 领域层接口(编译时)。反转了传统的“上层调用下层”,即依赖倒置原则(DIP)。
- 数据流(运行时):应用层调用领域层接口获得聚合根;领域层返回纯领域对象;持久化层在实现中完成对象 ↔ 表记录的转换。
- 信任链路:领域层是“可信核心”,它不信任任何外部注入的实现;持久化层必须履行领域层定义的契约,但领域层不检查持久化层的内部行为。
4.3 选型权衡:接口风格与实现策略
在确立分层边界后,Repository 接口的设计方式直接影响耦合度与团队维护成本:
| 接口风格 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 集合式 findById/save/delete |
语义贴近领域,易测试 | 复杂查询需规范命名 | 聚合根较小,CQRS 不频繁 |
| 仓储式 getBySpecification(Spec) |
查询灵活,避免接口膨胀 | 需要 Specification 模式支持 | 大量组合查询、过滤条件多 |
| 分类式 IOrderRepository / IUserRepository |
按聚合划分清晰 | 每个聚合增加一个接口文件 | 大多数分层架构默认选择 |
常见反模式:在领域层 Repository 接口中暴露
Pageable、Sort、Criteria 等持久化框架专有对象。这些类一旦出现在领域层,隔离便形同虚设。正确做法是将排序、分页等需求表示为领域层自身的值对象或闭包,由持久化层在实现时转换。
4.4 风险点与控制措施
- 泄漏持久化关注点:领域层出现
@Table、@Column注解或继承JpaRepository—— 立即隔离,通过代码审查和依赖检查工具(如 JDepend)阻止。 - ORM 懒加载陷阱:在应用层或表现层尝试访问未初始化的延迟加载属性,导致
LazyInitializationException。应使用“关闭读写”或“在持久化层完成聚合加载”,可使用专用 DTO 或@Transactional边界。 - 过度抽象:为简单 CRUD 引入泛型
IRepository<T>导致领域语义丧失。建议为每个聚合定义独立的接口,即使方法签名相似。 - 事务边界模糊:应用层控制事务,持久化层不应直接开启事务。若持久化层自行提交回滚,会破坏应用层的原子性。使用依赖注入控制事务管理器。
4.5 落地步骤
- 抽取接口:为每个聚合根定义领域层 Repository 接口,方法签名使用领域语言(如
findByCustomerId(CustomerId)而非getByCondition)。 - 创建空实现:在持久化模块中编写实现类,注入数据源或 ORM 会话,完成对象映射。
- 依赖注入配置:确保应用层组合根(Composition Root)将持久化实现绑定到领域接口,且领域模块不引用持久化模块。
- 编写集成测试:针对持久化实现执行数据库测试,验证映射正确性;领域层 Repository 接口使用模拟对象进行单元测试。
- 审查层间引用:使用模块化工具(如 Java 的 module-info.java 或 Maven/Guard)强制领域层不依赖持久化层包。
4.6 验收标准
- 领域层不引用任何持久化框架(无 JPA、Hibernate、MyBatis 包导入)。
- Repository 接口方法签名不含
Session、Connection、Pageable等类型。 - 持久化层实现类的编译时只依赖领域接口和数据访问框架,不依赖应用层。
- 替换持久化方案(如从 JPA 换为 MyBatis)时,仅需改动持久化层实现类,领域层和应用层无需修改。
- 所有对聚合的访问均通过 Repository 接口,不出现直接使用 DAO 或原生 SQL 的情况(性能补救场景除外,但需文档说明并走架构例外流程)。
核心度量:在单元测试中,领域层业务逻辑与持久化实现完全解耦。继承自持久化层的 mock 接口应能在 5 分钟内完成替换,而不影响任何领域测试用例的通过率。