設計易用的RESTFul API
早期互聯網很大一部分是靜態文件的堆積。因此,我們上網訪問的其實都是一個個靜態的資源。通過URL的定義,可以很方便地找到資源。
隨著互聯網應用規模的擴大,以及應用復雜度的提高,之前的靜態資源已經不能滿足需求,網站返回給訪問者的內容需要通過動態的方式生成(例如通過獲取數據庫的內容),同時還支持由訪問者控制希望看到的內容。 傳統的用于定位資源位置的URL的定義變得復雜起來。
本篇內容偏向于Web API設計中,有關URL部分的討論。
習以為常的一些URL
在Ajax還未發展起來前,網站應用的復雜度就已經變得相當復雜。因此當我們訪問很多網站時,觀察地址欄以及網絡傳輸時,不難見到類似這樣的請求。
http://xxx.com/index.jsp?page=10&catelog=food&create_time=20151001 |
這樣的請求存在一個問題:無法很好地描述一個資源。
- 無法知曉參數之間的層級關系,從請求串來看它們的關系是并列的
- 無法知曉某個參數具體服務于頁面中的哪一部分
- 由于HTTP是無狀態的,因此通過請求也無法準確知曉對于資源狀態的影響是什么
以上這些弊端對于描述一個資源是不友好的,特別當應用規模擴大時,會產生較大的對于接口的理解成本與使用成本。
自從Ajax技術興起,接口的表現形式又有了一些變化。由于頁面內容可以實現異步控制,因此通過統一的地址參數與服務器通信的模型改變為,特定的接口指定特定的參數。
http://xxx.com/index.jsp?page=10&catelog=food&create_time=20151001 => http://xxx.com/index.jsp?catelog=food http://xxx.com/items.action?page=10&create_time=20151001
這在一定程度上解決了參數混雜的問題,但是請求資源的層級關系仍然無法做到合理的區分。
API設計原則
一般來說,設計良好的Web API會至少滿足如下幾條原則:
- 參數職責單一
- 意圖清晰,便于開發者調用
- 易于訪問者輸入
REST原則
雖然原則看起來簡單易懂,但是現實應用中需要面對各種場景,實際上缺少一個相對統一的規范。
不過在滿足基本原則的基礎上,還是存在一些事實上的行業標準。業內用得比較多的是REST。
REST是什么
REST不僅是一份API設計準則,更是一套軟件實現架構,用于指導客戶端與服務器端之間的交互。 最早由 Roy Thomas Fielding 于2000年在他的博士論文中提出這個概念。
REST即Representational State Transfer的縮寫。它的理論比較抽象不太具體,理解它主要在于理解這些概念:資源、表現層、狀態轉換。
對于這些名詞的解釋,主要參考了 阮一峰 在博客中的描述。
資源
資源表示了存儲在網絡上的一個特定實體,可以是一段文本、一張圖片,也可以一種服務。每個資源由一個特定的URI進行描述。因此我們在瀏覽器中輸入一個特定的URI表示通過瀏覽器訪問某個資源。
例如
http://xxx.com/index.html 表示訪問一個超文本資源 http://xxx.com/music.mp3 表示訪問一個多媒體資源
表現層
按照字面意思翻譯,表現層表示一個資源的外在表現形式。當然這段話非常學術,不是很好理解。
通俗地理解表現層,大概可以認為它是一個資源以某種文件格式進行包裝返回給訪問者的形式。
例如一段文本,既可以以普通文本,也可以以超文本的格式承載,在客戶端分別以不同的形式進行展示。但是在網絡上確實對應的是同一個資源。
一般與表現層關聯最大的是應用服務器與Web服務器,通過設置Content-Type的HTTP響應給予資源不同的表現形式。
狀態轉換
客戶端對于服務器端的訪問可能會引起資源的狀態變化。
比如:增加一條記錄、修改一條記錄。 這些操作都會引起狀態的變化,因此需要客戶端可以合理地操作。
由于HTTP的無狀態特性,所有描述訪問者狀態的內容都保存在服務器端,因此客戶端需要通過某種手段引起服務器端資源的狀態變化。
客戶端可以使用的手段之一,是HTTP協議。HTTP協議包含幾種對于資源操作的描述,REST對應地可以使用其中四個動詞:GET、POST、PUT、DELETE。 分別與應用服務器的CRUD操作進行對應。
這樣對于某次請求的描述就包含了:訪問什么資源、以什么樣的形式封裝資源、資源的操作后狀態是什么。所有網絡請求的本質都認為是對于資源的操作。
RESTFul API
基于REST原則設計的API,一般稱為RESTFul API,需要遵守以下這些原則。
- URL描述的是一個特定資源。因此描述需要名詞,不能出現動詞。因為動詞描述的不再是資源本身,而是行為
- 利用HTTP請求的動詞表示對于資源操作的行為
同時,對于URL的設計一般還有約定俗成的以下補充。
- 對于資源的描述的名詞應該是層級嵌套的方式,比如/company/department/projects。通過這種對于信息層級描述的方式,更利于實體的抽象,以及增加客戶端與服務器端開發人員對于整個系統模塊認知的一致性
- 路徑終點的命名考慮用復數形式,比如/books。一般一個URL路徑表示的資源會映射為數據庫一系列表的記錄的集合,因此使用復數更直觀
符合RESTFul API的接口簡單示意如下
GET /books (獲取所有書列表) GET /books/1 (獲取編號為1的書) GET /books/1/summary (獲取編號為1的書目的簡介信息) POST /books (增加一本書) PUT /books/1 (修改編號為1的書) DELETE /books/2 (刪除編號2的書)
RESTFul API設計的折衷
REST原則可以用于優化系統架構,指導系統設計采用更清晰的實體劃分。 但是實際項目的使用場景迥異,一味地照搬原則并不能完全滿足所有場景的需求。在某些情況下,應用REST需要針對性地進行一些補充。
API版本化
API版本化的應用在實際開發中是有實際意義的。
接口一般情況下是相對穩定的,但是也會隨著業務需求的變更而發生變化。 另外還可能是業務需求并未發生變更,為了更好地組合資源,在接口層面進行了抽象和組合。 這種情況下,會要求服務器端實現一套新的接口實現,同時還需要保持原有接口可用。
從本質來看,接口的變化其實反映的是接口使用者感知的“資源表現層”的變化。對于相同的資源,在URL上可以反映為多個不同的名詞描述。 但是這樣會帶來一些使用上的弊端。
- 接口的使用者需要感知一個新的實體名稱,提高了理解與使用成本
- 新的實體名稱未必能夠直觀表達接口更新后的狀態(無法區分接口的新舊)
所以比較常見的做法是在接口上增加版本信息。API的版本化也有兩種做法,各有利弊。
- 請求頭增加API信息
- 將API體現于URL
使用前者的理由在于,雖然接口版本不同,但是很多時候其實描述的是相同的資源,在描述的資源地址中增加版本號信息會破壞REST原則。這是偏學術層面的理由。
使用后者的理由在于,版本號信息體現在URL會使接口更加直觀與易用。同時版本號作為接口服務器信息的補充方式存在,可以理解為并非對于資源本體的描述。
事實上,在實際項目中出于靈活性考慮,也會將兩種方式結合使用的。例如Github的API v3就同時支持了兩種方式
實體別名
在實體層級關系較多的系統中,某些實體對外可訪問的接口是通過上層實體關聯暴露的。比如
GET /bookstores/1/catelogs/2/saleBooks |
saleBook是作為bookstore關聯的catelog的下層關聯實體存在。因此接口使用者感知的是三個實體的關聯關系。
出于使用的便捷性考慮,用戶也可以只需要理解一個實體名稱saleBook。雖然架構上或者物理上仍然作為下層關聯實體存在,但是在接口上可以重新設計為
GET /saleBooks/1/2 |
這時,我們稱saleBook為bookstore/catelog/saleBook的一個別名。
但是需要注意的是,在一般情況下,除非URL特別長導致難以理解或者調用,不建議過度使用別名。
理由:別名會導致使用者減少了對于實體間關系本質的理解,同時別名還占用了額外的命名空間,可能會和未來出現的某個新的實體的名稱重復,或者由于相似命名造成額外的理解成本。
自定義視圖
資源是某些特定的數據的聚合,它向外暴露的結構應該是固定不變的。但是在實際應用場景中,同一個接口的不同調用方可能需要用到不同的數據視圖。
如果為每個調用方設計一套獨立的接口自然能夠滿足需求,但是缺點同樣也很明顯。
- 每個接口需要實現獨立的業務邏輯,增加了開發成本
- 增加了接口數量,提高了后期維護成本
因此,允許調用方自定義資源的視圖是一種可行的解決方案。 針對同一個描述資源的接口,具體返回的字段由請求參數進行約定。參數來自調用方,服務器端將資源組裝成不同的視圖返回調用者。
在具體實現上,接口的可指定視圖參數更類似于存儲資源這方支持數據查詢接口的約定。比如接口的查詢條件在定義上與關系型數據庫的查詢條件比較類似。
這種方式應用比較廣泛,在各種系統中都可見。比如指定特定的查詢條件,以及指定需要返回的特定字段。
GET /books?catelogs=JS&fields=name,price,author,rate&orderBy=rate&order=desc |
慎用刪除
RESTFul的設計原則中,對資源的刪除操作往往會用HTTP的DELETE動詞來描述。但是實際大部分業務場景下,刪除操作并不會被使用到。 一般會出于以下幾方面的考慮:
- 資源的刪除會導致系統邏輯一旦出錯缺少完全回滾的機制
- 企業越來越注重對于用戶的數據挖掘
因此我們一般認為業務型系統架構中對于資源采用非物理性刪除更優于物理性刪除。例如為資源增加一個狀態的描述,調用者期望的刪除其實對應的是狀態的變更。
DELETE /books/1 => PUT /books/1/status status=0
這種接口定義的資源操作沒有發生真實的刪除,而僅僅是某些數值的變更。另外通過應用層的業務邏輯輔助屏蔽不希望調用者查看的數據。最終通過一種“模擬刪除”的行為同樣滿足了對“刪除”操作的需求。
從REST設計原則的角度來看,這種方式其實只是利用PUT操作替代了DELETE操作的效果,資源其實還是真實存在的,并未破壞HTTP動詞的語義,也沒有打破REST設計原則。
總結
RESTFul架構在指導抽象業務實體以及組織系統模塊方面有著巨大的優勢,目前在流量端系統中也已經得到比較多的應用。
但同時我們也可以看到,部分系統的接口設計也存在一些欠缺,比如
- 對部分資源的描述使用了動詞
- 資源的層級劃分不合理
- 接口設計時未考慮版本化
其中對于資源層級的合理劃分,可以幫助前后端對于系統理解保持一致;
大部分流量端系統目前都是前后端分離部署的,但是接口未實現版本化,意味著
- 一旦出現需要修改某個接口定義時,不得不約定新接口,增加了冗余的資源定義
- 在不約定新接口的情況下,前后端必須要同時部署
- 前后端部署分離時,可能在中間的某個階段,系統的服務可能是會出現錯誤的
而接口實現版本化意味著可以實現真正的前后端分離部署。
以上這些都等待我們在之后的迭代開發去逐步優化。
參考文章
- Best Practices for Designing a Pragmatic RESTful API
- Some REST best practices
- 理解RESTFul架構
- RESTFul API設計指南