区分 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. 发布时附带详细变更日志和迁移文档

四、最佳实践:兼顾协作效率与稳定性

  1. 接口设计:对外「最小化暴露」,内部「适度灵活」

    • 对外公开接口:仅暴露核心能力(如支付模块对外仅暴露「创建订单」「查询状态」),隐藏所有内部逻辑(如库存校验、渠道选择);
    • 内部公开接口:可暴露更多协作所需的细节(如支付模块给订单模块暴露「预扣库存」「取消支付」),但避免暴露实现类(优先用接口+工厂模式)。
  2. 避免「内部接口对外泄露」

    • 禁止对外接口调用内部公开接口(如 PaySdkApi 不能依赖 InternalPayApi),防止内部逻辑变更影响外部;
    • 对外接口的参数/返回值必须使用「独立的公开数据模型」(如 OrderParam),禁止复用内部模块的实体类(如 InternalOrderInfo)。
  3. 文档与沟通:对外「详尽」,内部「简洁」

    • 对外公开接口:用 Dokka/JavaDoc 生成完整文档,包含参数说明、异常类型、兼容版本、使用示例(甚至提供 Demo 项目);
    • 内部公开接口:可简化文档,重点说明「参数约束、回调时机」,依赖团队内同步(如接口变更在团队会议中同步)。
  4. 定期审计:清理冗余接口

    • 内部公开接口:每迭代结束后,清理未被使用的接口,避免接口膨胀;
    • 对外公开接口:每主版本升级前,审计废弃接口(标记 @Deprecated 超过 2 个版本的),统一清理。

总结

区分两类接口的核心逻辑是:用「包结构+注解+访问修饰符」做代码标记,用「Lint+依赖管理」做工程管控,用「不同的稳定性要求+变更流程」做生命周期管理

  • 对外公开接口:聚焦「稳定、兼容、最小暴露」,保障外部依赖不崩溃;
  • 内部公开接口:聚焦「协作、效率、适度灵活」,提升团队内开发效率。

通过这套组合方案,既能避免两类接口混淆误用,又能平衡「灵活迭代」和「稳定可靠」的需求。