4. 模块化设计原则:Feature模块与Base模块的拆分、如何划分功能粒度

模块化这件事,说起来简单,做起来全是细节。我个人习惯把应用拆成三层:Base、Shared、Feature。为什么这么分?说白了就是为了让每个模块只做一件事,改代码时不用拖家带口。

4.1 为什么需要模块化?

我记得最早接手一个单体项目,所有代码都在一个module里。业务逻辑和通用工具混在一起,改个网络请求版本号,结果导航栏样式也跟着崩了。后来我花了两个周末重构,把公共代码抽成Base模块,网络、数据库、UI组件各放一块。从那以后,改基础设施从来没影响过业务逻辑。

  • 隔离变化:底层库升级时只影响Base模块,Feature模块不用动。
  • 并行开发:团队可以同时开发不同Feature,不用抢一个module。
  • 按需交付:SplitInstall本身就是按模块动态下发,没有模块化,动态交付就是空中楼阁。

4.2 Base模块的核心职责

Base模块是地基,它不关心任何业务。你想想看,一个网络请求库需要知道用户是登录还是注册吗?不需要。Base模块只负责提供底层能力。

模块包含内容示例
base-core基础工具类、常量、扩展函数DateUtils、StringExt、AppConfig
base-network网络框架封装、拦截器、序列化Retrofit + OkHttp 封装
base-ui通用UI组件、主题、资源BaseActivity、LoadingButton、色值
base-data数据库、本地存储、Repository基类RoomDatabase 封装

我在项目中踩过的一个坑:把“用户信息管理”也放进Base模块。后来发现用户信息上游依赖了Feature模块中的登录逻辑,导致循环依赖。正确做法是:Base模块完全不依赖任何Feature,只依赖第三方库和Android SDK。

4.3 Feature模块的划分粒度

Feature模块按业务功能拆。但粒度怎么定?太粗等于没拆,太细导致模块爆炸。我建议遵循两个原则:

  1. 单一业务闭环:一个Feature模块应该覆盖一个完整的用户故事。比如“登录注册”作为一个Feature,“商品详情”作为一个Feature。
  2. 独立交付能力:这个模块可以不依赖其他Feature就能单独跑起来。嗯,这里要注意:Feature之间不要直接依赖,通过接口层(Shared模块)沟通。

举个例子,电商App的Feature划分:

  • feature-auth:登录、注册、找回密码
  • feature-home:首页推荐、搜索入口
  • feature-detail:商品详情、规格选择
  • feature-cart:购物车管理
  • feature-order:订单列表、订单详情

每个Feature都有自己的数据源、ViewModel、页面。它们通过Shared模块暴露的接口互相调用。比如“加入购物车”这个操作,Feature-detail会调用Shared中定义的CartService接口,具体实现由Feature-cart提供。

4.4 实战中的拆分示例

假设我们要做一个新闻App。从SplitInstall的角度,新闻列表、详情、搜索是完全独立的动态交付单元。我们这样建模块:

// settings.gradle
include ':app'
include ':base-core'
include ':base-network'
include ':base-data'
include ':shared'
include ':feature-news-list'
include ':feature-news-detail'
include ':feature-search'

每个Feature的build.gradle中依赖Base和Shared:

dependencies {
    implementation project(':base-core')
    implementation project(':base-network')
    implementation project(':base-data')
    implementation project(':shared')
    // 不依赖其他Feature!
}

Shared模块里定义接口和公共数据模型:

// shared模块中的接口
interface NewsRepository {
    fun getNewsDetail(id: String): Flow<NewsDetail>
}

新闻详情模块实现这个接口,然后通过依赖注入注册。其他模块只需要调用接口,不用关心实现。这种模式我在项目里用了很久,确实干净。

小技巧:用@AutoService或Hilt的@Binds来自动注册接口实现,避免手动在App壳中配置。

4.5 确定粒度的最佳实践

我曾经在划分粒度时犯过一个错误:把“收藏”和“点赞”拆成两个Feature。结果发现它们共用同一个数据源和UI组件,反而增加了模块间通信的开销。后来我把它们合并成“feature-favorite”,包括收藏、点赞、历史记录等类似操作。

如何判断粒度是否合适?问自己几个问题:

  • 这个模块是否能够独立完成一个用户可感知的任务?(能 → 保留,不能 → 合并)
  • 拆开后会不会导致大量跨模块接口调用?(是 → 考虑合并,否 → 可以拆)
  • 如果这个模块未来需要单独动态下发,是否有价值?(有 → 保留,无 → 合并到父模块)

我把这些原则总结成下面的结构图:

模块化分层结构与依赖关系 App 壳(入口) Feature-Netflix Feature-News Feature-Podcast Shared 公共层 Base-Core Base-Network Base-UI 依赖方向:App → Feature → Shared → Base(不允许回环) Feature之间不直接依赖,通过Shared层接口通信,支持SplitInstall独立动态交付 Base模块不依赖任何Feature,保证底层稳定性
注意:避免出现Feature模块直接依赖另一个Feature模块的情况。我曾经为了省事让订单模块直接引用购物车模块的类,结果后来购物车模块需要按需下发,订单模块就编译不过了。用接口解耦是唯一的出路。

最后聊一句,如何判断当前项目是否需要模块化?如果团队超过3人,或者app功能超过10个页面,我建议就开始酝酿了。不一定要一次性拆完,可以从一个稳定的Base模块开始,再逐步把常用功能抽成Feature。这种渐进式的改进,团队也容易接受。