天天看點

API設計的幾條原則

API 本身的含義指應用程式接口,包括所依賴的庫、平台、作業系統提供的能力都可以叫做 API。我們在讨論微服務場景下的 API 設計都是指 WEB API,一般的實作有 RESTful、RPC等。API 代表了一個微服務執行個體對外提供的能力,是以 API 的傳輸格式(XML、JSON)對我們在設計 API 時的影響并不大。

API 設計是微服務設計中非常重要的環節,代表服務之間互動的方式,會影響服務之間的內建。 通常來說,一個好的 API 設計需要滿足兩個主要的目的:

平台獨立性。 任何用戶端都能消費 API,而不需要關注系統内部實作。API 應該使用标準的協定和消息格式對外部提供服務。傳輸協定和傳輸格式不應該侵入到業務邏輯中,也就是系統應該具備随時支援不同傳輸協定和消息格式的能力。

系統可靠性。 在 API 已經被釋出和非 API 版本改變的情況下,API 應該對契約負責,不應該導緻資料格式發生破壞性的修改。在 API 需要重大更新時,使用版本更新的方式修改,并對舊版本預留下線時間視窗。

實踐中發現,API 設計是一件很難的事情,同時也很難衡量設計是否優秀。根據系統設計和消費者的角度,給出了一些簡單的設計原則。

使用成熟度合适的 RESTful API

RESTful 風格的 API 具有一些天然的優勢,例如通過 HTTP 協定降低了用戶端的耦合,具有極好的開放性。是以越來越多的開發者使用 RESTful 這種風格設計 API,但是 RESTful 隻能算是一個設計思想或理念,不是一個 API 規範,沒有一些具體的限制條件。

是以在設計 RESTful 風格的 API 時候,需要參考 RESTful 成熟度模型。

API設計的幾條原則

根據自己的應用場景選擇對應的成熟度模型,一般來說系統成熟度模型在 Level 2左右。

避免簡單封裝

API應該服務業務能力的封裝,避免簡單封裝讓API徹底變成了資料庫操作接口。例如标記訂單狀态為已支付,應該提供形如POST /orders/1/pay這樣的API。而非PATCH /orders/1,然後通過具體的字段更新訂單。

因為訂單支付是有具體的業務邏輯,可能涉及到大量複雜的操作,使用簡單的更新操作将業務邏輯洩漏到系統之外。同時系統外也需要知道訂單狀态 這個内部使用的字段。

更重要的是,破壞了業務邏輯的封裝,同時也會影響其他非功能需求。例如,權限控制、日志記錄、通知等。

關注點分離

好的接口應該做到不多東西,不少東西。 怎麼了解呢?在使用者修改密碼和修改個人資料的場景中,這兩個操作看起來很類似,然後設計API的時候使用了一個通用的/users/1/udpateURI。

然後定義了一個對象,這個對象可能直接使用了User這個類:

{
  "username": "使用者名",
  "password": "密碼"
}
           

這個對象在修改使用者名的時候, password是不必要的,但是在修改密碼的操作中,一個password字段卻不夠用了,可能還需要

confirmPassword。

于是這個接口變成:

{
  "username": "使用者名",
  "password":"密碼",
  "confirmPassword":"重複密碼"
}
           

這種類的複用會給後續維護的開發者帶來困惑,同時對消費者也非常不友好。合理的設計應該是兩個分離的 API:

// POST /users/{userId}/password

{
  "password":"密碼",
  "confirmPassword":"重複密碼"
}
// PATCH /users/{userId}

{
  "username":"使用者名",
  "xxxx":"其他可更新的字段"
}
           

對應的實作,在 Java 中需要定義兩個 DTO,分别處理不同的接口。這也展現了面向對象思想中的關注點分離。

完全窮盡,彼此獨立

API 之間盡量遵守完全窮盡,彼此獨立 (MECE) 原則,不應該提供互相疊加的 API。例如訂單和訂單項這兩個資源,如果提供了形如 PUT /orders/1/order-items/1 這樣的接口去修改訂單項,接口 PUT /orders/1 就不應該具備處理某一個 order-item 的能力。

這樣的好處是不會存在重複的 API,造成維護和了解上的複雜性。如何做到完全窮盡和彼此獨立呢?

簡單的方法是使用一個表格設計 API,标出每個 URI 具備的能力。

API設計表格

API設計表格

API設計的幾條原則

資源 URL 設計來源于 DDD 領域模組化就非常簡單了,聚合根作為根 URL,實體作為二級 URI 設計。聚合根之間應該徹底沒有任何聯系,實體和聚合根之間的責任應該明确。

産生這類問題的根源還是缺乏合理的抽象。如果存在 API 中可以通過使用者組操作使用者,通過使用者的 URI 操作使用者屬于的使用者組,這其中的問題是缺少了成員這一概念。使用者組下面的本質上并不是使用者,而是使用者和使用者組的關系,即成員。

版本化

一個對外開放的服務,極大的機率會發生變化。業務變化可能修改 API 參數或響應資料結構,以及資源之間的關系。一般來說,字段的增加不會影響舊的用戶端運作。但是當存在一些破壞性修改時,就需要使用新的版本将資料導向到新的資源位址。

版本資訊的傳輸,可以通過下面幾種方式

URI 字首

Header

Query

比較推薦的做法是使用 URI 字首,例如/v1/users/表達擷取 v1 版本下的使用者清單。

常見的反模式是通過增加 URI 字尾來實作的,例如/users/1/updateV2。這樣做的缺陷是版本資訊侵入到業務邏輯中,對路由的統一管理帶來不便。

使用 Header 和 Query 發送版本資訊則較為相似,不同之處在于,使用 URI 字首在 MVC 架構中實作相對簡單,隻需要定義好路由即可。使用 Header 和 Query 還需要編寫額外的攔截器。

合理命名

設計 API 時候的命名涉及多個地方:URI、請求參數、響應資料等。通常來說最主要,也是最難的一個是全局命名統一。

其次,命名需要注意這些:

盡可能和領域名詞保持一緻,例如聚合根、實體、事件等

RESTful 設計的 URI 中使用名詞複數

盡可能不要過度簡寫,例如将 user 簡寫成usr

盡可能使用不需要編碼的字元

用領域名詞來對 API 設計命名不是一件特别難的事情。識别出的領域名詞可以直接作為 URI 來使用。如果存在多個單詞的連接配接可以使用中橫線,例如/orders/1/order-items

安全

安全是任何一項軟體設計都必須要考慮的事情,對于 API 設計來說,暴露給内部系統的 API 和開放給外部系統的 API 略有不同

内部系統,更多的是考慮是否足夠健壯。對接收的資料有足夠的驗證,并給出錯誤資訊,而不是什麼資訊都接收,然後内部業務邏輯應該邊界值的影響變得莫名其妙。

而對于外部系統的 API 則有更多的挑戰。

錯誤的調用方式

接口濫用

浏覽器消費 API 時因安全漏洞導緻的非法通路

是以設計 API 時應該考慮響應的應對措施。針對錯誤的調用方式,API 不應該進入業務處理流程,及時給出錯誤資訊;對于接口濫用的情況,需要做一些限速的方案;對于一些浏覽器消費者的問題,可以在讓 API 傳回一些安全增強頭部,例如:X-XSS-Protection、Content-Security-Policy 等。

API 設計評審清單

URI 命名是否通過聚合根和實體統一

URI 命名是否采用名詞複數和連接配接線

URI 命名是否都是單詞小寫

URI 是否暴露了不必要的資訊,例如/cgi-bin

URI 規則是否統一

資源提供的能力是否彼此獨立

URI 是否存在需要編碼的字元

請求和傳回的參數是否不多不少

資源的 ID 參數是否通過 PATH 參數傳遞

認證和授權資訊是否暴露到 query 參數中

參數是否使用奇怪的縮寫

參數和響應資料中的字段命名統一

是否存在無意義的對象包裝 例如{“data”:{}’}

出錯時是否破壞約定的資料結構

是否使用合适的狀态碼

是否使用合适的媒體類型

響應資料的單複是否和資料内容一緻

響應頭中是否有緩存資訊

是否進行了版本管理

版本資訊是否作為 URI 的字首存在

是否提供 API 服務期限

是否提供了 API 傳回所有 API 的索引

是否進行了認證和授權

是否采用 HTTPS

是否檢查了非法參數

是否增加安全性的頭部

是否有限流政策

是否支援 CORS

響應中的時間格式是否采用ISO 8601标準

是否存在越權通路