當初在設計
MLGame
專案時,為了能夠讓遊戲開發者和玩家能夠使用 MLGame 架構,於是嘗試查了關於 API
設計的概念。本篇整理一場 Google 的技術演講,演講標題為「How to Design a Good
API and Why it Matters」,演講雖然是在講 API
的設計概念,但我認為這些概念在平常撰寫程式上也很受用。演講以 Java
為舉例語言,文章中會引用演講中的例子,我也會盡量舉在專案中(以 Python
撰寫)遇到的情況。
- 投影片:http://fwdinnovations.net/whitepaper/APIDesign.pdf
- 演講錄影:https://www.youtube.com/watch?v=aAb7hSCtvGw(有 CC 字幕)
上篇整理演講前半段的部分,主要是 API 設計的通用概念。
為何 API 設計重要?
- 好的 API 可以是公司最重要的資產之一:客戶會使用 API 來製作產品,同時也會學習使用 API。而放棄一個 API 去學習另一個全新的 API 的成本太高,所以一個好的 API 會吸引客戶來使用
- 壞的 API 就會是公司的負債:客戶會佔滿客服線路。公司要修改 API 也會困難重重,造成技術負債
- 發布的 API 就發布了:修改 API 可能會造成客戶的程式崩潰。
為何 API 設計對你重要?
- 平常寫程式也是在做 API 設計:好程式是模組化的,而模組之間的界線就是 API。而且好的模組應該要能夠一再被重複使用
- 以 API 設計的方式去寫程式可以增進程式品質
好 API 的特徵
- 容易學習、使用:API 要好記憶、要合理、不會誤用,即使沒有文件也可以知道怎麼使用
- 使用 API 的程式要好讀寫容易維護
- 只做好它該做的事
- 容易擴展:會與 API 模組化的程度有關
- 適合對象使用者:像是用詞等
API 的設計過程
收集需求
- 收集到的需求應該是「使用情境」,也就是 API 要解決的「問題」,而不是解法。它會變成評估 API 是否解決問題的標準
- 將收集到的需求擴大為較通用的需求,會比較容易設計 API。太特定的功能,會讓 API 沒有那麼好用
以 MLGame
來說,它是一個「讓玩家可以用程式玩遊戲的平台,並且可以自由加入遊戲」。而不是「幫助遊戲分離控制的
I/O,讓玩家程式能夠與遊戲溝通」。
從一頁內的規格開始
- 快速完成重於完整性:如果一開始的規格短,就容易修改、彈性也高。可以避免牽一髮而動全身
- 給越多人看越好:重視他們的意見。如果得到認可,才去充實規格
MLGame 的設計是將遊戲程式與玩家程式執行在不同的 process
中,兩者間透過專屬的溝通 API
來傳輸場景資訊或是遊戲指令。當初嘗試畫的規格(不知道架構圖算不算規格):
及早且常使用 API
- 在你實作 API 之前開始,避免在實作後發現 API 不好用而丟棄。甚至是在規劃完整之前開始,避免寫規格時發現不好用而丟棄
- 在實作 API 的期間也要經常使用 API,避免在完成後在用的時候發現沒有解決問題。
- 可以透過寫例子(短短的程式碼)或單元測試來檢視。在實作 API 的過程中,這些程式也會變成範例程式
在實作 MLGame
的過程中,我先寫了兩個遊戲(單人跟多人遊戲)和對應的玩家程式來測試架構,後續的更新也一直使用這些遊戲來測試。在
MLGame
發布之後,這些遊戲就成為預設遊戲,而對應的玩家程式就成為玩家撰寫玩遊戲程式的樣板,讓玩家剛下載
MLGame 時就有遊戲可以玩。
不過要在實作 API 之前就先嘗試使用 API
對我來說有點困難,雖然有先假想使用流程(如下方的假想程式碼),但我是在快速實現
API 的功能後,也就是先不管程式好不好看,只要呼叫 API
可以動就好了,用到程式上才明確感受到好不好用。
class Game:
def game_loop(self):
api.wait_ml_ready()
while True:
scene_info = self.get_scene_info()
api.send_to_ml(scene_info)
wait_fps()
command = api.recv_from_ml()
self.update_the_game()
記得實際的期望
- 大部分的 API 設計過度約束:與其迎合每一個使用者的需求,不如找出通用的需求,公平的得罪每一個使用者
- 預期 API 會出錯:多年的使用會使這些錯誤被消除,所以期望能夠一直改進 API
真的在實際給人用之後,才會發現有些部份有問題、有些部份需要改進。
API 設計通則
API 應該只做一件事,而且作到好
- API 的功能應該要能簡單描述:API 會與使用者「對話」。如果很難為 API 命名,那就是個壞現象
- 好的命名,驅動好的程式開發
- 如果模組太大,就合適地分割;如果不同模組內作了相似的事情,就融合或提取這些功能
API 越小越好,但不要釋出後縮小
- API 應該滿足需求就好
- API 釋出就釋出了:可以擴展 API,但不要移除 API,因為已經有人在用了,移除 API 會讓使用者的程式壞掉
雖然建議釋出後不要移除,不過現在會搭配棄用警告(Deprecation
Warning),告知 API 即將被移除,但也不會立刻造成程式壞掉。
- API 的概念比起 API 的體積(提供的類別、函式等)重要,會影響學習的難度:讓 API 可以作更多事,但使用者學的更少。
例如用來溝通的模組提供 IPC 跟 TCP
兩種方式,可能建構子的參數不一樣,但是用來傳接訊息的函式都叫 send
與
recv。如此一來使用者轉換溝通方式,只需要確認設定參數,而不太需要更動程式其他部份。
API 的實作不該影響 API
- 不要讓實作細節洩漏到 API 上:會混淆使用者,也會限制改變實作的空間
例如 API 回傳的 exception 應該要有抽象層(為 API 制定一組它的
exception),而不是直接回傳內部產生的 exception。像是溝通模組的 exception
應該有自己的
CommunicationError,如果回傳實作的
exception,像是 BrokenPipeError(IPC
溝通方式),此時使用者切換到另一種溝通方式,卻得到
TimeoutError(TCP
溝通方式),使用者會混亂一陣子。此外,如果要再新增一種溝通方式,可能讓使用者需要多處理一種
exception,相當不方便。
- 注意什麼為實作細節,不要過度命名函式
例如一個儲存名字到資料庫的函式命名,用
save_to_db 比
save_hash_to_db
還要好,一是後者跟使用者說是怎麼儲存的,二是如果要換一種儲存方式,前者有較自由的變動空間。
盡量縮小每樣東西的存取性
- 盡可能的隱藏資訊(information hiding):public 類別應該沒有 public member,除了常數,而是透過 setter 與 getter 來存取 class member
- 減少模組織間的連結(coupling):不要讓模組直接存取其他模組的 member。這樣模組就可以個別開發、測試、優化
命名很重要,API 是一個小語言
- 使用他的人也會使用你定的名稱溝通,包含你
- API 名稱必須能自我解釋:能夠理解用法,也能增加程式的可讀性,讓程式碼讀起來像文章
- 相同的概念使用相同的字詞:remove vs delete,除非他們在 API 中有差別,不然擇一使用
- 盡可能對稱:動詞有 add 與 remove,名詞有 entry 與 key,如果用動詞 + 名詞的格式,如:addKey、removeEntry,那其餘的 API 命名也應該要一致
在 MLGame 中,遊戲端就稱為 game,玩家端就稱為 ml,像是執行兩者的控制類別就稱為 GameExecutor、MLExecutor,而通訊類別就是 GameCommManager、MLCommManager。
文件也很重要
- 有好的設計還不夠,沒有好的文件就不會被一再使用
- 虔誠地寫文件:從類別、界面、函式、建構子、引數到例外,為 API 的每個部份寫文件
例如:是類別就講它的 instance
代表什麼,是函式就說會有什麼樣的前提、效果或是副作用,是引數就提示型別、單位(byte
還是 bit、秒還是毫秒)、傳進去的物件的擁有者會不會轉移
- 謹慎地為物件有幾種狀態(state space)寫文件
例如:溝通物件會有連線中跟關閉連線兩種狀態,關閉狀態下,recv
跟 send
就不能被呼叫。
考慮決定 API 設計的效能後果
- 壞的決定限制 API 的效能: 不需使用 mutable 型別卻還是用了、提供建構子而不是 factory、使用實作的類別而不是介面
- 不要為了效能而扭曲 API:好的 API 伴隨好的效能
API 必須與平台(或使用語言)和平共存
- 使用平台習慣的用法:遵守命名習慣、避免過時的型別、模仿語言與其核心 API 的模式、利用對 API 友善的功能、避免陷阱
- 不要直接複製 API 到其他平台:而是退一步去想 API 本身提供的功能,而在目標平台上提供對應的 API。例如在 C++ 合理的名字,在 Java 上不一定合理
沒有留言:
張貼留言