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模块按业务功能拆。但粒度怎么定?太粗等于没拆,太细导致模块爆炸。我建议遵循两个原则:
- 单一业务闭环:一个Feature模块应该覆盖一个完整的用户故事。比如“登录注册”作为一个Feature,“商品详情”作为一个Feature。
- 独立交付能力:这个模块可以不依赖其他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”,包括收藏、点赞、历史记录等类似操作。
如何判断粒度是否合适?问自己几个问题:
- 这个模块是否能够独立完成一个用户可感知的任务?(能 → 保留,不能 → 合并)
- 拆开后会不会导致大量跨模块接口调用?(是 → 考虑合并,否 → 可以拆)
- 如果这个模块未来需要单独动态下发,是否有价值?(有 → 保留,无 → 合并到父模块)
我把这些原则总结成下面的结构图:
最后聊一句,如何判断当前项目是否需要模块化?如果团队超过3人,或者app功能超过10个页面,我建议就开始酝酿了。不一定要一次性拆完,可以从一个稳定的Base模块开始,再逐步把常用功能抽成Feature。这种渐进式的改进,团队也容易接受。