模块化项目结构设计原则

模块化听起来高大上,说白了就是把一个大工程拆成多个独立的小模块。我在早期做项目时,其实还踩过不少坑——项目越做越大,一个模块里塞了几十个功能,最后想改个地方连编译都要等十分钟。嗯,从那以后我对模块化结构就格外上心。

今天我们不聊虚的,直接讲我在实战中总结的几条设计原则。它们不是官方的标准,但确实帮我解决了很多实际问题。

原则一:单一职责 —— 一个模块只做一件事

这条原则源自 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 类都不允许被其他模块引用。

注意:千万别用 @VisibleForTesting 来暴露内部方法。我曾经见过一个项目因为这种注释太多,导致模块边界完全失效。测试其实可以通过接口 mock 来做,不需要打破封装。

原则五:模块间通信 —— 采用总线或依赖注入

模块之间怎么交流?我推荐三种方式:

  • 接口 + 依赖注入(如 Hilt):最标准,适合有明确依赖的场景。
  • 事件总线(如 EventBus、Flow SharedFlow):适合一对多的广播场景,比如某个模块登录成功后通知其他模块刷新状态。
  • 路由框架(如 ARouter、Jetpack Navigation):适合页面跳转和跨模块服务调用。

我个人更倾向接口 + 依赖注入,因为它可追溯、类型安全。事件总线我会谨慎使用,因为它会让调用链变得隐晦,用多了代码会难以调试。

原则六:模块分层 —— 建立清晰的依赖方向

一个健康的模块化项目,依赖关系应该是单向的,不能形成循环依赖。我通常会这样分层:

层级 包含内容 依赖方向
App 壳层 Application 类、全局初始化、依赖注入配置 依赖所有业务和基础模块
业务模块 登录、首页、购物车、我的等 可以互相依赖(通过接口),但必须单向
基础模块 网络、数据库、图片加载、工具类 不能依赖任何业务模块
公共层 公共 UI 组件、公共数据模型、Router 路由协议 被所有模块依赖,自身不依赖任何模块

记得我之前做过一个项目,业务模块之间互相引用,导致改了一个模块的接口,五个模块都报错。后面我用依赖检查工具(比如 Gradle 的 buildScan 结合自定义 lint)强制约束了依赖方向,循环依赖问题就再没出现过。

一张图看懂模块化结构设计原则

下面这张图汇总了模块化设计的核心分层和依赖关系,你可以把它贴在项目文档里当参考。

模块化项目结构设计原则 - 分层与依赖 App 壳层 登录模块 首页模块 购物车模块 网络模块 数据库模块 图片加载模块 公共层(UI组件/数据模型/Router)

结语

模块化结构设计没有银弹,但上面这几条原则确实帮我避开了不少坑。你回头看看自己的项目,如果发现某个模块「什么都管」,或者模块之间互相依赖乱成麻花,不妨试试按这些原则重构一下。记住:好的模块化结构,会让你以后每次改需求时都想给当初的自己点个赞。