DAO、Repository、DataSource 三层边界定义:职责分离与协作关系

在 Room 持久化架构中,将数据访问逻辑拆分为 DataSource(数据源层)、DAO(数据访问对象层)与 Repository(仓储层)三个明确的边界,是控制复杂性与应对需求变化的核心手段。本章定义每一层的职责、边界契约与协作方式,为后续的落地实践奠定理论基础。

核心原则:每一层只关注一个抽象级,下层对上层无感知,上层通过接口依赖下层。信任链路自顶向下传递,控制流自底向上收敛。

2.1 三层职责与边界矩阵

层次 英文名称 核心职责 依赖方向 典型实现载体
DataSource 数据源层 封装原始数据源的连接、事务、表结构映射;不包含业务逻辑。 无下层依赖(底层) RoomDatabase、网络 API 客户端、本地文件
DAO 数据访问对象层 定义原子数据操作(CRUD),处理查询参数映射与结果集转换;不可感知外层业务。 依赖 DataSource(Room 数据库实例) @Dao 注解接口、Ktor 请求封装
Repository 仓储层 聚合多个 DAO / 数据源,编排完整业务单元(如「先插入缓存再返回远程」),对外暴露领域级接口。 依赖 DAO 接口(或抽象数据源) UserRepositoryImpl 类
关键认知:Repository 不直接操作数据库,DAO 不感知调用方身份,DataSource 不负责数据组合或缓存策略。每一层只拥有完成自身职责所需的最小知识集。

2.2 数据流与控制流的信任链路

数据流(实线)自 DataSource → DAO → Repository → ViewModel。控制流(虚线)反向,每次调用都携带清晰的边界约束:

  • DAO 层保证原子性:一次查询要么完全成功,要么回滚;
  • Repository 层保证业务完整性:跨 DAO 调用必须处理异常和一致性;
  • DataSource 层保证物理连接生命周期:由 Room 或网络库管理,上层不关心具体连接池。
三层边界定义:职责分离与协作关系 DataSource(数据源层) 管理物理连接、事务、表结构映射 DAO(数据访问对象层) 原子 CRUD,参数绑定,结果映射 Repository(仓储层) 跨数据源编排,暴露领域接口,缓存策略 数据流 控制流 信任链路约束 1. Repository 只能依赖 DAO 接口,不可依赖 DataSource 具体实现。 2. DAO 不处理缓存、 重试或业务编排。 3. DataSource 不感知 调用方身份,不实现 业务逻辑。 4. 跨层调用必须经过 下一层接口,禁止 跳过层级。

2.3 选型权衡:为什么必须是三层?

业界常见误区是将 DAO 与 Repository 合并为一个“大 DAO”层,或让 Repository 直接操纵 RoomDatabase。以下对比说明三层分离的必要性:

架构形态 优点 风险点 适用场景
双层(DAO + 业务层) 代码量少,快速原型 业务逻辑混入 DAO,单元测试困难,更换数据源需重写业务 小型工具类 App
三层(Datasource + DAO + Repository) 职责清晰,测试隔离,替换数据源只需改 DAO 实现 初期搭建成本略高,需设计泛型或接口抽象 中大型产品级 App
多层过度抽象 理论上模块化更强 显著增加认知负担,调试链路过长,开发效率下降 不推荐
风险提示:当业务方要求“一个查询增加排序字段”时,如果修改了 DAO 签名,将导致所有调用方(即使只是 Repository)重新编译。因此需提前约定 DAO 接口返回不可变集合或 Flow,并使用参数对象而非散列参数来隔离高频变动。

2.4 落地步骤

  1. 定义 DataSource 契约(如 RoomDatabase 抽象类)—— 只声明数据库表结构与版本迁移策略。
  2. 编写 DAO 接口 —— 每个实体对应一个 DAO,只包含基础的增删改查方法,尽量不使用 suspend 或 Flow 之外的协程上下文。
  3. 实现 Repository 类 —— 注入 DAO 接口,在方法内编排跨 DAO 逻辑或缓存策略。
  4. 测试验证 —— 使用 FakeDataSource 模拟 DAO 行为,验证 Repository 逻辑;使用 Room 的内存数据库验证 DAO 查询正确性。

2.5 验收标准

  • 每个 DAO 接口的方法能够独立通过单元测试,不依赖任何 Repository。
  • Repository 单元测试中,可以完全替换 DAO 为 Mock 或 Fake。
  • DataSource 层不包含任何业务相关的判断、循环或字符串拼接。
  • 修改数据库表结构(如新增字段)时,只需修改对应实体类 + DAO 方法,无需修改任何 Repository 或 ViewModel。