状态隔离的本质:模块解耦、数据自治、副作用管控
在跨模块协作中,状态隔离是最底层、最关键的架构约束。它要求每个模块的 ViewModel 持有自己的状态副本,并通过明确定义的契约与其他模块通信,从而在编译期防止状态泄漏、在运行期控制副作用蔓延。本章从五个维度拆解这一本质,为后续章节的边界策略奠定基础。
1. 边界定义:模块解耦的物理与逻辑边界
解耦不是消除依赖,而是将依赖限制在接口层面。状态隔离要求模块之间只通过 纯数据通道(如通道、回调、总线)交换信息,绝不共享 ViewModel 内部状态引用。物理边界体现在独立编译单元,逻辑边界体现在状态所有者唯一。
| 边界类型 | 描述 | 示例 |
|---|---|---|
| 物理边界 | 模块以独立库或服务形式存在,不内聚其他模块的状态类 | A模块无法import B模块的ViewModel |
| 逻辑边界 | 数据所有权清晰,禁止跨模块直接修改状态 | 模块B只能通过A暴露的接口读取状态快照 |
| 运行时边界 | 状态容器生命周期与模块绑定,模块销毁时状态自动释放 | ViewModel的onCleared不会泄漏到外部 |
关键原则:任何状态的写操作必须发生在所属模块的内部;读操作必须通过无副作用的查询接口,且返回不可变快照。
2. 分层策略:职责与信任关系
跨模块 ViewModel 边界通常分为三层:
- 接口层(边界契约):定义模块对外提供的状态查询命令、事件订阅方法,不暴露内部实现。
- 协调层(跨模块调度):处理跨模块事件路由、状态聚合,但不持有其他模块的状态副本。
- 自洽层(本地状态管理):模块内部的 ViewModel + UseCase + Repository 组合,完全控制自己的状态与副作用。
这三层形成严格的 信任链路:上层只能通过下层公开的接口获取数据,下层永远信任上层传递的命令,但不确定上层如何使用结果。
3. 组件职责:各司其职,不越界
| 组件 | 职责 | 隔离规则 |
|---|---|---|
| ViewModel | 管理界面状态、收集用户事件、调度业务逻辑 | 不持有其他模块的ViewModel引用 |
| UseCase / Interactor | 编排数据流,调用多个Repository | 不直接访问UI层;返回结果不可变 |
| Repository | 提供数据源统一访问,封装本地/远程存储 | 不暴露内部缓存的引用,仅提供Snapshot |
| 跨模块桥接器 | 转换模块A的事件为模块B的接口调用 | 无状态,纯函数式映射 |
实践检查:如果模块A需要修改模块B的ViewModel中的字段,说明边界已被破坏。正确的做法是模块B暴露一个“命令接口”,由模块A调用,然后模块B自行更新状态。
4. 数据流 / 控制流 / 信任链路
数据流从产生到消费严格遵循“向下传递、向上反馈”的链式结构:
- 数据流方向:用户事件 → 所属ViewModel → 内部UseCase → Repository → 数据源。跨模块数据交互通过桥接器以事件或异步流形式流动。
- 控制流方向:命令从外部模块 → 桥接器 → 目标模块接口层 → 内部处理,控制权始终在接收模块。
- 信任链路:模块A信任模块B提供的接口签名,但不信任其内部实现细节;模块B信任模块A传递的命令参数,但不对命令来源进行身份校验(仅在架构层面信任)。
常见风险:跨模块直接读写状态会导致“隐式依赖”——模块A修改B的状态后,B的行为依赖A的时序,一旦A改动,B可能出现非预期状态。必须通过事件和不可变数据来切断这种隐式依赖。
5. 选型权衡
不同隔离策略各有利弊,下表对比三种主流方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 接口隔离(依赖倒置) | 编译期强约束,易测试 | 接口膨胀,维护成本高 | 大型项目,需要严格契约 |
| 事件总线 / 消息中间件 | 弱耦合,新增监听方便 | 调试困难,时序不可控 | 跨模块通知类场景(如登出、主题切换) |
| 共享状态工厂 | 集中管理跨模块共享数据 | 违背自治原则,引入单点风险 | 极小规模模块组,且只有只读共享 |
6. 风险点
- 循环依赖:模块A需要模块B的状态,模块B又需要模块A的状态,形成逻辑死锁。规避方法:始终保持依赖方向为“上层→下层”单向。
- 状态不一致:多个模块缓存同一实体,写入时序不一致导致脏数据。需要引入乐观锁或版本号机制。
- 副作用逃逸:ViewModel在onCleared后仍有匿名回调执行,修改已销毁模块的UI。必须清理所有跨模块订阅。
- 性能损耗:过度抽象导致每次跨模块调用都序列化/反序列化状态。需在热路径上提供批量查询或分页。
7. 落地步骤
- 定义模块边界图:画出现有模块的依赖关系,识别循环依赖和跨模块直接引用。
- 对每个模块提取接口层:创建纯数据契约(接口/协议),只暴露必要查询和命令。
- 引入跨模块桥接器:将原有跨模块直接调用替换为桥接器调用,桥接器内部完成类型转换与参数校验。
- 迁移状态到内部:检查每个ViewModel中是否有其他模块的状态引用,将其改为通过接口获取快照。
- 增加副作用管控:在桥接器出口和入口增加日志/监控,记录每次跨模块调用,便于调试。
- 编写验收测试:模拟模块销毁、重建场景,验证状态不泄漏。
8. 验收标准
| 验收项 | 通过条件 | 验证方式 |
|---|---|---|
| 无跨模块状态引用 | 代码扫描显示模块A未直接引用模块B内部类 | 依赖分析工具 |
| 数据一致性 | 同时修改同一实体时,最终状态与预期一致 | 并发测试用例 |
| 副作用受控 | 模块卸载后无残留任务或回调 | 泄漏检测(LeakCanary等) |
| 信任链路明确 | 所有跨模块调用都经过桥接器或接口层 | 架构规则检查(ArchUnit) |
本章核心:状态隔离的本质不是消除所有跨模块交互,而是将交互严格限定在“无状态、无副作用”的桥接契约中,让每个模块成为自身状态的主人。模块解耦、数据自治、副作用管控三者相辅相成,是判定架构是否健康的三大黄金指标。