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