48、模块化优化:模块划分原则、模块依赖管理、模块编译优化、模块测试

模块化,说白了就是把一个大系统拆成若干小块。每个小块独立开发、独立测试、独立编译。我做了这么多年Android系统优化,发现很多性能问题其实根源不在代码本身,而在模块划分不合理。

你想想看,一个App如果所有代码都塞在一个模块里,改一行代码就得全量编译,测试也得全量回归。这效率能高吗?更可怕的是,模块之间耦合太紧,牵一发而动全身,改一个功能可能引发十几个模块的连锁问题。

嗯,今天我们就来聊聊模块化优化的四个核心维度:怎么划分、怎么管依赖、怎么加速编译、怎么高效测试。

模块划分原则

模块划分不是拍脑袋决定的。我个人习惯遵循几个基本原则:

  • 单一职责:一个模块只做一件事。比如网络请求模块就别掺和图片加载的逻辑。
  • 高内聚低耦合:模块内部要紧密,模块之间要松散。说白了就是内部随便改,对外接口尽量稳定。
  • 业务边界清晰:按业务领域划分,而不是按技术分层。比如“支付模块”、“用户模块”、“订单模块”,而不是“工具类模块”、“数据层模块”。
  • 复用性优先:如果一个功能可能被多个模块使用,那就单独抽成一个公共模块。

核心原则:模块划分的最终目标,是让每个模块可以独立演进、独立测试、独立部署。做不到这三点,你的模块化就是伪模块化。

我在项目中遇到过一种情况:团队把所有的工具类都塞进了一个叫“common”的模块里。结果这个模块越来越大,最后变成了一个“垃圾堆”。改一个工具类,所有依赖它的模块都得重新编译。这就是典型的划分失败案例。

模块依赖管理

依赖管理是模块化的命门。依赖关系乱了,整个项目就成了一团乱麻。

我建议你遵循几个原则:

  • 单向依赖:依赖方向要清晰,不能出现循环依赖。比如基础模块不能依赖业务模块。
  • 依赖倒置:高层模块不依赖低层模块,两者都依赖抽象接口。这样换实现的时候,只需要改一个模块。
  • 最小依赖原则:一个模块只依赖它真正需要的东西。别为了省事,把整个库都引进来。

举个例子,假设我们有三个模块:core(核心)、network(网络)、user(用户)。正确的依赖关系应该是:

// 正确的依赖关系
user --依赖--> network --依赖--> core
// 或者
user --依赖--> core
user --依赖--> network

千万别搞成:

// 错误的循环依赖
user --依赖--> network
network --依赖--> core
core --依赖--> user  // 这就出问题了

避坑指南:我曾经在一个大项目里发现了一个隐藏的循环依赖。两个模块互相引用对方的接口,编译时没问题,但运行时偶尔会崩溃。查了三天才找到原因。从那以后,我强制团队在CI流程里加了依赖检查脚本。

依赖管理的工具方面,我推荐使用Gradle的apiimplementation关键字。记住一个原则:

  • api:暴露给外部模块的依赖。比如你的模块对外提供了某个库的接口,那就用api
  • implementation:只在模块内部使用的依赖。外部模块不需要知道。这样可以减少编译时的依赖传递。

模块编译优化

编译速度是模块化最直接的收益点。但前提是你得配置得当。

我常用的几个优化手段:

  • 增量编译:只编译修改过的模块。Gradle默认支持,但需要你正确配置模块间的依赖关系。
  • 并行编译:多模块可以同时编译。在gradle.properties里设置org.gradle.parallel=true
  • 缓存编译结果:使用Gradle的构建缓存,避免重复编译。特别是CI环境,缓存能省下大量时间。
  • 按需编译:只编译你当前需要的模块。比如你在改user模块,就别让Gradle去编译payment模块。

小技巧:在Android Studio里,你可以用Build > Select Build Variant来指定只编译某个模块的debug版本。配合--configure-on-demand参数,编译速度能提升30%以上。

我记得有一次,项目从单模块拆成20多个模块后,全量编译时间从15分钟降到了3分钟。但增量编译时间反而变长了。为什么?因为模块间依赖关系没理清,改一个模块导致十几个模块都得重新编译。后来我们重新梳理了依赖,把公共接口抽出来,增量编译时间才降下来。

模块测试

模块化之后,测试策略也得跟着变。你不能还像以前那样,每次改完代码就跑全量测试。

我建议采用分层测试策略:

  • 单元测试:每个模块内部的功能测试。用JUnit + Mockito,测试粒度要细,覆盖核心逻辑。
  • 模块内集成测试:测试模块内部多个类的协作。比如测试网络模块的请求-响应流程。
  • 跨模块集成测试:测试模块间的接口是否正常工作。比如用户模块调用网络模块的场景。
  • 端到端测试:模拟真实用户操作,测试整个App的流程。这个可以放在CI的夜间构建里跑。

测试的配置也很重要。我习惯在每个模块的build.gradle里单独配置测试依赖:

// 模块的 build.gradle
dependencies {
    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.mockito:mockito-core:4.0.0'
    androidTestImplementation 'androidx.test:runner:1.4.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

这样做的好处是,每个模块的测试环境是独立的。你可以在模块级别运行测试,不用等整个项目编译完。

关键点:模块测试的核心是“隔离”。每个模块的测试只依赖它自己的代码和mock的外部依赖。不要因为测试一个模块,就得启动整个App。

我曾经踩过一个坑:某个模块的单元测试依赖了另一个模块的数据库操作。结果每次跑测试都得先初始化数据库,测试速度慢得离谱。后来我们把数据库操作mock掉,测试时间从5分钟降到了10秒。

模块化知识体系

下面这张图总结了模块化优化的核心逻辑,你可以对照着梳理自己的项目:

模块化优化核心逻辑 模块化优化 模块划分原则 依赖管理 编译优化 模块测试 单一职责 · 高内聚低耦合 业务边界清晰 · 复用性优先 单向依赖 · 依赖倒置 最小依赖 · api/implementation 增量编译 · 并行编译 缓存编译 · 按需编译 单元测试 · 模块内集成 跨模块集成 · 端到端测试 最终目标:独立演进 · 独立测试 · 独立部署 避免伪模块化,确保每个模块都有明确的边界和职责

模块化优化不是一蹴而就的事。它需要你在项目初期就做好规划,在开发过程中持续维护。但一旦做对了,收益是巨大的——编译更快、测试更稳、团队协作更顺畅。

嗯,今天就聊到这里。记住一句话:模块化不是为了炫技,而是为了让你的代码更健康、团队更高效。

专注资料整理