深度 | API 设计最佳实践的思考
API 是模塊或者子系統(tǒng)之間交互的接口定義。好的系統(tǒng)架構(gòu)離不開好的 API 設(shè)計,而一個設(shè)計不夠完善的 API 則注定會導(dǎo)致系統(tǒng)的后續(xù)發(fā)展和維護非常困難。
接下來,阿里巴巴研究員谷樸將給出建議,什么樣的 API 設(shè)計是好的設(shè)計?好的設(shè)計該如何做?
作者簡介:張瓅玶 (谷樸),阿里巴巴研究員,負(fù)責(zé)阿里云容器平臺集群管理團隊。本科和博士畢業(yè)于清華大學(xué)。
前言
API 設(shè)計面臨的挑戰(zhàn)千差萬別,很難有處處適用的準(zhǔn)則,所以在討論原則和最佳實踐時,無論這些原則和最佳實踐是什么,一定有適應(yīng)的場景和不適應(yīng)的場景。因此我們在下文中不僅提出一些建議,也盡量去分析這些建議在什么場景下適用,這樣我們也可以有針對性地采取例外的策略。
為什么去討論這些問題? API 是軟件系統(tǒng)的核心,而軟件系統(tǒng)的復(fù)雜度 Complexity 是大規(guī)模軟件系統(tǒng)能否成功最重要的因素。但復(fù)雜度 Complexity 并非某一個單獨的問題能完全敗壞的,而是在系統(tǒng)設(shè)計尤其是API設(shè)計層面很多很多小的設(shè)計考量一點點疊加起來的(John Ousterhout老爺子說的Complexity is incremental【8】)。
成功的系統(tǒng)不是有一些特別閃光的地方,而是設(shè)計時點點滴滴的努力積累起來的。
范圍
本文偏重于一般性的API設(shè)計,并更適用于遠(yuǎn)程調(diào)用(RPC或者HTTP/RESTful的API),但是這里沒有特別討論RESTful API特有的一些問題。
另外,本文在討論時,假定了客戶端直接和遠(yuǎn)程服務(wù)端的API交互。在阿里,由于多種原因,通過客戶端的 SDK 來間接訪問遠(yuǎn)程服務(wù)的情況更多一些。這里并不討論 SDK 帶來的特殊問題,但是將 SDK 提供的方法看作遠(yuǎn)程 API 的代理,這里的討論仍然適用。
API 設(shè)計準(zhǔn)則:什么是好的 API
在這一部分,我們試圖總結(jié)一些好的 API 應(yīng)該擁有的特性,或者說是設(shè)計的原則。這里我們試圖總結(jié)更加基礎(chǔ)性的原則。所謂基礎(chǔ)性的原則,是那些如果我們很好地遵守了就可以讓 API 在之后演進的過程中避免多數(shù)設(shè)計問題的原則。
提供清晰的思維模型 provides a good mental model
為什么這一點重要?因為 API 的設(shè)計本身最關(guān)鍵的難題并不是讓客戶端與服務(wù)端軟件之間如何交互,而是設(shè)計者、維護者、API使用者這幾個程序員群體之間在 API 生命周期內(nèi)的互動。一個 API 如何被使用,以及API本身如何被維護,是依賴于維護者和使用者能夠?qū)υ?API 有清晰的、一致的認(rèn)識。這非常依賴于設(shè)計者提供了一個清晰易于理解的模型。這種狀況實際上是不容易達(dá)到的。
就像下圖所示,設(shè)計者心中有一個模型,而使用者看到和理解的模型可能是另一個模式,這個模式如果比較復(fù)雜的話,使用者使用的方式又可能與自己理解的不完全一致。 對于維護者來說,問題是類似的。
而好的 API 讓維護者和使用者能夠很容易理解到設(shè)計時要傳達(dá)的模型。帶來理解、調(diào)試、測試、代碼擴展和系統(tǒng)維護性的提升 。
- 好的例子:很多基礎(chǔ)設(shè)施領(lǐng)域的 API 都提供了非常好的正面的設(shè)計典型,如后面會重點提到的 Posix File API,就提供了非常清晰明了的 mental model。
- 不好的例子:String 是軟件中常見的類型,但是在一些 String 類庫的實現(xiàn)中,我們會看到設(shè)計者為了某些方便,提供了以數(shù)組方式訪問字符串的 API,這類 API 容易讓使用者形成字符串 = array of chars 的模型印象,而這樣的印象在一些特殊場景實際是不成立的(例如 Unicode 編碼等形態(tài))。
簡單 is simple
“Make things as simple as possible, but no simpler.” 在實際的系統(tǒng)中,尤其是考慮到系統(tǒng)隨著需求的增加不斷地演化,我們絕大多數(shù)情況下見到的問題都是過于復(fù)雜的設(shè)計,在 API 中引入了過多的實現(xiàn)細(xì)節(jié)(見下一條),同時也有不少的例子是Oversimplification 引起的,一些不該被合并的改變合并了,導(dǎo)致設(shè)計很不合理。
過于簡單化的例子:過去曾經(jīng)見過一個系統(tǒng),將一個用戶的資源賬戶模型的 account balance 和 transactions 都簡化為用 transactions 一個模型來表達(dá),邏輯在于 account balance 可以由歷史的 transactions 累計得到。但是這樣的過于簡化的模型設(shè)計帶來了很多的問題,尤其在引入分期付款、預(yù)約交易等概念之后,暴露了很多復(fù)雜的邏輯給一些只需要獲取簡單信息的客戶端(如計算這個用戶是否還有足夠的余額交易變得和很多業(yè)務(wù)邏輯耦合),屬于典型的模型過度簡化帶來的設(shè)計復(fù)雜度上升的案例。
容許多個實現(xiàn) allows multiple implementations
這個原則看上去更具體,也是我非常喜歡的一個原則。Sanjay Ghemawat 常常提到該原則。一般來說,在討論 API 設(shè)計時常常被提到的原則是解耦性原則或者說松耦合原則。然而相比于松耦合原則,這個原則更加有可核實性:如果一個 API 自身可以有多個完全不同的實現(xiàn),一般來說這個API已經(jīng)有了足夠好的抽象,那么一般也不會出現(xiàn)和外部系統(tǒng)耦合過緊的問題。因此這個原則更本質(zhì)一些。
舉個例子,比如我們已經(jīng)有一個簡單的 API
QueryOrderResponse queryOrder(string orderQuery)但是有場景需求希望總是讀取到最新更新數(shù)據(jù),不接受緩存,于是工程師考慮。
QueryOrderResponse queryOrder(string orderQuery, boolean useCache)增加一個字段 useCache 來判斷如何處理這樣的請求。
這樣的改法看上去合理,但實際上泄漏了后端實現(xiàn)的細(xì)節(jié)(后端采用了緩存),后續(xù)如果采用一個新的不帶緩存的后端存儲實現(xiàn),再支持這個 useCache 的字段就很尷尬了。
在工程中,這樣的問題可以用不同的服務(wù)實例來解決,通過不同訪問的 endpoint 配置來區(qū)分。
最佳實踐
本部分則試圖討論一些更加詳細(xì)、具體的建議,可以讓 API 的設(shè)計更容易滿足前面描述的基礎(chǔ)原則。
想想優(yōu)秀的API例子:POSIX File API
如果說 API 的設(shè)計實踐只能列一條的話,那么可能最有幫助的和最可操作的就是這一條。本文也可以叫做“通過 File API 體會 API 設(shè)計的最佳實踐”。
所以整個最佳實踐可以總結(jié)為一句話:“想想 File API 是怎么設(shè)計的。”
首先回顧一下 File API 的主要接口(以C為例,很多是 Posix API,選用比較簡單的I/O接口為例【1】:
int open(const char *path, int oflag, .../*,mode_t mode */); int close (int filedes); int remove( const char *fname ); ssize_t write(int fildes, const void *buf, size_t nbyte); ssize_t read(int fildes, void *buf, size_t nbyte);File API 為什么是經(jīng)典的好 API 設(shè)計?
- File API 已經(jīng)有幾十年歷史(從1988年算起,已30年),盡管期間硬件軟件系統(tǒng)的發(fā)展經(jīng)歷了好幾代,這套 API 核心保持了穩(wěn)定。這是極其了不起的。
- API 提供了非常清晰的概念模型,每個人都能夠很快理解這套API背后的基礎(chǔ)概念:什么是文件,以及相關(guān)聯(lián)的操作(open, close, read, write),清晰明了;
- 支持很多的不同文件系統(tǒng)實現(xiàn),這些系統(tǒng)實現(xiàn)甚至于屬于類型非常不同的設(shè)備,例如磁盤、塊設(shè)備、管道(pipe)、共享內(nèi)存、網(wǎng)絡(luò)、終端 terminal 等等。這些設(shè)備有的是隨機訪問的,有的只支持順序訪問;有的是持久化的有的則不是。然而所有不同的設(shè)備不同的文件系統(tǒng)實現(xiàn)都可以采用了同樣的接口,使得上層系統(tǒng)不必關(guān)注底層實現(xiàn)的不同,這是這套 API 強大的生命力的表現(xiàn)。
例如同樣是打開文件的接口,底層實現(xiàn)完全不同,但是通過完全一樣的接口,不同的路徑以及 Mount 機制,實現(xiàn)了同時支持。其他還有 Procfs, pipe 等。
int open(const char *path, int oflag, .../*,mode_t mode */);
上圖中,cephfs 和本地文件系統(tǒng),底層對應(yīng)完全不同的實現(xiàn),但是上層 client 可以不用區(qū)分對待,采用同樣的接口來操作,只通過路徑不同來區(qū)分。
基于上面的這些原因,我們知道 File API 為什么能夠如此成功。事實上,它是如此的成功以至于今天的 *-nix 操作系統(tǒng),everything is filed based.
盡管我們有了一個非常好的例子 File API,但是要設(shè)計一個能夠長期保持穩(wěn)定的 API是一項及其困難的事情,因此僅有一個好的參考還不夠,下面再試圖展開去討論一些更細(xì)節(jié)的問題。
Document well 寫詳細(xì)的文檔
寫詳細(xì)的文檔,并保持更新。 關(guān)于這一點,其實無需贅述,現(xiàn)實是,很多API的設(shè)計和維護者不重視文檔的工作。
在一個面向服務(wù)化/Micro-service 化架構(gòu)的今天,一個應(yīng)用依賴大量的服務(wù),而每個服務(wù) API 又在不斷的演進過程中,準(zhǔn)確的記錄每個字段和每個方法,并且保持更新,對于減少客戶端的開發(fā)踩坑、減少出問題的幾率,提升整體的研發(fā)效率至關(guān)重要。
Carefully define the "resource" of your API 仔細(xì)的定義“資源”
如果適合的話,選用“資源”加操作的方式來定義。今天很多的 API 都可以采用這樣一個抽象的模式來定義,這種模式有很多好處,也適合于 HTTP 的 RESTful API 的設(shè)計。但是在設(shè)計 API 時,一個重要的前提是對 Resource 本身進行合理的定義。什么樣的定義是合理的? Resource 資源本身是對一套 API 操作核心對象的一個抽象Abstraction。
抽象的過程是去除細(xì)節(jié)的過程。在我們做設(shè)計時,如果現(xiàn)實世界的流程或者操作對象是具體化的,抽象的 Object 的選擇可能不那么困難,但是對于哪些細(xì)節(jié)應(yīng)該包括,是需要很多思考的。例如對于文件的API,可以看出,文件 File 這個 Resource(資源)的抽象,是“可以由一個字符串唯一標(biāo)識的數(shù)據(jù)記錄”。這個定義去除了文件是如何標(biāo)識的(這個問題留給了各個文件系統(tǒng)的具體實現(xiàn)),也去除了關(guān)于如何存儲的組織結(jié)構(gòu)(again,留給了存儲系統(tǒng))細(xì)節(jié)。
雖然我們希望API簡單,但是更重要的是選擇對的實體來建模。在底層系統(tǒng)設(shè)計中,我們傾向于更簡單的抽象設(shè)計。有的系統(tǒng)里面,域模型本身的設(shè)計往往不會這么簡單,需要更細(xì)致的考慮如何定義 Resource。一般來說,域模型中的概念抽象,如果能和現(xiàn)實中的人們的體驗接近,會有利于人們理解該模型。選擇對的實體來建模往往是關(guān)鍵。結(jié)合域模型的設(shè)計,可以參考相關(guān)的文章,例如阿白老師的文章【2】。
Choose the right level of abstraction 選擇合適的抽象層
與前面的一個問題密切相關(guān)的,是在定義對象時需要選擇合適的 Level of abstraction (抽象的層級)。不同概念之間往往相互關(guān)聯(lián)。仍然以 File API 為例。在設(shè)計這樣的 API 時,選擇抽象的層級的可能的選項有多個,例如:
- 文本、圖像混合對象
- “數(shù)據(jù)塊” 抽象
- ”文件“抽象
這些不同的層級的抽象方式,可能描述的是同一個東西,但是在概念上是不同層面的選擇。當(dāng)設(shè)計一個 API 用于與數(shù)據(jù)訪問的客戶端交互時,“文件 File “是更合適的抽象,而設(shè)計一個 API 用于文件系統(tǒng)內(nèi)部或者設(shè)備驅(qū)動時,數(shù)據(jù)塊或者數(shù)據(jù)塊設(shè)備可能是合適的抽象,當(dāng)設(shè)計一個文檔編輯工具時,可能會用到“文本圖像混合對象”這樣的文件抽象層級。
又例如,數(shù)據(jù)庫相關(guān)的 API 定義,底層的抽象可能針對的是數(shù)據(jù)的存儲結(jié)構(gòu),中間是數(shù)據(jù)庫邏輯層需要定義數(shù)據(jù)交互的各種對象和協(xié)議,而在展示(View layer)的時候需要的抽象又有不同【3】。
Naming and identification of the resource 命名與標(biāo)識
當(dāng) API 定義了一個資源對象,下面一般需要的是提供命名/標(biāo)識( Naming and identification )。在 naming/ID 方面,一般有兩個選擇(不是指系統(tǒng)內(nèi)部的 ID,而是會暴露給用戶的):
- 用free-form string作為ID(string nameAsId)
- 用結(jié)構(gòu)化數(shù)據(jù)表達(dá)naming/ID
何時選擇哪個方法,需要具體分析。采用 Free-form string 的方式定義的命名,為系統(tǒng)的具體實現(xiàn)留下了最大的自由度。帶來的問題是命名的內(nèi)在結(jié)構(gòu)(如路徑)本身并非API強制定義的一部分,轉(zhuǎn)為變成實現(xiàn)細(xì)節(jié)。如果命名本身存在結(jié)構(gòu),客戶端需要有提取結(jié)構(gòu)信息的邏輯,這是一個需要做的平衡。
例如文件 API 采用了 free-form string 作為文件名的標(biāo)識方式,而文件的 URL 則是文件系統(tǒng)具體實現(xiàn)規(guī)定。這樣,就容許 Windows 操作系統(tǒng)采用 "D:DocumentsFile.jpg" 而 Linux 采用 "/etc/init.d/file.conf" 這樣的結(jié)構(gòu)了。而如果文件命名的數(shù)據(jù)結(jié)構(gòu)定義為:
disk: string,path: string }這樣結(jié)構(gòu)化的方式,透出了 "disk" 和 "path" 兩個部分的結(jié)構(gòu)化數(shù)據(jù),那么這樣的結(jié)構(gòu)可能適應(yīng)于 Windows 的文件組織方式,而不適應(yīng)于其他文件系統(tǒng),也就是說泄漏了實現(xiàn)細(xì)節(jié)。
如果資源 Resource 對象的抽象模型自然包含結(jié)構(gòu)化的標(biāo)識信息,則采用結(jié)構(gòu)化方式會簡化客戶端與之交互的邏輯,強化概念模型。這時犧牲掉標(biāo)識的靈活度,換取其他方面的優(yōu)勢。例如,銀行的轉(zhuǎn)賬賬號設(shè)計,可以表達(dá)為:
{account: numberrouting: number }這樣一個結(jié)構(gòu)化標(biāo)識,由賬號和銀行間標(biāo)識兩部分組成,這樣的設(shè)計含有一定的業(yè)務(wù)邏輯在內(nèi),但是這部分業(yè)務(wù)邏輯是被描述的系統(tǒng)內(nèi)在邏輯而非實現(xiàn)細(xì)節(jié),并且這樣的設(shè)計可能有助于具體實現(xiàn)的簡化以及避免一些非結(jié)構(gòu)化的字符串標(biāo)識帶來的安全性問題等。因此在這里結(jié)構(gòu)化的標(biāo)識可能更適合。
另一個相關(guān)的問題是,何時應(yīng)該提供一個數(shù)字 unique ID ? 這是一個經(jīng)常遇到的問題。有幾個問題與之相關(guān)需要考慮:
- 是否已經(jīng)有結(jié)構(gòu)化或者字符串的標(biāo)識可以唯一、穩(wěn)定標(biāo)識對象?如果已經(jīng)有了,那么就不一定需要 numerical ID;
- 64位整數(shù)范圍夠用嗎?
- 數(shù)字 ID 可能不是那么用戶友好,對于用戶來講數(shù)字的 ID 會有幫助嗎?
如果這些問題都有答案而且不是什么阻礙,那么使用數(shù)字 ID 是可以的,否則要慎用數(shù)字ID。
Conceptually what are the meaningful operations on this resource? 對于該對象來說,什么操作概念上是合理的?
在確定下來了資源/對象以后,我們還需要定義哪些操作需要支持。這時,考慮的重點是“ 概念上合理(Conceptually reasonable)”。換句話說,operation + resource 連在一起聽起來自然而然合理(如果 Resource 本身命名也比較準(zhǔn)確的話。當(dāng)然這個“如果命名準(zhǔn)確”是個 big if,非常不容易做到)。操作并不總是CRUD(create, read, update, delete)。
例如,一個 API 的操作對象是額度(Quota ),那么下面的操作聽上去就比較自然:
- Update quota(更新額度),transfer quota(原子化的轉(zhuǎn)移額度)
但是如果試圖 Create Quota,聽上去就不那么自然,因額度這樣一個概念似乎表達(dá)了一個數(shù)量,概念上不需要創(chuàng)建。額外需要思考一下,這個對象是否真的需要創(chuàng)建?我們真正需要做的是什么?
For update operations, prefer idempotency whenever feasible 更新操作,盡量保持冪等性
Idempotency 冪等性,指的是一種操作具備的性質(zhì),具有這種性質(zhì)的操作可以被多次實施并且不會影響到初次實施的結(jié)果“the property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result beyond the initial application.”【3】
很明顯 Idempotency 在系統(tǒng)設(shè)計中會帶來很多便利性,例如客戶端可以更安全地重試,從而讓復(fù)雜的流程實現(xiàn)更為簡單。但是 Idempotency 實現(xiàn)并不總是很容易。
- Create 類型的 idempotency 創(chuàng)建的 Idempotency,多次調(diào)用容易出現(xiàn)重復(fù)創(chuàng)建,為實現(xiàn)冪等性,常見的做法是使用一個 client-side generated de-deduplication token(客戶端生成的唯一ID),在反復(fù)重試時使用同一個Unique ID,便于服務(wù)端識別重復(fù)。
- Update類型的Idempotency,更新值(update)類型的API,應(yīng)該避免采用"Delta"語義,以便于實現(xiàn)冪等性。對于更新類的操作,我們再簡化為兩類實現(xiàn)方式: Incremental(數(shù)量增減),如 IncrementBy (3)這樣的語義;SetNewTotal(設(shè)置新的總量)。
IncrementBy 這樣的語義重試的時候難以避免出錯,而 SetNewTotal(3)(總量設(shè)置為x)語義則比較容易具備冪等性。
當(dāng)然在這個例子里面,也需要看到,IncrementBy 也有優(yōu)點,即多個客戶請求同時增加的時候,比較容易并行處理,而 SetTotal 可能導(dǎo)致并行的更新相互覆蓋(或者相互阻塞)。
這里,可以認(rèn)為 更新增量和_設(shè)置新的總量_這兩種語義是不同的優(yōu)缺點,需要根據(jù)場景來解決。如果必須優(yōu)先考慮并發(fā)更新的情景,可以使用_更新增量_的語義,并輔助以 Deduplication token 解決冪等性。
- Delete 類型 idempotency : Delete 的冪等性問題,往往在于一個對象被刪除后,再次試圖刪除可能會由于數(shù)據(jù)無法被發(fā)現(xiàn)導(dǎo)致出錯。這個行為一般來說也沒什么問題,雖然嚴(yán)格意義上不冪等,但是也無副作用。如果需要實現(xiàn)Idempotency,系統(tǒng)也采用了 Archive->Purge 生命周期的方式分步刪除,或者持久化 Purge log 的方式,都能支持冪等刪除的實現(xiàn)。
Compatibility 兼容
API的變更需要兼容,兼容,兼容!重要的事情說三遍。這里的兼容指的是向后兼容,而兼容的定義是不會 Break 客戶端的使用,也即老的客戶端能否正常訪問服務(wù)端的新版本(如果是同一個大版本下)不會有錯誤的行為。這一點對于遠(yuǎn)程的API(HTTP/RPC)尤其重要。關(guān)于兼容性,已經(jīng)有很好的總結(jié),例如【4】提供的一些建議。
常見的不兼容變化包括(但不限于):
- 刪除一個方法、字段或者enum的數(shù)值
- 方法、字段改名
- 方法名稱字段不改,但是語義和行為的變化,也是不兼容的。這類比較容易被忽視。 更具體描述可以參加【4】。
另一個關(guān)于兼容性的重要問題是,如何做不兼容的API變更?通常來說,不兼容變更需要通過一個 Deprecation process,在大版本發(fā)布時來分步驟實現(xiàn)。關(guān)于Deprecation process,這里不展開描述,一般來說,需要保持過去版本的兼容性的前提下,支持新老字段/方法/語義,并給客戶端足夠的升級時間。這樣的過程比較耗時,也正是因為如此,我們才需要如此重視API的設(shè)計。
有時,一個面向內(nèi)部的 API 升級,往往開發(fā)的同學(xué)傾向于選擇高效率,采用一種叫”同步發(fā)布“的模式來做不兼容變更,即通知已知的所有的客戶端,自己的服務(wù)API要做一個不兼容變更,大家一起發(fā)布,同時更新,切換到新的接口。這樣的方法是非常不可取的,原因有幾個:
- 我們經(jīng)常并不知道所有使用 API 的客戶
- 發(fā)布過程需要時間,無法真正實現(xiàn)“同步更新”
- 不考慮向后兼容性的模式,一旦新的 API 有問題需要回滾,則會非常麻煩,這樣的計劃八成也不會有回滾方案,而且客戶端未必都能跟著回滾。
因此,對于在生產(chǎn)集群已經(jīng)得到應(yīng)用的API,強烈不建議采用“同步升級”的模式來處理不兼容API變更。
Batch mutations 批量更新
批量更新如何設(shè)計是另一個常見的API設(shè)計決策。這里我們常見有兩種模式:
- 客戶端批量更新
- 服務(wù)端實現(xiàn)批量更新。 如下圖所示。
API的設(shè)計者可能會希望實現(xiàn)一個服務(wù)端的批量更新能力,但是我們建議要盡量避免這樣做。除非對于客戶來說提供原子化+事務(wù)性的批量很有意義( all-or-nothing),否則實現(xiàn)服務(wù)端的批量更新有諸多的弊端,而客戶端批量更新則有優(yōu)勢:
- 服務(wù)端批量更新帶來了API語義和實現(xiàn)上的復(fù)雜度,例如當(dāng)部分更新成功時的語義、狀態(tài)表達(dá)等。
- 即使我們希望支持批量事物,也要考慮到是否不同的后端實現(xiàn)都能支持事務(wù)性。
- 批量更新往往給服務(wù)端性能帶來很大挑戰(zhàn),也容易被客戶端濫用接口。
- 在客戶端實現(xiàn)批量,可以更好的將負(fù)載由不同的服務(wù)端來承擔(dān)(見圖)。
- 客戶端批量可以更靈活的由客戶端決定失敗重試策略。
Be aware of the risks in full replace 警惕全體替換更新模式的風(fēng)險
所謂 Full replacement 更新,是指在 Mutation API 中,用一個全新的Object/Resource 去替換老的 Object/Resource 的模式。
API寫出來大概是這樣的:
UpdateFoo(Foo newFoo);這是非常常見的 Mutation 設(shè)計模式。但是這樣的模式有一些潛在的風(fēng)險作為 API 設(shè)計者必須了解。
使用 Full replacement 的時候,更新對象 Foo 在服務(wù)端可能已經(jīng)有了新的成員,而客戶端尚未更新并不知道該新成員。服務(wù)端增加一個新的成員一般來說是兼容的變更,但是,如果該成員之前被另一個知道這個成員的client設(shè)置了值,而這時一個不知道這個成員的 client 來做 full-replace,該成員可能就會被覆蓋。
更安全的更新方式是采用 Update mask,也即在 API 設(shè)計中引入明確的參數(shù)指明哪些成員應(yīng)該被更新。
UpdateFoo {Foo newFoo; boolen update_field1; // update maskboolen update_field2; // update mask }或者 update mask 可以用 repeated "a.b.c.d“這樣方式來表達(dá)。
不過由于這樣的 API 方式維護和代碼實現(xiàn)都復(fù)雜一些,采用這樣模式的 API 并不多。所以,本節(jié)的標(biāo)題是 “be aware of the risk“,而不是要求一定要用 update mask。
Don't create your own error codes or error mechanism 不要試圖創(chuàng)建自己的錯誤碼和返回錯誤機制
API 的設(shè)計者有時很想創(chuàng)建自己的 Error code,或者是表達(dá)返回錯誤的不同機制,因為每個 API 都有很多的細(xì)節(jié)的信息,設(shè)計者想表達(dá)出來并返回給用戶,想著“用戶可能會用到”。但是事實上,這么做經(jīng)常只會使API變得更復(fù)雜更難用。
Error-handling 是用戶使用 API 非常重要的部分。為了讓用戶更容易的使用 API,最佳的實踐應(yīng)該是用標(biāo)準(zhǔn)、統(tǒng)一的 Error Code,而不是每個 API 自己去創(chuàng)立一套。例如 HTTP 有規(guī)范的 error code 【7】,Google Could API 設(shè)計時都采用統(tǒng)一的Error code 等【5】。
為什么不建議自己創(chuàng)建 Error code 機制?
- Error-handling 是客戶端的事,而對于客戶端來說,是很難關(guān)注到那么多錯誤的細(xì)節(jié)的,一般來說最多分兩三種情況處理。往往客戶端最關(guān)心的是"這個error 是否應(yīng)該重試( retryable )"還是應(yīng)該繼續(xù)向上層返回錯誤,而不是試圖區(qū)分不同的 error 細(xì)節(jié)。這時多樣的錯誤代碼機制只會讓處理變得復(fù)雜。
- 有人覺得提供更多的自定義的 error code 有助于傳遞信息,但是這些信息除非有系統(tǒng)分別處理才有意義。如果只是傳遞信息的話,error message 里面的字段可以達(dá)到同樣的效果。
More
更多的Design patterns,可以參考[5] Google Cloud API guide,[6] Microsoft API design best practices等。不少這里提到的問題也在這些參考的文檔里面有涉及,另外他們還討論到了像versioning,pagination,filter等常見的設(shè)計規(guī)范方面考慮。這里不再重復(fù)。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的深度 | API 设计最佳实践的思考的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一位技术校招生在支付宝的成长笔记
- 下一篇: 如何实现7*24小时灵活发布?阿里技术团