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