区分 Android 模块的「内部公开接口」与「对外公开接口」
在多模块协同开发中,内部公开接口(模块间/团队内共享)和对外公开接口(第三方/跨业务线使用)的核心差异的是「访问范围、稳定性要求、管控粒度」—— 前者聚焦团队内协作效率,后者聚焦外部依赖稳定性。以下从「定义与核心差异」「代码层面区分方案」「工程管控与最佳实践」三部分展开,帮你清晰划分两类接口。
一、核心定义与差异对比
先明确两类接口的本质定位,再通过表格直观对比关键维度:
1. 定义
- 内部公开接口:仅允许「同一项目内的其他业务模块」调用(如电商 App 中「支付模块」给「订单模块」「购物车模块」暴露的接口),面向团队内协作,可适度灵活调整。
- 对外公开接口:允许「第三方项目、跨业务线模块」调用(如公司封装的「统计 SDK」对外提供的接口、App 给插件模块暴露的核心能力),面向外部依赖,需极强的稳定性和兼容性。
2. 核心差异对比
| 对比维度 | 内部公开接口 | 对外公开接口 |
|---|---|---|
| 访问范围 | 同一项目内的关联模块(团队内可见) | 第三方、跨业务线、外部项目(公开可见) |
| 稳定性要求 | 中等:允许小范围不兼容变更(需同步关联模块) | 极高:除非主版本升级,禁止任何不兼容变更 |
| 兼容周期 | 短(如 1 个迭代过渡期) | 长(如 3 个主版本+,需预留充足迁移时间) |
| 变更审批 | 团队内评审(模块负责人+关联模块对接人) | 跨团队审批(模块负责人+外部依赖方+架构师) |
| 文档要求 | 简化:仅需说明功能、参数,可依赖团队内沟通 | 详尽:需包含使用示例、异常处理、兼容历史、迁移指南 |
| 容错设计 | 可依赖内部约定(如参数格式、回调时机) | 必须强容错(参数校验、空安全、降级逻辑) |
| 废弃流程 | 简单:标记 @Deprecated 后,同步关联模块迁移即可删除 |
复杂:标记 @Deprecated 后,需保留至少 1-2 个主版本,提供替代方案+适配层 |
| 核心目标 | 提升团队内模块协作效率 | 保障外部依赖的稳定性和可用性 |
二、代码层面:3 种落地性极强的区分方案
推荐按「包结构隔离 + 访问修饰符 + 注解标记」的组合方案区分,兼顾「直观性」和「工具可检测性」,以下是具体实现:
1. 方案一:包结构强制隔离(最核心、最直观)
通过固定包名约定,明确划分接口归属,是所有区分方案的基础(团队内必须统一规范)。
核心原则:不同访问范围的接口,放在完全独立的包路径下,禁止交叉存放。
示例项目包结构(以「支付模块」为例):
com.xxx.pay // 支付模块根包
├── api/ // 对外公开接口(仅放对外暴露的核心能力)
│ ├── PaySdkApi.kt // 对外入口(第三方调用的唯一入口)
│ ├── dto/ // 对外数据模型(Parcelable/Serializable,兼容外部)
│ │ ├── OrderParam.kt
│ │ └── PayResult.kt
│ └── callback/ // 对外回调接口
│ └── PayCallback.kt
├── internal_api/ // 内部公开接口(仅项目内模块调用)
│ ├── InternalPayApi.kt // 内部入口(订单/购物车模块调用)
│ ├── dto/ // 内部数据模型(可使用项目内共用类型)
│ │ └── InternalPayConfig.kt
│ └── callback/
│ └── InternalPayListener.kt
└── internal/ // 模块私有代码(无公开接口,仅内部实现)
├── impl/ // 接口实现类(对外/内部接口的实现均放这里)
│ ├── PaySdkApiImpl.kt
│ └── InternalPayApiImpl.kt
└── utils/ // 内部工具类(不对外/不对内暴露)
关键约定:
- 对外接口固定放在
xxx.api包下,仅暴露「最小必要能力」(如支付模块对外仅暴露「创建支付」「查询支付结果」); - 内部接口固定放在
xxx.internal_api包下,可包含更多协作所需的细节能力(如支付模块给订单模块暴露「预扣库存」「取消支付」的内部逻辑); - 模块私有代码放在
xxx.internal包下(用internal/private修饰),禁止任何外部/内部模块调用。
2. 方案二:访问修饰符 + 注解标记(工具可检测)
包结构是「物理隔离」,访问修饰符和注解是「逻辑隔离」,配合使用可避免误调用,且支持自动化工具检测。
(1)访问修饰符选择
| 接口类型 | Kotlin 推荐修饰符 | Java 推荐修饰符 | 说明 |
|---|---|---|---|
| 内部公开接口 | internal(默认 public 需显式声明) |
public(但限制包访问,仅内部模块可见) |
Kotlin 的 internal 天生适合:仅对「同一模块集(module set)」可见,第三方无法访问;Java 需配合包结构+Lint 限制 |
| 对外公开接口 | public(显式声明,或默认不写) |
public |
必须公开访问,允许外部模块/第三方通过依赖引用 |
(2)自定义注解标记(便于工具识别)
定义 2 个自定义注解,明确标记接口类型,后续可通过 Lint/Gradle 插件自动化检测(如禁止外部模块调用内部接口):
// 1. 对外公开接口注解(标记后视为稳定契约)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class PublicApi(
val version: String = "", // 标记接口引入版本
val desc: String = "" // 简要描述功能
)
// 2. 内部公开接口注解(标记后仅团队内模块可用)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class InternalPublicApi(
val relatedModules: Array<String> = [], // 关联模块(如["订单模块", "购物车模块"])
val owner: String = "" // 接口负责人(便于协作沟通)
)
(3)代码示例(Kotlin)
// 对外公开接口:必须 public + @PublicApi + 放在 api 包下
package com.xxx.pay.api
@PublicApi(version = "1.0.0", desc = "对外支付核心接口,第三方可调用")
interface PaySdkApi {
// 对外方法:参数用稳定的 DTO,返回值支持 Result 封装异常
fun createPayOrder(param: OrderParam): Result<PayResult>
fun queryPayStatus(orderId: String): PayStatus
}
// 内部公开接口:internal + @InternalPublicApi + 放在 internal_api 包下
package com.xxx.pay.internal_api
@InternalPublicApi(relatedModules = ["订单模块", "购物车模块"], owner = "张三")
internal interface InternalPayApi {
// 内部方法:可使用项目内共用类型(如内部的 BaseResponse)
fun preDeductStock(orderId: String, amount: Int): BaseResponse<Boolean>
fun cancelPay(orderId: String): Boolean
}
// 实现类:统一放在 internal 包下,隐藏实现细节
package com.xxx.pay.internal.impl
class PaySdkApiImpl : PaySdkApi { ... }
class InternalPayApiImpl : InternalPayApi { ... }
3. 方案三:模块拆分(适用于大型项目)
如果两类接口的「稳定性要求、管控逻辑」差异极大,可直接将「对外公开接口」拆分到独立的「SDK 模块」,「内部公开接口」保留在原业务模块:
- 示例:
pay-sdk-module:仅包含对外公开接口(PaySdkApi)、数据模型、回调接口,编译成 AAR 供第三方使用;pay-business-module:包含内部公开接口(InternalPayApi)、核心业务逻辑、对pay-sdk-module的实现依赖,仅供项目内其他模块调用。
- 优势:物理隔离彻底,避免内部逻辑泄露到外部,且对外 SDK 可独立版本管理(如语义化版本),内部模块可灵活迭代。
三、工程管控:避免接口误用与越权访问
仅靠代码标记不够,需通过「工具检测 + 工程规范」强制管控两类接口的访问权限,避免「外部模块调用内部接口」「内部接口被误当作对外接口使用」。
1. 用 Lint 规则强制隔离(核心手段)
| 自定义 Lint 检查规则,针对两类接口的包名/注解做校验,编译阶段直接报错,从源头阻止误用: | 校验场景 | Lint 规则逻辑 | 报错提示示例 |
|---|---|---|---|
| 外部模块调用内部公开接口 | 检测到「非关联模块」引用 internal_api 包下的类/方法,或标记 @InternalPublicApi 的接口 |
「禁止外部模块调用内部公开接口,请使用 api 包下的对外接口」 | |
内部接口未加 internal 修饰 |
检测到 internal_api 包下的接口用了 public 修饰 |
「内部公开接口必须用 internal 修饰,避免外部访问」 | |
对外接口缺少 @PublicApi 注解 |
检测到 api 包下的接口未加 @PublicApi |
「对外公开接口必须标记 @PublicApi,明确稳定契约」 | |
| 对外接口使用内部依赖类型 | 检测到 @PublicApi 接口的参数/返回值用了 internal 包下的类型 |
「对外接口禁止使用内部类型,请使用 api/dto 下的公开模型」 |
2. 依赖管理:控制接口暴露范围
通过 build.gradle 配置,避免内部接口被意外暴露给外部:
// 对外 SDK 模块(pay-sdk-module):仅暴露 api 包下的接口
android {
// 隐藏内部实现类,仅对外暴露 api 包
defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}
}
// consumer-rules.pro(混淆规则:保留对外接口,隐藏实现)
-keep @com.xxx.annotation.PublicApi class * { *; }
-keep class com.xxx.pay.api.** { *; }
-dontwarn com.xxx.pay.internal.** // 内部包不对外暴露
// 内部业务模块(pay-business-module):仅允许项目内模块依赖
dependencies {
// 内部模块间依赖用 implementation(避免传递依赖)
implementation project(":common-module")
// 对外 SDK 依赖用 api(允许关联模块调用对外接口)
api project(":pay-sdk-module")
}
3. 版本管理与变更管控差异
两类接口的变更流程需区分对待,避免「内部变更影响外部」或「外部管控限制内部效率」:
| 接口类型 | 版本管理策略 | 变更流程 |
|---|---|---|
| 内部公开接口 | 跟随项目迭代版本(无需独立版本) | 1. 评估影响范围(仅关联模块);2. 团队内同步变更计划;3. 发布后通知关联模块迁移;4. 预留 1 个迭代过渡期即可删除废弃接口 |
| 对外公开接口 | 独立语义化版本(如 1.0.0 → 1.1.0 → 2.0.0) | 1. 严格评估外部依赖影响(通过工具扫描第三方使用者);2. 跨团队审批(含外部依赖方);3. 提前 1-2 个版本标记 @Deprecated 并提供替代方案;4. 主版本升级时才可删除废弃接口;5. 发布时附带详细变更日志和迁移文档 |
四、最佳实践:兼顾协作效率与稳定性
-
接口设计:对外「最小化暴露」,内部「适度灵活」
- 对外公开接口:仅暴露核心能力(如支付模块对外仅暴露「创建订单」「查询状态」),隐藏所有内部逻辑(如库存校验、渠道选择);
- 内部公开接口:可暴露更多协作所需的细节(如支付模块给订单模块暴露「预扣库存」「取消支付」),但避免暴露实现类(优先用接口+工厂模式)。
-
避免「内部接口对外泄露」
- 禁止对外接口调用内部公开接口(如
PaySdkApi不能依赖InternalPayApi),防止内部逻辑变更影响外部; - 对外接口的参数/返回值必须使用「独立的公开数据模型」(如
OrderParam),禁止复用内部模块的实体类(如InternalOrderInfo)。
- 禁止对外接口调用内部公开接口(如
-
文档与沟通:对外「详尽」,内部「简洁」
- 对外公开接口:用 Dokka/JavaDoc 生成完整文档,包含参数说明、异常类型、兼容版本、使用示例(甚至提供 Demo 项目);
- 内部公开接口:可简化文档,重点说明「参数约束、回调时机」,依赖团队内同步(如接口变更在团队会议中同步)。
-
定期审计:清理冗余接口
- 内部公开接口:每迭代结束后,清理未被使用的接口,避免接口膨胀;
- 对外公开接口:每主版本升级前,审计废弃接口(标记
@Deprecated超过 2 个版本的),统一清理。
总结
区分两类接口的核心逻辑是:用「包结构+注解+访问修饰符」做代码标记,用「Lint+依赖管理」做工程管控,用「不同的稳定性要求+变更流程」做生命周期管理。
- 对外公开接口:聚焦「稳定、兼容、最小暴露」,保障外部依赖不崩溃;
- 内部公开接口:聚焦「协作、效率、适度灵活」,提升团队内开发效率。
通过这套组合方案,既能避免两类接口混淆误用,又能平衡「灵活迭代」和「稳定可靠」的需求。