第3章 分层架构中的ViewModel位置:UI层与领域层之间的中介
3.1 边界定义
在经典的分层架构中,ViewModel 明确位于 UI 层与领域层之间,承担着“中介”角色。它不属于 UI 层(不持有任何视图引用),也不属于领域层(不包含业务规则),而是独立存在于所谓的“表示逻辑层”或“应用层”。这个边界的核心原则是:
- UI 层只与 ViewModel 交互,绝不直接访问领域层。
- ViewModel 只与领域层(通过接口或抽象)交互,不依赖 UI 具体实现。
- 领域层对上层完全无知,不引用 UI 或 ViewModel 的任何类型。
边界定义公式:
UI → ViewModel (接口/抽象) → 领域层 (接口) → 领域实现
反向依赖箭头从不允许出现。
UI → ViewModel (接口/抽象) → 领域层 (接口) → 领域实现
反向依赖箭头从不允许出现。
3.2 分层策略
推荐采用 四层结构(Presentation / Application / Domain / Infrastructure),其中 ViewModel 归属于 Application 层顶部,与 Use Case / Service 同层但职责不同。
| 层次 | 包含内容 | 依赖方向 |
|---|---|---|
| Presentation(UI) | Activity / Fragment / ViewController / Compose 屏幕 | 依赖 Application 层(ViewModel 接口) |
| Application(ViewModel) | ViewModel 实现、状态容器、Use Case 编排 | 依赖 Domain 层(Repository 接口) |
| Domain | 实体、值对象、领域服务、Repository 接口 | 无依赖(纯净业务规则) |
| Infrastructure | 数据库、网络、文件系统实现 | 实现 Domain 接口,不反向依赖 |
3.3 组件职责
- UI 层:仅负责渲染状态、收集用户事件并转发给 ViewModel。不执行任何数据转换或业务判断。
- ViewModel:管理屏幕级状态(State)、处理用户动作(Event)、调用领域层获取/提交数据、执行轻量级表示逻辑(如格式化、合并数据)。
- 领域层:封装核心业务规则、实体行为、数据完整性校验。完全不关心状态如何展示。
关键判定标准: 如果在 ViewModel 中发现了“订单总价不能为负数”这类规则,说明领域逻辑泄漏到了 ViewModel。此类规则应下移到领域实体或领域服务中。
3.4 数据流 / 控制流 / 信任链路
分层架构通过强制单向依赖来建立信任链路:
- 数据流(下行):领域层 → ViewModel → UI。ViewModel 从领域层获取数据,转换成 UI 可观察的状态流(如 StateFlow / LiveData)。
- 控制流(上行):UI → ViewModel → 领域层。用户点击事件通过 ViewModel 转换为领域命令(Command)或查询(Query)。
- 信任链路:UI 信任 ViewModel 提供的数据格式正确;ViewModel 信任领域层返回的数据已通过业务规则校验;领域层信任基础设施提供的数据持久化能力。每一层只信任直接下一层,不跨层信任。
3.5 选型权衡
| 优势 | 代价 |
|---|---|
| UI 与领域完全解耦,可独立替换/测试 | 每屏需要额外 ViewModel 类,项目文件增多 |
| 领域层保持纯净,便于复用和单元测试 | 简单 CRUD 场景可能过度抽象 |
| 状态集中管理,生命周期安全 | 需要依赖注入框架管理 ViewModel 及其依赖 |
| 团队可以并行开发(UI 开发用 mock ViewModel) | 新手容易将领域逻辑误放入 ViewModel |
3.6 风险点
- ViewModel 膨胀: 状态和事件过多导致单个 ViewModel 代码量过大。应当拆分为多个专注的 ViewModel 或使用协程/Flow 进行组合。
- 领域逻辑泄漏: 将实体方法直接复制到 ViewModel 中。团队需通过 Code Review 和静态分析约束。
- 过度分层: 对于极简单的列表-详情场景,强制使用 ViewModel 反而增加复杂性。建议在架构规范中允许“快捷路径”(如直接使用 Repository + LiveData),但要声明为例外并明确重构时机。
常见反模式: ViewModel 中调用 Infrastructure 层(如直接访问 OkHttp 或 Room)。正确的做法是 ViewModel → Domain 接口 → Infrastructure 实现。跳过领域层将导致接口不稳定,且无法切换数据源。
3.7 落地步骤
- 定义领域层接口: 在 Domain 模块中声明 Repository 接口和 UseCase 类。
- 创建 ViewModel 类: 在 Application 模块中编写 ViewModel,通过构造函数注入 Repository 接口。
- 注册依赖: 在 DI 容器(如 Hilt / Koin)中将 ViewModel 与 UI 绑定,并实现 Repository 接口与 Infrastructure 实现的映射。
- 实现数据流管道: ViewModel 使用 Flow / LiveData 暴露状态,UI 层通过 collect / observe 订阅。
- 编写单元测试: 对 ViewModel 注入 mock Repository,验证状态转换正确性。
- 集成测试: 检查 UI 层是否通过 ViewModel 间接访问领域层,确保无直接依赖。
3.8 验收标准
| 验收项 | 检查方法 |
|---|---|
| UI 层无领域层引用 | 查看 UI 模块的 import,不应包含 Domain 实体类或 Repository 接口(仅可引用 ViewModel 接口/抽象) |
| ViewModel 不包含领域规则 | 检查 ViewModel 方法中无 if-else 业务判断;所有业务规则应委托给领域服务或实体 |
| 领域层可独立运行(无运行时依赖上层) | 使用单纯 JUnit 测试领域实体,不需加载 Android 框架或 DI 容器 |
| 替换 UI 技术栈不影响领域层 | 从 Compose 切换到 View System 只需重写 UI 层和 ViewModel 的适配代码,领域层完全不变 |
| ViewModel 可被单独测试 | 不启动 Activity,通过 mock Repository 即可执行 ViewModel 的单元测试 |