My Profile Photo

Jesse


Hi, 我是 Jesse,一名 iOS 开发者,热爱编程,平常也喜欢摄影。欢迎大家能多多交流。


访问控制与模块边界:internal、public、package、open 的设计取舍

访问控制与模块边界:internal、public、package、open 的设计取舍

在 Swift 或 Kotlin 等现代语言开发中,访问控制(Access Control) 绝不仅仅是“保护变量”那么简单,它实际上是架构设计的边界线

很多开发者在写代码时,习惯性地一把梭全用 public,或者干脆默认不写。但当你开始维护一个大型项目、编写跨团队使用的 SDK,或者尝试组件化时,你会发现这四个关键字:internalpublicpackageopen,承载了完全不同的设计哲学。

今天我想和大家聊聊,在模块化开发中,我们该如何在这几个权限之间做取舍。


1. 默认的舒适区:internal

在 Swift 中,如果你什么都不写,默认就是 internal

  • 设计初衷: 模块内可见。它假设在一个 Target(比如一个 App 模块或一个 Framework)内部,所有代码都是“自己人”,互相之间高度信任。
  • 使用场景: 绝大多数的业务逻辑、UI 实现细节、内部辅助类。
  • 取舍逻辑: 既然是模块内,就不需要担心外部调用带来的破坏。它是敏捷开发的基石,让你在模块内部重构时,不需要修改任何外部接口。

2. 契约的起点:public

一旦你将某个属性或类标记为 public,你就签署了一份法律契约

  • 设计初衷: 跨模块访问,但禁止继承或重写。
  • 使用场景: SDK 的核心 API、工具类的静态方法、对外暴露的模型定义。
  • 核心痛点: * 脆弱性: 一旦公开,后续的修改必须考虑向前兼容。
    • 限制性: 外部模块虽然能看到这个类,但没法继承它(在 Swift 中)。这是一种保护机制,防止外部开发者通过继承改变你设计的核心行为。

3. 精细化的产物:package

这是 Swift 5.9 引入的一个非常实用的层级,填补了 internalpublic 之间的巨大鸿沟。

  • 设计初衷: 解决“大组件”内部多个 Target 协作的问题。
  • 使用场景: 当你的项目采用了高度组件化,一个功能被拆分成 AuthCoreAuthUIAuthAPI 等多个模块,但它们又都属于“身份认证”这个大包(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)

在实践中,我建议遵循 “最小特权原则”

  1. 从 private/internal 开始: 所有代码默认不公开。
  2. 按需提升: 只有当另一个模块确实需要调用时,才考虑提升至 packagepublic
  3. 谨慎 open: 除非你正在写一个像 UIKit 这样的基础框架,否则 public final 往往比 open 更安全。

好的访问控制设计,就像是一个精密的手表外壳。它只露给你调整时间所需的旋钮(Public API),而将复杂的齿轮啮合(Internal Logic)深藏在表壳之内。这不仅是为了安全,更是为了让我们在面对不断变化的需求时,依然拥有灵活调整内部实现的能力。


目录