模块化项目结构设计原则
模块化听起来高大上,说白了就是把一个大工程拆成多个独立的小模块。我在早期做项目时,其实还踩过不少坑——项目越做越大,一个模块里塞了几十个功能,最后想改个地方连编译都要等十分钟。嗯,从那以后我对模块化结构就格外上心。
今天我们不聊虚的,直接讲我在实战中总结的几条设计原则。它们不是官方的标准,但确实帮我解决了很多实际问题。
原则一:单一职责 —— 一个模块只做一件事
这条原则源自 SOLID 中的单一职责原则。在 Android 模块化里,它的意思很直接:一个模块应该只关注一个业务或一个技术能力。
举个例子,很多项目会把「登录」和「用户中心」放在同一个模块里。你觉得合理吗?我个人习惯是分开的:登录模块只负责认证交互和 管理,用户中心模块负责展示用户资料和设置。为什么?因为登录的逻辑可能会频繁变更(比如换第三方 SDK),而用户中心的 UI 变化又很频繁。耦合在一起,改一处就要重新编译整个模块,太痛苦了。
原则二:接口隔离 —— 依赖最小化
我见过最糟糕的模块结构,就是一个基础模块里面放了网络请求、图片加载、数据库、工具类……所有业务模块都依赖它。结果呢?我改了一个图片加载库的配置,所有业务模块都要重新编译一遍。
正确的做法应该是:只暴露必要的接口给外部调用。比如网络模块只对外提供 API 接口和返回的数据模型,具体的实现细节(OkHttp 配置、Retrofit 拦截器)完全封装在模块内部。
我曾经接手过一个项目,它的用户模块暴露了十几个 public 方法,但实际上只有两个被外部用到。这就是典型的接口设计过度。后面我重构时,把所有内部方法改成 internal,只保留必要接口,编译速度提升了 30%。
原则三:依赖反转 —— 让抽象掌控细节
模块之间应该依赖抽象,而不是具体实现。我用 Dagger Hilt 或者手动依赖注入时,会专门为每个模块定义接口层。
举个例子:购物车模块需要知道用户是否已登录。如果购物车模块直接依赖登录模块的具体类,那就耦合了。更合理的方式是:购物车模块定义一个 UserProvider 接口,然后由 app 层注入实现了这个接口的登录模块逻辑。
// 购物车模块 - 接口定义
interface UserProvider {
fun isLoggedIn(): Boolean
fun getUserId(): String
}
// app 层 - 注入实现
@Module
class UserProviderImpl @Inject constructor(
private val loginRepository: LoginRepository
) : UserProvider {
override fun isLoggedIn() = loginRepository.isLoggedIn()
override fun getUserId() = loginRepository.userId
}
这样一来,购物车模块根本不关心登录模块的内部实现,替换或测试都变得非常简单。
原则四:封装性 —— 把秘密藏在模块内部
每个模块都应该尽可能地隐藏实现细节。在 Android 里,我们可以用 internal 修饰符来限制可见性。
我通常会约定:模块对外暴露的类,必须加一个公共前缀或者放在一个专门的 api 包下。比如登录模块的对外接口类放在 com.example.login.api 包,实现类放在 com.example.login.internal 包。所有 internal 类都不允许被其他模块引用。
原则五:模块间通信 —— 采用总线或依赖注入
模块之间怎么交流?我推荐三种方式:
- 接口 + 依赖注入(如 Hilt):最标准,适合有明确依赖的场景。
- 事件总线(如 EventBus、Flow SharedFlow):适合一对多的广播场景,比如某个模块登录成功后通知其他模块刷新状态。
- 路由框架(如 ARouter、Jetpack Navigation):适合页面跳转和跨模块服务调用。
我个人更倾向接口 + 依赖注入,因为它可追溯、类型安全。事件总线我会谨慎使用,因为它会让调用链变得隐晦,用多了代码会难以调试。
原则六:模块分层 —— 建立清晰的依赖方向
一个健康的模块化项目,依赖关系应该是单向的,不能形成循环依赖。我通常会这样分层:
| 层级 | 包含内容 | 依赖方向 |
|---|---|---|
| App 壳层 | Application 类、全局初始化、依赖注入配置 | 依赖所有业务和基础模块 |
| 业务模块 | 登录、首页、购物车、我的等 | 可以互相依赖(通过接口),但必须单向 |
| 基础模块 | 网络、数据库、图片加载、工具类 | 不能依赖任何业务模块 |
| 公共层 | 公共 UI 组件、公共数据模型、Router 路由协议 | 被所有模块依赖,自身不依赖任何模块 |
记得我之前做过一个项目,业务模块之间互相引用,导致改了一个模块的接口,五个模块都报错。后面我用依赖检查工具(比如 Gradle 的 buildScan 结合自定义 lint)强制约束了依赖方向,循环依赖问题就再没出现过。
一张图看懂模块化结构设计原则
下面这张图汇总了模块化设计的核心分层和依赖关系,你可以把它贴在项目文档里当参考。
结语
模块化结构设计没有银弹,但上面这几条原则确实帮我避开了不少坑。你回头看看自己的项目,如果发现某个模块「什么都管」,或者模块之间互相依赖乱成麻花,不妨试试按这些原则重构一下。记住:好的模块化结构,会让你以后每次改需求时都想给当初的自己点个赞。