在 Swift 或 Kotlin 等现代语言开发中,访问控制(Access Control) 绝不仅仅是“保护变量”那么简单,它实际上是架构设计的边界线。
很多开发者在写代码时,习惯性地一把梭全用 public,或者干脆默认不写。但当你开始维护一个大型项目、编写跨团队使用的 SDK,或者尝试组件化时,你会发现这四个关键字:internal、public、package、open,承载了完全不同的设计哲学。
今天我想和大家聊聊,在模块化开发中,我们该如何在这几个权限之间做取舍。
1. 默认的舒适区:internal
在 Swift 中,如果你什么都不写,默认就是 internal。
- 设计初衷: 模块内可见。它假设在一个 Target(比如一个 App 模块或一个 Framework)内部,所有代码都是“自己人”,互相之间高度信任。
- 使用场景: 绝大多数的业务逻辑、UI 实现细节、内部辅助类。
- 取舍逻辑: 既然是模块内,就不需要担心外部调用带来的破坏。它是敏捷开发的基石,让你在模块内部重构时,不需要修改任何外部接口。
2. 契约的起点:public
一旦你将某个属性或类标记为 public,你就签署了一份法律契约。
- 设计初衷: 跨模块访问,但禁止继承或重写。
- 使用场景: SDK 的核心 API、工具类的静态方法、对外暴露的模型定义。
- 核心痛点: * 脆弱性: 一旦公开,后续的修改必须考虑向前兼容。
- 限制性: 外部模块虽然能看到这个类,但没法继承它(在 Swift 中)。这是一种保护机制,防止外部开发者通过继承改变你设计的核心行为。
3. 精细化的产物:package
这是 Swift 5.9 引入的一个非常实用的层级,填补了 internal 和 public 之间的巨大鸿沟。
- 设计初衷: 解决“大组件”内部多个 Target 协作的问题。
- 使用场景: 当你的项目采用了高度组件化,一个功能被拆分成
AuthCore、AuthUI、AuthAPI等多个模块,但它们又都属于“身份认证”这个大包(Package)时。 - 取舍逻辑: * 如果你希望
AuthUI访问AuthCore的某些底层方法,但又不希望最终的 App 业务层看到这些方法,package是唯一的救星。- 它降低了 public 的泛滥,让真正的“对外接口”保持简洁。
4. 彻底的开放:open
这是权限等级中最高、也最危险的一级。
- 设计初衷: 允许跨模块继承(Subclassing)和重写(Overriding)。
- 使用场景: 基础框架类(如
BaseViewController)、插件式架构中的基类。 - 取舍逻辑: * 性能损耗:
open会引入更多的动态派发(Dynamic Dispatch),编译器很难对其进行去虚拟化(Devirtualization)优化。- 维护噩梦: 允许外部重写意味着你失去了对执行流的完全控制。
- 格言: “如果你没打算让别人继承它,请永远不要用 open。”
总结:如何选择?
为了方便大家记忆,我整理了一个简单的决策矩阵:
| 场景 | 推荐访问级别 | 关键考虑点 |
|---|---|---|
| App 内部业务逻辑 | internal |
保持简单,方便模块内重构 |
| 同一个 Package 下的多个模块通信 | package |
减少不必要的 public 暴露 |
| SDK 提供的功能接口 | public |
严格定义契约,防止外部恶意篡改逻辑 |
| 需要被外部模块扩展的框架基类 | open |
明确支持继承,需处理子类重写的副作用 |
我的建议:由内向外(Inside-Out)
在实践中,我建议遵循 “最小特权原则”。
- 从 private/internal 开始: 所有代码默认不公开。
- 按需提升: 只有当另一个模块确实需要调用时,才考虑提升至
package或public。 - 谨慎 open: 除非你正在写一个像
UIKit这样的基础框架,否则public final往往比open更安全。
好的访问控制设计,就像是一个精密的手表外壳。它只露给你调整时间所需的旋钮(Public API),而将复杂的齿轮啮合(Internal Logic)深藏在表壳之内。这不仅是为了安全,更是为了让我们在面对不断变化的需求时,依然拥有灵活调整内部实现的能力。