about 2 years ago

過去寫閒談軟體架構文章時,並沒有特別注意文章的長度,能寫多少就寫多少 (謎之音:實話是能掰多少是多少?) 在友人的提醒下,有些文章有點太長,不過,我只能說盡量,能找到合適的切割點且還要有適合的標題,有時候蠻難的。接下來想聊聊 library 和 framework 等 reusable components,目前想想可能會有點長,所以預計會拆成幾篇文章,這是第一篇。

在學習任何新程式語言,大多都用熟悉的語言習慣去看新語言有沒有類似的事物,自己也是如此,第一次擔任視窗程式設計的助教,也是我第一次認真學 C# 的時候,網路上有不少人說 C# 就是 Java,所以當時我也是以最熟悉的 Java 習慣去學 C#,在好的 model 設計基礎下,寫出來的程式其實並不會太差,但就是有那麼一點點格格不入的感覺,原因主要是沒有使用像是 eventdelegate 等 C# 的語言特性,及遵循 .Net 既有的 《Coding Conventions》。

大多數的語言社群或官方都會有一套該語言的 coding convention,例如 Java 官方提供 《Code Conventions for the Java Programming Language》,雖然這份 convention 已經沒有繼續維護了,但既有的 Java SE/EE framework 大都是以這份 convention 為主,所以自己在寫 Java 程式也是遵循這份 convention。公司或組織內部的 convention 大多也是以既有的基礎再做些調整,例如 Google 公開的 《Google Java Style Guide》。

Coding convention 會因為語言背後的哲學在嚴謹度上不大相同 (有的語言會管很多),相較 Java 與 .Net Framework 的《Framework Design Guidelines》 會發現 Java 的 convention 大多著重在格式上,像是空白、大括弧、換行等規則,對於 API 的命名則只有 method 以動詞開始、class 需是一個名詞盡量避免縮寫幾個簡單的規則,.Net 則有較清楚的 naming 建議,但整體來看,不論是官方或第三方函式庫,Java 和 Net 的 API 還算是相當一致的。

為什麼要提到 API naming style 呢?前幾天在 Facebook 上看到一段針對 Swift 2.2 轉換到 3.0 引起大量 API 變更的討論,因此我也抽空看了一下 WWDC 2016 的《Swift API Design Guidelines》,session 中出現了一個字 swifty,代表 API 的命名是合乎 Swift 風格與背後的哲學 (整段影片其實很有意思,有興趣的人可以看一下),看影片時腦中浮現了前陣子在天瓏翻閱《Fluent Python》的一段話:

One of the best qualities of Python is its consistency. After working with Python for a while, you are able to start making informed, correct guesses about features that are new to you. However, if you learned another object oriented language before Python, you may have found it strange to spell len(collection) instead of collection.len(). This apparent oddity is the tip of an iceberg which, when properly understood, is the key to everything we call Pythonic.

試想你正在替公司開發一個 Python 的函式庫,但卻用 JavaScript 或 Java 的 naming style 設計 API 而忽視 Python 背後哲學的慣例,這恐怕不會受到 Python 社群的青睞,因為 API 不夠 Pythonic!

設計 API 時考慮些什麼呢?對我來說穩定性是很重要的,Swift 當初推出時,既有的 Objective C 和 C API 採取不變動的原則,這個決定能讓大量 Objective C 的開發者快速上手 Swift,但老實說,混合的程式碼讀起來有種不協調感,久而久之會變成新語言的負擔,所以這次 Swift 2.2 到 3.0 以別名的方式,為大量既有的 API 加上了適合 Swift 的 API 命名,並提供 migration 工具幫助開發者轉換既有的程式碼,將開發者的負擔降到最小,不過還是引起不小的抱怨。

如果沒有像 Apple 有這麼大量的開發人力,改變 API 之外還能夠開發 migration 工具,那就盡量避免 API 的改變,如果真要改變,應該要是同時提供新舊 API,並將舊 API 宣告為捨棄,讓開發者能夠有充足的時間轉換到新 API,在多個版本後才將舊 API 移除,這在很多語言都有對應的標示方式,以 Java 來說就是在舊的 API 加上 @Deprecated,並以 JavaDoc 告知該換成哪個 API,話雖如此,API 一旦發布出去就收不回來了,這也是為什麼向下相容很辛苦,但微軟一直在做的事,辛苦了。

除了穩定性,一致性亦很重要,就如同剛剛引用《Fluent Python》的那段話中,一致性能讓開發者在使用一個新的 libaray 或新功能時,能用過去的習慣去猜測新 API 該如何使用,但這真的不是件容易的事情,畢竟網路上每位貢獻者,並不一定使用相同的 convention,甚至像剛剛提到 Java 和 .Net 有 coding convention,但在我過去的經驗還是會遇到一些不一致的地方,舉例來說,Java 在 AWT/Swing 的時期,蠻多 listener 的 method 是以過去分詞結尾:

public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);
}

public interface FocusListener extends EventListener {
    void focusGained(FocusEvent e);
    void focusLost(FocusEvent e);
}

但也有例外,像是 CaretListener 的 method 不是 caretUpdated,但對習慣這慣例的開發者來說還算能接受,因為 IDE 應該能在前面幾個字打完後,就能找到對的 method

public interface CaretListener extends EventListener {
    void caretUpdate(CaretEvent e);
}

比較讓我稍微花點時間適應的是 JavaFX,引入了 property binding 機制,造成一些使用上的習慣和 Swing 稍有不同。這些不大的變動,也許不算是太嚴重,或是因為新機制的導入造成大變動,都會需要開發者花時間適應,所以在設計 API 前,也許可以先想想想要的一致性是什麼?這期望的一致性最好能成為一份 API design guideline,讓組織的成員能遵循,這次 Swift 3.0 就有詳列一份《API Design Guidelines》,希望開發者在設計給 Swift 使用的 API 時能遵守。

整結來說,受到幾種語言的影響,我個人設計 API 時,除了合乎該語言的 convention、上述的穩定性及一致性外,大致還會注意幾點:

  • 語意清楚,雖然為了讓語意清楚,可能會讓名稱變長,但在 IDE 的協助下,即使名稱稍長,實際上不需要打這麼多字,所以語意比簡潔重要。
  • 相近的顆粒度,API 的抽象程度不應該差太多,或是說,該隱藏不必要的細節。
  • 簡要的文件,即便語意夠清楚,但還是有個文件說明用途、輸入、輸出和可能的例外。
  • 讓程式能像文章般閱讀,這要跟語言配合,像是 Objective C 需要替參數取兩個名字,一開始覺得很瑣碎,但習慣後,在讀 Objective C 程式時真的像在閱讀文章,可讀性高很多,若有合適的參數名稱,查詢 API 文件的頻率會比其他語言少很多;但不是每個語言都是如此,因此只能靠抽象,若抽象做得好,API 就能組成 domain specific language,讓程式,也能讓程式的可讀性變高。

因此,要設計好的 API 真的不容易。最後,引用 Martin Fowler 也引用的一段話做結束,因為命名真的是一件很難的事!

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

系列索引
上一篇《閒談軟體架構:Client Server
下一篇《閒談軟體架構:內部函式庫

← 閒談軟體架構:Client Server 欣賞程式語言有限的選擇 →