
前言
去年的某個時候就想寫一篇關于接口的吐槽,當時后端提出了接口方案對于我來說調用起來非常難受,但又說不上為什么,沒有論點論據所以也就作罷。最近因為寫全棧的緣故,團隊內部也遇到了一些關于接口設計的問題,于是開始思考實現接口的最佳實踐是什么。在參考了許多資料之后,逐漸對這個問題有了自己的理解。同時回想起過去的經驗,終于恍然大悟自己當時的痛點在哪里。
既然是吐槽,那么請原諒我接下來態度的不友善。本文中列舉的所有例子都是我個人的親身經歷。
誰應該主導接口的設計
或者更直白一些,應該是接口的消費方還是提供方來決定接口的設計?
當然是接口的消費方
「接口」最吊詭的地方在于提供方大費周章把它實現了,但它自己卻(幾乎)重來都不使用。于是這極易陷入一種自嗨的境地,因為他更本不知道接口的好壞。就好比一個從來不嘗自己做的菜的廚子,你指望他的菜能好到哪里去,他的廚藝能好到哪里去。上面隱含的前提是(我認為)接口是有絕對好壞之分的,壞的接口消費者調用難受,提供者維護難受,還導致產品行為別扭體驗變差。
然而接口的好壞與誰來主導設計有什么關系?因為壞接口產生的原因之一是提供方只站在開發者的角度解決問題:
例子一 (Chatty API)
某次需要實現允許用戶創建儀表盤頁面的功能(如果你對儀表盤頁面感到陌生的話,可以想象它是一張集中了不同圖表的頁面,比如柱狀圖、折線圖、餅圖等等。用戶可以添加自己想要的圖表到頁面中,并且手動調整它們的尺寸和位置。儀表盤通常用于總覽某個產品或者服務的運行狀態)。后端同學的接口初步設計是,當用戶填寫完基本信息、添加完圖表、點擊創建按鈕之后,我需要連續調用兩次接口才能完成一次儀表盤的創建:
- 利用用戶填寫的基本信息以及圖表的尺寸和位置創建一個空的儀表盤
- 再向儀表盤中填充圖表的具體信息,比如圖表類型,使用的維度和指標等
很明顯看出他完全是按照自己后端的存儲結構在設計接口,不僅是存儲結構,甚至存儲過程都一覽無余。想象一種極端的情況,那不只提供一些更新數據庫表的接口得了,前端自己把通過接口把數據插入庫中面對這類底層性質的接口,消費者在集成時需要考慮接口的調用步驟以及理解背后的原理。如果后端的底層結構一旦發生更改,接口很有可能也需要發生更改,前端的調用代碼也需要隨之更改。后端研發可能會辯解說:后端用了微服務啊,不同類型的數據存儲在不同的服務上,所以你需要和不同的服務通信才能實現完整的存儲。他們始終沒有明白的事情是,后端的實現導致了接口的碎片化,那是你的問題,而不應該把這部分負擔轉移到前端的開發者上,其實也是間接轉移到了用戶身上。不要欺負我不懂后端,至少我了解加一層類似于 BFF 的 Orchestration Layer 就能解決這個問題Netflix 的工程師 Daniel Jacobson 在他的文章 The future of API design: The orchestration layer 中指出, API 無非是要面對兩類受眾:
- LSUD: Large set of unknown developers
- SSKD: Small set of known developers
隨著產品服務化的趨勢,很有可能需要像 AWS 或者 Github 那樣對公共開發者即 LSUD 暴露接口。且不說上面例子中的接口方案會不會被唾沫星子淹死,如此明顯的暴露內部服務的細節是非常危險的事情。所以在設計接口時,應該讓消費者來主導。如果消費者沒能給出很好的建議,那么至少提供者在設計時也應該站在消費者的立場上來思考問題。又或者,至少想一想如果你自己會樂意使用用你自己設計出來的接口嗎?使用后端思維設計接口不僅體現在 URI 的設計上,還有可能體現在請求參數和返回體結構上:
例子二
假設現在需要一個請求批量文章的接口,接口同時返回多篇文章的內容,包括這些文章的內容,作者信息,評論信息等等。理想情況下,我們期望返回的數據是以文章為單位的,比如
articles: [{id: ,author: {},comments: []},{id:author: {},comments: []}
]
However, 后端的返回結果可能是以實體為單位:
{articles: [],authors: [],comments: []
}
comments 里包含不同文章的 comment,我必須通過類似于 articleId 的字段對它們執行 group by 操作才能分離出屬于不同文章的評論。對其他實體做同樣的操作,最終手動的拼接成前端代碼需要的 articles 數據結構很明顯這又是按照后端庫表關系返回的結果,嚴格來說這并不算是 anti-pattern,在 redux 中也鼓勵將數據 normalize。但如果前端用不到原始數據,請不要返回原始數據。例如我需要在頁面上展示一個百分比格式的數據,除非用戶有動態調整數據格式的需求,例如千分位、小數或者是切換精度等等,否則就直接返回給我百分比的字符串就好了,不要返回給我原始的浮點數據。前端對數據的二次加工還會給問題排查帶來干擾,如果任何數據都需要前端進行二次加工,那么所以問題的排查都必須從前端發起,前端確認無誤后再進入后端排查流程,這始終會占用兩個端的人力,并且 delay 了排查的進度
關于 meta 信息
例子三:
后端接口在返回時通常會帶上 meta 信息,meta 信息包含接口的狀態以及如果失敗時的失敗原因,便于調試使用。后端提供的接口的 meta 信息的數據結構如下:
{meta: {code: 0,error: null,host: "127.0.0.1"},result: []
}
在我看來,以上數據結構有兩個問題
meta 信息包含獨立的狀態信息
在包含狀態碼的 meta 信息接口設計中,一條默認的隱藏邏輯是:接口返回的 HTTP status code 一定是 200,數據是否真的獲取成功需要通過 meta 里的自定狀態碼 code 進行判斷(換句話說,上面你看到的接口實際上是 “接口的接口”)。最終在前端的代碼中也不需要通過 HTTP code 判斷返回是否正常,只需要判斷接口里返回的meta.code
即可** 但是誰給你們的自信保證后端接口一定是不會掛的?!** 無論后端如何保證接口的堅固,前端仍然需要首先判斷 HTTP code 是否為 200,再判斷meta.code
是否與預期的符合一致。這和信任無關,和我程序的健壯有關。既然無論如何都要對接口判斷兩次,那為什么不將meta.code
與 HTTP code 合二為一?更何況我還需要再本地維護一份自定義 code 的枚舉值,還需要和后端保證同步。這就涉及到下一個問題了:
meta 信息的存放位置
我們需要 meta 信息沒有錯,但是我們沒有那么需要 meta 信息。這體現在幾點:
- 我們真的需要一個平行于返回結果的字段展示 meta 信息嗎?
- 每一次請求我們都需要 meta 信息嗎?
- meta 信息一定要在 meta 字段里嗎?
以請求失敗的錯誤信息為例,錯誤信息只會出現在接口非正常返回的情況下,但我們應該始終在返回體中用一個字段為它預留位置嗎?在關于 meta 信息存在位置的這個問題上,我傾向于將它們整合進入 HTTP Header 中。例如meta.code
完全可以使用 HTTP code 代替,我看不出始終要保證 200 返回以及自定義 code 的意義在哪里而至于其它的 meta 信息,可以通過以X-
開頭的自定義 HTTP Header 進行傳遞。例如Github API 中關于使用頻率限制的信息就放在 HTTP Header 中:
Status: 200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873
Design for today
例子四
我們需要為某個指標的折線圖設計查詢接口,查詢以天為單位,也就是說該接口只會根據查詢的日期返回指定日期的查詢結果,后端提供的返回數據結構如下:
{data: [{date: "2019-06-08",result: []}]
}
雖然需求很明確的指示只會返回某天的查詢結果,但是后端還是決定給我返回一個數組。他這么設計的理由是為了防止日后需求發生改變需要返回多日的查詢結果。這看上去是很聰明決策:“看,我預見性的 cover 了一個未來的需求!”,但實際上愚蠢至極:你的確 cover 了一個需求,不過是一個當前并不存在,未來也不見得會發生的需求;而且如果你真的想寫 future-proof 的代碼,那么還有未來千千萬萬的需求等待著你實現。問題在于沒有人知道將來是否真的會允許同時查詢多日數據,即使某天需要支持同時查詢多日數據了,數據結構也不一定非要如此。在數據分析領域我們面臨的查詢需求并不是線性從單個到多個,在其他業務領域也是這樣。這樣導致的后果是你花費多余的時間實現了不需要的代碼,并且前端也需要配合這樣的數據結構進行實現。并且在將來的維護中,每個看到返回體是數組的人都會納悶為什么返回的結果明明只有一條,還需要用數組封裝,是不是我遺漏了什么?于是不得不投入精力來驗證是否真的有可能返回更多的數據。API 和代碼應該是精準的,準確表達你想實現的一切而不存在有歧義有人可能會說不就是多了一層封裝嗎?實現上也花不了多少的功夫何至于大驚小怪。抱歉我不是針對這一個 case,而是在強調任何場景下無論實現的難易都不應該添加無意義的代碼,“勿以惡小而為之” 就是這個道理“關注當下” 還有另一個維度含義:
例子五
目前我們已經有創建單個文章的接口,現在需要支持批量創建文章。后端給出的建議是:不如調單個接口多次?
例子六
目前已經有一個接口能夠取得文章相關數據,比如內容、評論、作者等等。現在我們需要增加一個新的頁面用于展示用戶信息。后端給出的建議是:不如使用文章數據接口,里面已經包含了作者信息,這樣就不用開發新的接口了以上的例子看似都是想實現對接口的復用,但實際上起到的是事倍功半的效果在例五中,雖然語義上 “創建五篇文章” 和“連續五次創建一篇文章”是等效的,但是在實現和操作層面并不是如此。且不說調用五次和調用一次的性能大不相同,批量創建的五篇文章可能存在順序關系,可能需要事務操作。在例六中雖然能夠達到我們實現的效果,但這不能算是接口的復用,只能算是接口的 hack(hack 和復用的區別在于是否用物品的初衷功能做事情)。并且 hack 接口是有風險的,對于接口的提供者而言他們更關心接口服務 “正統” 的消費者,在這個 case 中接口的存在是為了展示完整的文章信息,如果有一天 “文章信息” 這個需求發生了變化很有可能會導致作者信息同時發生變化,縮減字段甚至取消字段。那么它們沒有義務這些 hack 用戶負責。一個接口本應該就專注一件事情所以最理想的事情是,為當前專注的業務開發獨立的接口。在例六的例子中,可能我們在開發一個獨立請求作者的信息的接口時實現代碼完全復制自另一個接口的實現,但是接口的隔離在長遠看來能給功能的維護帶來更大的便利
不僅限于 REST API
“接口” 是一個概念。在概念之下如何實現它我們擁有很多種選擇。目前看來絕大部分的方式是通過 REST API 來達成的,也并沒有什么事情是 REST API 無法做到的,但事實上這幾年技術的進步給了我們更多的選擇,如果選擇更有針對性的實現方案,效果會更好例如在實時數據的場景下,理論上是由后端(有數據更新時)驅動前端視圖的更新,這理應是 push 操作。但是在傳統實現中,我們不得不仍然通過被動的等待和輪詢實現功能。對于事件驅動類型的需求使用 WebSocket 或者是 Streaming 似乎是更好的選擇。如果是后端之間的交互還可以利用 WebHook。我通常對新技術持保留態度,但是不得不承認 GraphQL 在處理某些需求上也能夠比 REST API 做的更好。并且大部分廠商對于 GraphQL 接口的支持表明它是可行的。我了解實現 API 來只是后端實現功能的一個很小的環節,在接口背后是更多業務邏輯的修改和庫表結構的更迭。甚至說接口部分有一半都是交給框架來實現的。但是,哪怕只有很小的機會,也應該把這個環節做到盡善盡美。
結束語
對于糟糕的接口設計我還能繼續沒完沒了的抱怨下去,但突然然覺得洋洋灑灑的繼續寫下去似乎沒有太大意義。講真我不是來真的大吐苦水的,只是想表達接口設計也至關重要。在工作中痛心的看到很多問題明明用一些很基礎的技巧就能夠解決,而大家卻對它熟視無睹以造成兩敗俱傷的境地。以上就是我認為的在接口設計中需要遵循的一些原則和考慮要素,相信能夠解決大多數的痛點和避免部分的問題后端同學們,如果你們有心讓接口變得更好,多聽聽 “消費者” 的反饋。如果你們嘗試使用過第三方接口開發過應用的話,例如 Slack、Github,你會發現它們的接口是在不斷迭代的。不斷有舊的接口被淘汰,新的接口投入使用。這種迭代背后不是閑著沒事干,而是出于實際的用戶的聲音和需求最后推薦我最近閱讀的關于 API 設計的圖書,收益匪淺:
- Web API 的設計與開發
- Designing Web APIs
- APIs A Strategy Guide
作者:李熠
鏈接:http://juejin.im/post/5cfbe8c7e51d4556da53d07f
文章著作權歸作者所有,如有侵權,請聯系小編刪除。