怎樣編寫好的 API?

本文首先闡述了 RESTful 風格 API 的基礎理論知識以及 Richardson 成熟度模型,隨後討論了好的 API 應該具有哪些特徵,最後對流行的 API 實現方式,即 GraphQL 和 RESTful,進行了對比。

現在,每個人都在關注 API。API 最早開始流行於大約 20 年前,2000 年,Roy Fielding 在他的博士論文中首次提出了 REST 這個術語。同年,Amazon、Salesforce 和 eBay 向全世界的開發者介紹了他們的 API,永遠改變了我們構建軟件的方式。

在 REST 之前,Roy Fielding 論文中的原則被稱爲 “HTTP 對象模型”,隨後你會明白這爲何非常重要。

隨着閱讀的深入,你還會看到如何確定你的 API 是否成熟,好 API 的主要品質是什麼以及爲何在構建 API 的時候,要注重適應性。

1RESTful 架構基礎

REST 代表表述性狀態轉移(Representational State Transfer),由 Roy Fielding 在他的博士論文中定義,長期以來,它就是服務 API 的聖盃。它並不是構建 API 的唯一方式,但是由於它的流行,即便是非開發人員也知道這種標準。

RESTful 軟件有如下六種特點:

但是,對日常使用來說,這過於理論化了。我們需要更具操作性的東西,這也就是 API 成熟度模型。

2Richardson 成熟度模型

該模型是由 Leonard Richardson 提出的,它將 RESTful 開發原則結合成四個簡單易行的步驟。

在模型中的位置越高,就越接近 Roy Fielding 所定義的 RESTful 原始理念。

 Level 0:POX(Plain Old XML)的泥沼

Level 0 的 API 是一組簡單 XML 或 JSON 的描述。在前文中,我曾經說過在 Fielding 的論文之前,RESTful 原則被稱爲 “HTTP 對象模型”。

這是因爲 HTTP 是 RESTful 開發中最重要的組成部分。REST 要儘可能多地使用 HTTP 固有屬性中的理念。

在 Level 0,沒有使用任何這樣的東西。我們只是構建自己的協議並把它作爲一個專有層。這種架構被稱爲遠程過程調用(Remote Procedure Call,RPC),適用於遠程過程 / 命令。

通常我們會有一個端點,可以對它進行調用以獲取一堆 XML。在這方面,一個典型的例子就是 SOAP 協議:

另外一個很好的例子就是 Slack API。它有些多樣化,有多個端點,但依然是 RPC 風格的 API。它暴露了 Slack 的各種功能,中間沒有附加任何特性。如下的代碼展示瞭如何向一個特定的通道發送消息:

雖然按照 Richardson 的模型,這是一個 Level 0 的 API,但是這並不意味着它是不好的。只要它是可用的,並且恰當地服務於業務需求,那它就是很棒的 API。

 Level 1:資源

爲了構建 Level 1 的 API,我們需要找出系統中的名詞並將它們通過不同的 URL 暴露出來,如下面的樣例所示:

其中,“/api/books” 能讓我訪問一個通用的圖書目錄,“/api/profile” 能夠讓我訪問這些書的作者的基本信息。爲了獲取某個資源的第一個特定實例,我可以在 URL 中添加 ID(或其他引用)。

在 URL 中還可以嵌套資源,這展示了它們是以層級結構的形式組織的。

回到 Slack 的樣例,如下展示了按照 Level 1 API,它們會是什麼樣子的:

現在,URL 發生了變化,從原先的 “/api/chat.postMessage” 變成了現在的“/api/channels/general/messages”。

信息中 “channel” 部分從請求體轉移到了 URL 中。從字面就能看出,通過使用這個 URL,我們可以預期有條消息發佈到了 general 通道上。

 Level 2:HTTP 動作

Level 2 利用 HTTP 動作(verb)來添加更多的含義和意圖。在這方面可用的動作比較多,我這裏只用到一個基礎的子集:PUT / DELETE / GET / POST。

藉助這些動作,我們可以預期包含它們的 URL 有不同的行爲:

以上面提到的 “/api/books” 爲例:

那這裏的 “安全” 和“冪等”又是什麼意思呢?

“安全” 的方法指的是永遠不會改變數據的方法。REST 建議 GET 方法只能用來獲取數據,所以在上面的集合中,它是唯一一個安全的方法。不管你調用多少次基於 REST 的 GET 方法,它永遠不會改變數據庫中的任何東西。但是,這並不是該動作的固有特性,而是關係到你該如何實現它,所以我們需要確保它是這樣運行的。所有其他的方法都會以不同的方式改變數據,不能隨意使用。在 REST 中,GET 方法既是安全的,又是冪等的。

“冪等” 的方法指的是多次使用不會產生不同結果的方法。按照 REST,DELETE 方法應該是冪等的,如果刪除了某個資源,然後針對相同的資源再次調用 DELETE,它不會改變任何東西。資源應該早就已經消失了。在 REST 規範中,POST 是唯一一個非冪等的方法,所以我們可以對相同的資源多次調用 POST 方法,這樣我們會得到重複的資源。

我們重新看一下 Slack 樣例,如果我們使用 HTTP 動作來進行更多的操作會是什麼樣子:

我們可以使用 POST 方法發送消息到通用的通道,我們也可以使用 GET 方法從通用通道獲取消息。我們還可以使用 DELETE 方法和特定的 ID 刪除消息,這裏比較有意思的一點在於,消息並不是與特定通道關聯的,所以我可以設計一個單獨的 API 來刪除資源。這個例子表明,設計 API 並不總是那麼簡單,這方面有很多可選項和權衡。

 Level 3:HATEOAS

還記得純文字、沒有任何圖像的電腦遊戲嗎?我們只能看到一些文本,描述了你在哪裏,以及接下來能幹什麼。爲了取得進展,我們必須要輸入自己的選擇。在一定程度上來講,HATEOAS 就是做這件事情的。

HATEOAS 指的是 “超媒體作爲應用狀態引擎(Hypermedia as the Engine of Application State)”。

有了 HATEOAS 之後,當其他人使用你的 API 的時候,他們就能看到通過 API 還能做哪些其他的事情。HATEOAS 回答了 “從這裏出發,我還能去哪裏?” 的問題。

但這還不是所有的內容。HATEOAS 還可以對數據關係進行建模。我們可能會有一個關於圖書的資源,並且在 URL 中沒有將作者信息嵌套進來,但是我們可以包含它們的鏈接,如果有人對作者感興趣的話,那麼他們可以訪問這些鏈接並探索相關的數據。

HATEOAS 不像其他成熟度模型的等級那樣流行,但是有些開發人員確實在使用它。其中一個樣例就是 Jira,如下是它們的搜索 API 的響應:

他們將鏈接嵌入到了其他我們可以探索的資源中,以及該 issue 的狀態過渡列表。

另外一個使用 HATEOAS 的樣例是 Artsy。他們的 API 嚴重依賴 HATEOAS,並且還使用了 JSON Plus 調用規範,按照該規範強制要求使用一種特殊的約定來構建鏈接。下面是一個分頁的例子,這是使用 HATEOAS 最酷的樣例之一:

我們可以提供到下一頁、上一頁、第一頁和最後一頁的鏈接,還可以按照需要添加其他頁面的鏈接。這樣簡化了 API 的消費,因爲這樣不需要在客戶端添加 URL 的解析邏輯,也不需要追加頁碼的方法。我們只需要在客戶端使用已經實現結構化的鏈接就可以了。

3 好的 API 由什麼組成

我們已經介紹完了 Richardson 模型,但這並不是實現好的 API 的全部內容。其他重要的品質還有什麼呢?

 錯誤 / 異常處理

我對自己使用的 API 的基本期望之一就是,需要有一種明確的方式來判斷是否有錯誤或異常。我想要知道請求是否得到了處理。

HTTP 有一種簡單的方式來實現這一點:HTTP 狀態碼。

管理狀態碼的基本規則是:

我們的 API 至少要提供 4xx 和 5xx 狀態碼。有時候,5xx 是自動生成的。例如,客戶端發送了一些內容到服務器端,但是這非法的請求,而我們的校驗是有缺陷的,從而導致這個問題繼續在代碼中執行了下去,最終導致出現了異常,這樣就會返回一個 5xx 的狀態碼。

如果你想要承諾使用特定的狀態碼,那麼你會遇到 “哪種狀態碼最適合當前情況?” 的問題。這樣的問題並不總是那麼容易回答,我推薦你去閱讀聲明這些狀態碼的 RFC,它們給出了比其他來源更廣泛的解釋,並且告訴了你何時使用這些狀態碼更合適等。幸運的是,網上有些資源可以幫助我們做出選擇,比如 Mozilla 的 HTTP 狀態碼指南。

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

 文檔

優秀的 API 必須要有優秀的文檔。在文檔方面,最大的問題在於,隨着 API 的發展需要找人同步更新文檔。有個更好的方案是不脫離代碼自更新文檔。

例如,註釋與代碼的脫節。當代碼發生變化的時候,註釋依然保持不變,這樣的話,註釋就過時了。這甚至會比根本就沒有任何註釋更糟糕,因爲在隨後的一段時間內,它們會提供錯誤的信息。註釋不會自動更新,所以開發人員需要記得在維護代碼的時候同時維護它們。

自更新的文檔工具可以解決這個問題。在這方面,一個流行的工具就是 Swagger,它是基於 OpenAPI 構建的工具,可以很容易地描述你的 API。

Swagger 很酷的一點在於它是可執行的,所以如果你嘗試修改 API,能立即看到它的作用和變化。

爲了給 Swagger 添加自動更新功能,我們需要使用其他的插件和工具。在 Python 中,有針對大多數主流框架的插件。它們能生成 API 請求該如何組織的描述,並定義數據的輸入和輸出。

如果你不想要使用 Swagger,而是想使用更簡單的工具,那該怎麼辦呢?有個流行的替代方案是 Slate。

https://slatedocs.github.io/slate/#introduction

還有一些值得推薦的中間方案,如 widdershins 和 api2html 的組合,它允許我們從 Swagger 的定義中生成類似 Slate 的文檔。

https://github.com/Mermade/widdershins

https://api2html.com/docs/overview/

 緩存

在有些系統中,緩存可能並不是什麼大問題。這樣的系統可能沒有很多的數據可供緩存,所有的數據都在不斷地發生變化,或者系統根本沒有很大的流量。

但是,在大多數情況下,緩存對於良好的性能至關重要。它與 RESTful API 密切相關,因爲 HTTP 協議在緩存方面做了很多事情,比如 HTTP 頭信息允許我們控制緩存的行爲。

你可能想要在客戶端緩存東西,或者如果有註冊表或值存儲的話,那麼你可能想要在應用程序中緩存數據。但是,HTTP 讓我們能夠基本上免費就可以獲得一個很好的緩存,所以如果可能的話,請不要錯過這個免費的午餐。

同時,因爲緩存是 HTTP 規範的一部分,所以很多涉及 HTTP 的技術都知道如何進行緩存:瀏覽器原生支持緩存,客戶端和服務器之間的中間技術也是如此。

4API 設計的演化

構建 API 以及現代軟件最重要的部分就是適應性。如果沒有適應性,開發就會變慢,在合理的時間發佈特性就會變得更加困難,當面對最後截止時間的時候更是如此。

“軟件架構” 在不同的上下文語境中有不同的含義,不過我們現在採用這個定義:

軟件架構一種行爲 / 藝術,能夠避免會阻礙未來變化的決策。

記住了這一點,在設計軟件的時候,當你必須要在具有相似優點的方案中做出選擇時,你應該始終選擇更多考慮到未來的方案。

好的實踐並不是萬能的。按照正確的方式構建錯誤的東西並不是你想要的結果。最好採取一種成長的心態,接受變化是不可避免的,尤其是如果你的項目要持續發展的話更是如此。

要想讓你的 API 更具適應性,其中很關鍵的一點就是保持儘可能薄的 API 層,真正的複雜性應該往下層轉移。

5API 不應該限定實現

公開的 API 發佈之後,它就已經完成了,是不可改變的,你就不能再去觸碰它了。如果你已經有了一個設計古怪的 API,除了接受現狀之外,還能做些什麼呢?

你應該不斷尋找簡化實現的方法。有時候,你可以通過一個特定的 HTTP 頭信息來控制 API 響應的格式,相對於構建另外一個叫做 v2 的新 API,這是一種更簡單的解決方案。

API 只是另外一層的抽象。它們不應該決定如何實現,爲了避免這種問題,我們可以採用如下幾種開發模式。

 API 網關

這是一種類似於門面的開發模式。如果你要把一個單體結構拆分爲一組微服務,並且希望向外部暴露一些功能的話,那麼你只需要構建一個類似門面的 API 網關。

它將爲不同的微服務提供一個統一的接口(這些微服務可能有不同的 API,使用不同的錯誤格式等等)。

 適用於前端的後端

如果你必須要構建一個 API 來滿足一堆不同的客戶端的話,那麼這可能會非常困難。針對某個客戶端所作出的決策可能會影響其他客戶端的功能。

按照適用於前端的後端(backend for frontend)理念,如果你有不同的客戶端,它們喜歡不同形式的 API,比如移動應用可能會喜歡使用 GraphQL,那麼就單獨爲它們構建吧。

只有當你的 API 是一層抽象,並且這個抽象層很薄的時候,這種方式纔有效。如果它與你的數據庫耦合,或者太大,具有太多的邏輯,那麼就無法這樣做了。

6GraphQL 與 RESTful

很多人都在熱炒 GraphQL。它是一項新興的技術,但是已經有了很多粉絲,以至於有些開發者聲稱它將取代 REST。

儘管 GraphQL 比 RESTful 要新的多,但是它們有很多相似之處。GraphQL 最大的不足之處在於它的緩存,它必須要在客戶端或應用程序中實現。現在,有內置的實現了緩存功能的客戶端庫(比如 Apollo),但是這仍然要比使用 HTTP 提供的幾乎免費的緩存功能要困難。

https://www.apollographql.com/docs/react/caching/cache-configuration/

從技術講,GraphQL 位於 Richardson 模型的 Level 0 層級,但是它具有良好 API 的特質。我們可能無法同時使用多個 HTTP 的功能,但是 GraphQL 的出現就是解決這一問題的。

GraphQL 的殺手鐧就是聚合不同的 API,並將它們作爲一個 GraphQL API 暴露出來。

GraphQL 在處理數據抓取不足和數據過量抓取方面有很好的效果,而這些問題是 REST API 很難進行管理的。這兩個問題都與性能有關,如果數據抓取不足,那說明你沒有高效地使用 API,所以必須要進行大量的調用。如果數據過量抓取的話,那麼 API 調用的數據傳輸會比必要的數據傳輸更大,這是對帶寬的一種浪費。

7 小結

藉助 REST 與 GraphQL 的比較,我們能夠總結出一個好的 API 最重要的品質。

好的 API 的特性

我們需要一個清晰的數據表述方式:RESTful 以資源的方式提供了表述。

我們需要有一種方式顯示有哪些可用的操作:RESTful 通過組合資源和 HTTP 動作實現這一點。

我們需要有一種方式來確認是否存在錯誤 / 異常:HTTP 狀態碼可以實現這一點,可能還會包含闡述它們的響應信息。

最好能夠提供 API 發現和導航的功能:在 RESTful 中,HATEOAS 負責實現這一點。

有好的文檔是非常重要的:在這方面,可執行、自更新的文檔可以解決這個問題,這超出了 RESTful 規範的範圍。

最後,但同樣重要的是,優秀的 API 應該具有緩存功能,除非你的特定情況認爲它是不必要的。

REST 和 GraphQL 之間最大的區別是它們處理緩存性的方式。當我們使用 REST 方式構建 API 的時候,我們基本上可以免費獲得 HTTP 的緩存功能。如果選擇 GraphQL 的話,你需要自行負責爲客戶端或應用程序添加緩存。

原文鏈接:

https://www.stxnext.com/blog/how-to-build-a-good-api-that-wont-embarrass-you

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/5LHaWoBN58hDmLT0k9l0sg