模块边界定义与职责

模块边界是架构设计的核心骨架。边界不清晰时,调用链会无规则地穿越模块内部,导致主线程阻塞难以定位,ANR 治理沦为“打地鼠”游戏。本章从划分原则、契约机制、接口抽象、依赖倒置四个维度,构建可落地的模块边界体系,使其能承载 ANR 调用链的追踪与阻断需求。

模块边界与依赖倒置结构 上层调用域 上层调用方 Activity / Service 抽象契约域 接口抽象层 (边界契约) 实现模块域 业务实现模块 数据服务模块 基础设施模块 依赖倒置核心 高层与低层均依赖抽象, 边界清晰,调用链可追踪

4.1 模块划分原则

模块划分决定了 ANR 调用链的可追踪粒度。原则如下:

  • 单一职责:一个模块只承担一个业务或技术关注点,避免“上帝模块”拖慢主线程。
  • 按线程域分层:UI 模块、工作线程模块、后台任务模块严格分离,阻断阻塞传播。
  • 按变化速率切分:业务变化快的模块与基础设施稳定模块分属不同边界,降低回归风险。
  • 高内聚低耦合:模块内部高度相关,对外暴露最小依赖,减少调用链穿透深度。
实践准则:每个模块的对外可见类型不应超过 5 个;超过时说明边界过宽,需要拆分子模块。

4.2 边界契约

边界契约是模块间协作的“法律文件”。它定义了三要素:

契约要素说明ANR 关联
接口定义方法签名、参数类型、返回值、异常声明明确线程归属,标注 @MainThread / @WorkerThread
数据格式传输对象的字段、序列化方式、空值策略减少跨线程数据拷贝导致的卡顿
行为约束超时阈值、重试策略、幂等性、降级开关防止长时间等待触发 ANR
契约版本化:采用 @Contract(version = 1) 注解标记接口版本,变更时强制走兼容性评估,避免调用方未适配导致运行时异常。

4.3 接口抽象

接口抽象是隔离变化、阻断 ANR 传播的关键手段。核心设计原则:

  • 面向接口编程:调用方只依赖抽象,不依赖具体实现类。
  • 接口隔离:客户端不应依赖它不需要的方法;胖接口拆分为多个细粒度接口。
  • SPI / API 分离:服务提供接口(SPI)供实现方继承,应用编程接口(API)供调用方使用,两类接口分属不同包路径。
// 边界契约示例:异步任务执行器接口
public interface TaskExecutor {
    @WorkerThread
    void execute(@NonNull Runnable task,
                 @Nullable TaskCallback callback);

    boolean isCurrentThreadWorker();

    long getMaxExecutionTimeMs();
}
抽象泄漏陷阱:接口中包含具体实现类的依赖(如直接使用 OkHttp 的 Request 类型)会导致底层变更波及所有调用方。接口参数和返回值必须使用抽象类型或纯数据对象。

4.4 依赖倒置

依赖倒置原则(DIP)是模块边界解耦的底层保障。其核心含义是:

  • 高层模块不依赖低层模块:两者都依赖抽象接口。
  • 抽象不依赖细节:细节依赖抽象。

在 Android 架构中,这意味着 ViewModel 不直接依赖 OkHttp 或 Room,而是依赖 UserRepository 接口;具体实现通过依赖注入框架(如 Hilt)在模块初始化时提供。

// 依赖倒置:ViewModel 只依赖抽象
public class HomeViewModel extends ViewModel {
    private final UserRepository userRepo;

    @Inject
    public HomeViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;  // 运行时注入具体实现
    }
}
对 ANR 的价值:依赖倒置让主线程不再直接绑定耗时实现。替换或降级底层模块时,调用方无需修改,可快速切换为异步方案或熔断回退。

4.5 数据流 / 控制流 / 信任链路

模块边界内三种流的设计决定了 ANR 调用链的可观测性:

流类型定义边界策略
数据流跨模块传输的 DTO / VO / 事件使用不可变对象,限制数据体积,避免主线程序列化
控制流调用发起、回调、异步消息的传递方向明确标注线程归属,回调必须超时兜底
信任链路模块间安全信任假设与降级条件信任边界处设置校验、熔断、降级开关
实践建议:在模块边界设置统一的“流量仪表盘”——通过 AOP 切面记录每次跨边界调用的耗时、线程 ID、调用堆栈,为 ANR 调用链分析提供精准数据。

4.6 选型权衡

模块边界的具体实现需要根据项目阶段和团队规模做出选择:

  • 接口 vs 消息队列:同步接口适合低延迟、强一致场景;消息队列适合解耦、削峰场景,但增加异步追踪复杂度。
  • 编译期依赖 vs 运行时路由:编译期依赖(Java 接口)类型安全、重构成本低;运行时路由(ARouter / 事件总线)更灵活,但调用链隐式,不易追踪。
  • 集中式 DI 容器 vs 手动注入:大型项目推荐 Hilt / Dagger 统一管理依赖;小型项目手动注入更轻量,避免编译期开销。
过度设计警示:不要为每一个内部类都加抽象接口。边界只存在于模块之间;模块内部的类直接依赖即可,过早抽象会增加维护成本和调用链长度。

4.7 风险点

  1. 循环依赖:模块 A 依赖 B,B 依赖 A 导致编译失败或运行时栈溢出。必须通过引入抽象层或事件总线打破循环。
  2. 边界泄漏:实现类被错误地暴露给外部模块,导致调用方直接依赖具体实现,绕过契约。
  3. 抽象泄漏:接口中抛出底层异常(如 SQLException)或使用底层类型,导致调用方感知实现细节。
  4. 信任链断裂:跨模块调用时未做降级处理,底层故障直接导致上层 ANR。

4.8 落地步骤

步骤动作产出物
Step 1识别现有模块边界,绘制模块依赖图(推荐使用 ArchUnit 验证)模块依赖矩阵 + 反向依赖报告
Step 2定义边界契约:接口声明 + 超时 / 线程 / 数据格式约束契约定义文档 + 注解标记
Step 3抽取抽象接口,将跨模块调用替换为接口依赖抽象接口列表 + 依赖倒置改造 commit
Step 4实现依赖注入或服务注册,确保运行时实现可替换DI 模块配置 + 降级策略实现
Step 5添加边界测试(契约测试 + 线程归属检测)自动化测试套件 + 准入 gate

4.9 验收标准

  • 依赖方向验证:使用 ArchUnit 检查所有模块依赖方向与架构图一致,不存在反向依赖或循环依赖。
  • 接口稳定性:契约接口在 2 个迭代周期内无破坏性变更(新增方法不破坏兼容性)。
  • 边界测试通过率 100%:所有跨模块调用的契约测试、线程归属测试、超时熔断测试全部通过。
  • 调用链可追踪:在模块边界处打印跨模块调用摘要日志,可在 5 分钟内定位到具体模块及调用堆栈。
最终检查清单:(1)每个模块对外暴露类型 ≤ 5;
(2)跨模块引用均为接口类型,无直接类引用;
(3)所有跨模块调用都有超时降级方案;
(4)模块依赖图无环。