Kotlin 默认可见性为 public,是不是一个好的设计?

语言: CN / TW / HK

theme: smartblue highlight: a11y-dark


本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

众所周知,Kotlin 的默认可见性为 public,而这会带来一定的问题。比如最常见的,library 中的代码被无意中声明为 public 的了,导致用户使用者可以用到我们不想暴露的 API ,这样违背了最小知识原则,也不利于我们后续的变更

那么既然有这些问题,为什么 Kotlin 的默认可见性还被设计成这样呢?又该怎么解决这些问题?

为什么默认为 public

其实在 Kotlin M13 版本之前,Kotlin 的默认可见性是 internal 的,在 M13 版本之后才改成了 public

那么为什么会做这个修改呢?官方是这样说的

In real Java code bases (where public/private decisions are taken explicitly), public occurs a lot more often than private (2.5 to 5 times more often in the code bases that we examined, including Kotlin compiler and IntelliJ IDEA). This means that we’d make people write public all over the place to implement their designs, that would make Kotlin a lot more ceremonial, and we’d lose some of the precious ground won from Java in terms of brevity. In our experience explicit public breaks the flow of many DSLs and very often — of primary constructors. So we decided to use it by default to keep our code clean.

总得来说,官方认为在实际的生产环境中,public 发生的频率要比 private 要高的多,比如在 Kotlin 编译器和 InterlliJ 中是 2.5 倍到 5 倍的差距

这意味着如果默认的不是 public 的话,用户需要到处手动添加 public,会增加不少模板代码,并且会失去简洁性

但是官方这个回答似乎有点问题,我们要对比的是 internal 与 public,而不是 private 与 public

因此也有不少人提出了质疑

反方观点

包括 JakeWharton 在内的很多人对这一改变了提出了质疑,下面我们一起来看下loganj的观点

internal 是安全的默认值

如果一个类或成员最初具有错误的可见性,那么提高可见性要比降低可见性容易得多。也就是说,将 internal 类或成员更改为 public 不需要做什么额外的工作,因为没有外部调用者

在执行相反的操作的成本则很高,如果初始时是 public 的,你要将它修改为 internal 的,就要做很多的兼容工作。

因此,将 internal 设为默认值可以随着代码库的发展而节省大量工作。

分析使用的数据存在缺陷

官方提到 public 发生的频率是 private 的 2.5 倍到 5 倍,但这是建立在有瑕疵的数据上的

由于 Java 提供的可见性选项不足,开发人员被迫两害相权取其轻。更有经验的开发人员倾向于通过命名约定和文档来解决这个问题。经验不足的开发人员往往会直接将可见性设置为 public。

因此,大多数 Java 代码库的 public 类和成员比其作者需要或想要的要多得多。我们不能简单地查看 Java 可见性修饰符在普通代码库中的使用并假设它反映了作者的意愿

例如,我们常用的 Okhttp ,由经验丰富的 Java 开发人员编写的代码库,尽管 Java 存在限制,但他们仍努力将可见性降至最低。

下面是 Okhttp 的 public 包,它们旨在构成 Okhttp 的 API

这里是它的 internal 包,理想情况下只能在模块中被看到。

简单计算可以看到大根有 46% 的公共方法和 71% 的公共类。这已经比一般的代码库好很多,这是我们应该鼓励的方向。

但是 internal 包内部的类根本不应该被公开!而这是因为 Java 的可见性限制引起的(没有模块内可见)

如果 Java 有 Kotlin 的可见性修饰符,我们应该期望接近 24% 的公共方法和 35% 的 public 类。此外,48% 的方法和 65% 的类将是 internal 的!

internal 的潜力被浪费了

在 Java 中,别无选择,只能通过 public 来实现模块内可见,并使用约定和文档来阻止它们的使用。Kotlin 的 internal 可见性修复了 Java 中的这个缺陷,但是选择 public 作为默认可见性忽略了这个重要的修正。

默认 public 会浪费 Kotlin 内部可见性的潜力。它一反常态地鼓励了 Java 实际上不鼓励的不良做法,当 Kotlin 有办法向前迈出一大步时,这样做是从 Java 倒退了一大步。

正方观点

对于一些质疑的观点,官方也做了一些回应

我们曾经将 internal 设置为默认可见性,只是它没有被编译器检查,所以它被像 public 一样被使用。然后我们尝试打开检查,并意识到我们需要在代码中添加很多 public。在应用(Application)代码,而不是库(library)代码中,常常包括很多 public。我们分析了很多 case,结果发现并不是模块边界布局边界不清晰造成的。模块的划分是完全合乎逻辑的,但仍然有很多类由于到处都是 public 关键字而变得非常丑陋。

在主构造函数和基于委托属性的 DSL 中这个情况尤其严重:每个属性都承受着 public 一遍又一遍地重复的视觉负担

因此,我们意识到类的成员在默认情况下必须与类本身一样可见。请注意,如果一个类是内部的,那么它的公共成员实际上也是内部的。所以,我们有两个选择:

默认可见性是公开的 或者类具有与其成员不同的默认可见性。

在后一种情况下,函数的默认值会根据它是在顶层还是在类中声明而改变。我们决定保持一致,因此将默认可见性设置为了 public.

对于库作者,可以通过 lint 规则和 IDE 检查,以确保所有 public 的声明在代码中都是显式的。这会给库代码开发者带来一定的成本,但比起不一致的默认可见性,或者在应用代码中添加大量 public,这似乎并不是一个问题,总得来说优点大于缺点。

如何解决默认可见性的问题

总得来说,双方的观点各有各的道理,不过从 M13 到现在已经很多年了,Kotlin 的可见性一直默认是 public,看样子 Kotlin 官方已经下了结论

那么我们该如何解决库代码默认可见性为 public,导致用户使用者可以用到我们不想暴露的 API 的问题呢?

Kotlin 官方也提供了一个插件供我们使用:binary-compatibility-validator

这个插件可以 dump 出所有的 public API,将代码与 dump 出来的 api 进行对比,可以避免暴露不必要的 api

应用插件

kotlin plugins { id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.12.1" }

应用插件很简单,只要在 build.gradle 中添加以上代码就好了

插件任务

该插件包括两个任务

  • apiDump: 构建项目并将其公共 API 转储到项目 api 子文件夹中。API 以人类可读的格式转储。如果 API 转储文件已经存在,它将会被覆盖。
  • apiCheck: 构建项目并检查项目的公共 API 是否与项目 api 子文件夹中的声明相同。如果不同则抛出异常

工作流

我们可以通过以下工作流,确保 library 模块不会无意中暴露 public api

准备阶段(一次性工作):

  • 应用插件,配置它并执行 apiDump ,导出项目 public api
  • 手动验证您的公共 API (即执行 apiCheck 任务)。
  • 提交项目的 api (即 .api 文件) 到您的 VCS。

常规工作流程

  • 后续提交代码时,都会构建项目,并将项目的 API 与 .api 文件声明的 api 进行对比,如果两者不同,则 check 任务会失败
  • 如果是代码问题,则将可见性修改为 internal 或者 private,再重新提交代码
  • 如果的确应该添加新的 public api,则通过 apiDump 更新 .api 文件,并重新提交

与 CI 集成

常规工作流程中,每次提交代码都应该检查 api 是否发生变化,这主要是通过 CI 实现的

以 Github Action 为例,每次提交代码时都会触发检查,如果检查不通过会抛出以下异常

总结

本文主要介绍了为什么 Kotlin 的默认可见性是 public,及其优缺点。同时在这种情况下,我们该如何解决 library 代码容易无意中被声明为 public ,导致用户使用者可以用到我们不想暴露的 API 的问题

如果本文对你有所帮助,欢迎点赞~

示例项目

https://github.com/RicardoJiang/android-workflow