應用程序編程接口(API)設計與實踐的借鑒與思考
從md復制過來沒有格式了,可以到鏈接查看
https://gitee.com/cuiheCN/api-design/blob/master/%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E7%BC%96%E7%A8%8B%E6%8E%A5%E5%8F%A3%EF%BC%88API%EF%BC%89%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E8%B7%B5%E7%9A%84%E5%80%9F%E9%89%B4%E4%B8%8E%E6%80%9D%E8%80%83.md?
應用程序編程接口(API)設計與實踐的借鑒與思考
前言
眾所周知,當今世界運行在各式各樣的系統(tǒng)上,大多系統(tǒng)由多個模塊或子系統(tǒng)組成。在模塊或子系統(tǒng)交互時一定會涉及到應用程序編程接口(API)。API設計得好可以讓系統(tǒng)后續(xù)發(fā)展順利并且維護方便。本文內容包括API的評價要素與API的設計方法。
誠然想用一套API設計的指導原則套用在五花八門的場景上是不容易的,下面的內容是我結合同事們的意見并參考網上資料總結出來的,希望能在大多數(shù)場景中給API的設計人員一些幫助。鑒于API的設計藝術和技藝總是有新的提升空間并且我們水平有限,所以本文提到的內容也不是固定不變的完美狀態(tài),若您有相關的指導意見或改進建議歡迎告知作者。
成功的系統(tǒng)可能沒有什么閃光點,但是設計師點點滴滴的努力隨處可見。
API的評價要素
清晰易懂的思維模型
API設計最難的部分不是解決系統(tǒng)或模塊之間的交互,而是背后的設計者、開發(fā)者、維護者、使用者之間的溝通與理解。若能在API的設計中體現(xiàn)清晰易懂的思維模型則能讓不同角色快速達成共識,減少出錯提升效率。
簡潔而不簡陋
有時候我們普遍會將API設計得過于復雜,即引入了過多的實現(xiàn)細節(jié)。有時候我們會將問題過度抽象,即不該合并的概念合并了。API涉及的概念應該是即易于理解又簡潔清晰。
完整
API應支持在其基礎上構建完整的應用程序,所有預期的功能都能實現(xiàn)。
容許多個實現(xiàn),也說松耦合或正交性
如果一個API可以有多個完全不同的實現(xiàn),一般來說其已經具備了足夠的抽象也與外部系統(tǒng)或模塊實現(xiàn)了松耦合。
導致可讀性強的代碼
因為代碼被閱讀的時間遠比編寫他們的時間多,所以代碼的可讀性很重要。API設計得好會產生可讀性強的調用代碼。
在不同服務,使用相同的方法,是一種良好的體驗。
API的設計方法
一段佳話 —— POSIX File API
POSIX File API設計于1988年前后,距今30多年。期間計算機硬件和軟件系統(tǒng)發(fā)展了好幾代,F(xiàn)ile API的核心保持了穩(wěn)定。File API提供了清晰的概念模型,幾乎每個用過它的人都能理解其所描述的概念——文件、打開、關閉、讀、寫、路徑。File API支持多種不同實現(xiàn),例如磁盤、管道、內存、網絡、終端等。
更多信息查看IEEE 1003、IEEE 2003、ISO/IEC 9945相關標準。
python中的文件方法
面向“資源”的API設計流程推薦
確定API提供些哪些資源
按照類型分類,確定資源之間的關系(從屬、聚合等)
根據(jù)上述兩個信息,確定資源的命名(car、fuelCar、electricCar)
明確資源要素(ID,屬性)
給資源附加最少的操作方法(增刪改查)
寫文檔
API文檔的重要性不言而喻————當今技術向服務化甚至微服務化發(fā)展,單個應用可能依賴多個服務模塊或子系統(tǒng),每個服務模塊或子系統(tǒng)又在不斷演進。若有準確及時的API文檔則對客戶端開發(fā)調試幫助極大,減少出問題的幾率,提升整體研發(fā)效率。
常見文檔內容:他是什么?成功是什么情況?失敗是什么情況?什么情況下導致失???冪等么?單位是什么?正常值的范圍?副作用?如何使用?常見調用錯誤?什么情況下可以使用(總是存在么)?默認值是多少?默認值是什么含義?
若接口是動名詞描述的,那么文檔也應該是動名詞描述的。
描述資源的文檔應該圍繞資源描述。
描述參數(shù)或字段的可以描述其數(shù)量,值的含義,是否已棄用,描述邊界信息,描述默認值行為,描述其允許的字符,提供可能的示例值,是否必須等。
描述方法或操作的應該說明操作的對象或資源,會產生什么效果。例如:列出用戶的日歷事件。刪除一個位置記錄。
面向“資源”的API設計
資源是API中核心對象的一個抽象。今天“資源”+“操作”這種API設計模式幾乎成為業(yè)界共識。也可以描述成“數(shù)據(jù)模型”+“功能”
例如File API中將文件作為操作的資源。其含義是“可以由一個字符串唯一標識的數(shù)據(jù)記錄”。其中標識方法不甚關鍵,組織結構存儲結構也不在其中。
一般來講,根據(jù)業(yè)務抽象的資源概念應該和日常生活中的概念相似,這樣可以幫助人們更好地建立思維模型。
抽象深度合適
在能滿足需求的情況下,抽象越淺越好。
概念命名
包括接口、資源、集合、方法、消息的命名應該簡單、直觀、一致。
只有在表示一組資源時才使用復數(shù)。
使用美式英語單詞。
使用熟悉的詞,正例delete、remove,反例erase、destory。
對同一概念使用同一單詞,對不同概念使用不同單詞。
使用簡潔的命名但避免籠統(tǒng)的命名。一般使用定語+名詞。反例element、entry、instance、item、object、resource、type、value。正例rowValue。
盡量避免與編程語言中保留字發(fā)生沖突。
資源全名不加版本信息,URL包括版本信息。例
//這是一個日歷事件資源名稱
"//calendar.googleapis.com/users/john smith/events/123"
//這是對應的HTTP URL
"https://calendar.googleapis.com/v3/users/john%20smith/events/123"
用名詞描述資源地址。
產品名,應該由公司市場團隊和產品團隊來命名以保證公司產品名的一致性。 服務名,應符合域名規(guī)范,以確保其可以被解析成網絡地址。 包名,應當盡可能使用產品名+服務名+包名+版本。 集合名,小駝峰復數(shù)形式。 接口名,避免與編程語言的保留字或運行庫沖突,可以使用Api或者Service后綴解決沖突。 方法名,小駝峰,動名詞。動詞時操作,名詞是資源。 消息名,建議附帶Request和Response后綴。 枚舉名,枚舉類型用大駝峰,枚舉值用下劃線分割的全大寫,記得寫默認枚舉值或者未指定枚舉值。 字段名,可以使用全小寫下劃線分割。以符合認知的情況使用復數(shù)形式。表示時間的字段使用time、time_seconds、duration、delay等結尾并且不使用過去式,反例updated_time.日期和時刻使用data結尾。表示數(shù)量的需要包含計量單位以meters、bytes等結尾。表示過濾語義的以filter結尾。 縮寫。只能使用熟知的約定俗成的縮寫,即使這樣在文檔中也應寫明全稱。config,configuration。id,identifier。
資源的標識(ID)設計
資源標識是暴露給用戶的,一般會有三種考慮:字符串作為ID,結構化數(shù)據(jù)作為ID,數(shù)字作為ID。
暫時沒有選擇原則,提供幾個例子供參考:
FIle API中使用了字符串作為文件的標識,這樣容許了Windows系統(tǒng)和Linux系統(tǒng)使用不同的路徑標識其文件。
某銀行系統(tǒng)使用{銀行,存款人標識}這樣的結構化數(shù)據(jù)來表示轉賬賬號
慎用數(shù)字id,除非你考慮過這三個問題:其余兩種方法都不好用?64位整數(shù)夠么?對用戶來講數(shù)字id友好么?
資源的操作在概念上(直覺上)是合理的
前提,資源的定義與抽象是合理的,才能考慮對資源的操作是否是合理的。
標準操作有List、Get、Create、Update和Delete。標準操作都應給予用戶返回,刪除操作返回一個空對象,讓開發(fā)人員確切的掌握接口狀態(tài)。
“操作”+“資源”連起來要符合直覺,聽起來自然。
若是一個耗時操作,應先返回一個統(tǒng)一的耗時對象表示已接收。讓客戶端可以根據(jù)耗時對象獲取進度和結果。
寫入和更新等操作應具備冪等性
冪等性是指多次操作結果和單次操作結果一致。
這種設計的好處顯而易見——客戶端可以簡單地安全的進行重試。
創(chuàng)建類的冪等性實現(xiàn)一般由客戶端創(chuàng)建一個操作id,服務端可以根據(jù)操作id識別重復的操作。當識別到重復的創(chuàng)建操作時應返回第一次成功的信息,因為客戶端可能沒有收到成功的信息。
更新類的操作應該避免使用“Delta”(變化量)語義的接口而應使用“set”(設置成新值)。當然我們也應知道set語義在多客戶端并發(fā)請求時不如delta,應根據(jù)使用情況來定到底是用哪個語義。
刪除類的冪等性一般沒什么問題。也可以使用先獲取再清除這樣的思路。
版本與兼容性
兼容指的是向后兼容,老客戶端訪問同一個主版本的新服務端不會出現(xiàn)錯誤的行為。
這里有一些反面例子:刪除一個方法、字段等。方法、字段等改名。方法名不變但語義或行為變化。
如果一定要發(fā)布不兼容的變更,建議在大版本發(fā)布時發(fā)布不兼容的變更并且保留老接口。老接口標記Deprecation,給客戶端足夠的升級時間。也要記得在過渡期后刪除老接口。
反面例子:通知所有使用服務端API的客戶端同步做一次不兼容更新。難以回退,難以通知所有客戶端,發(fā)布不同步。
一般來講,服務不設版本號,API接口設置版本號。
版本號應該使用語義化 主版本號.小版本號.補丁號。當有不兼容修改時,增加主版本號。當有兼容的功能修改時,增加小版本號。當有兼容的錯誤修復時,增加補丁號。只有主版本號可以編碼在程序包中,小版本號和補丁號寫在配置文件和文檔中。
客戶端批量操作
盡量避免服務端批量操作的API,除非在業(yè)務上服務端做批量非常具有意義,否則應該在客戶端做批量。
服務端批量操作會增加語義和實現(xiàn)的復雜度。例如部分操作成功如何返回,狀態(tài)如何表達。
服務端批量難以容許多種實現(xiàn)。
服務端批量容易被客戶端濫用,給服務器帶來挑戰(zhàn)。
客戶端批量可以做負載均衡。
客戶端批量可以按需做失敗重試策略。
警惕替換式更新
反例看起來是這樣的UpdateFoo(Foo newFoo);
服務端可能已經對此對象做了兼容性更新而客戶端沒有更新,造成錯誤。
多個客戶端更新時互相覆蓋。
推薦操作是在API中引入明確的參數(shù)field指明哪些成員應該被更新。代碼看起來是這樣的:
UpdateFoo {
? Foo newFoo;?
? Object field; // update mask
}
使用已有的錯誤碼
推薦使用已有的標準的錯誤碼,例如HTTP規(guī)范的錯誤碼——200 OK 沒有錯誤,404 NOT_FOUND 找不到指定資源等等。
錯誤處理是客戶端的事情。但是不推薦讓客戶端每API處理超過三種錯誤。
客戶端一般只關心三種錯誤:1 是否需要重試;2 是否需要向上層繼續(xù)拋出;3 做一些其他事。
優(yōu)先考慮使用已有的錯誤碼,將不同的錯誤內容寫到錯誤消息中。
不要假定用戶是API專家,不要假定用戶了解上下文,如果可能應構造錯誤消息以幫助技術用戶相應錯誤,保持錯誤消息簡練要不提供獲取更多信息的鏈接。別忘了錯誤消息本地化。
服務端傳播錯誤時,注意隱藏實現(xiàn)信息和機密信息并且調整錯誤的發(fā)生方為自己。反例"客戶端IP地址不在白名單128.0.0.0/8"。正例"Resource 'xxx' not found." "Resource 'xxx' already exists."
關于http錯誤碼可閱讀:https://httpwg.org/specs/rfc7231.html
其他設計
通過搜索等手段獲取的資源,返回時也應返回資源本身路徑而非搜索路徑。
若允許客戶端提交排序請求,則排序字符串應符合SQL語法。
若允許客戶端請求驗證,則需要返回驗證結果再考慮請求結果。
枚舉值記得定義默認值,一般為0值。
若需要API接收語法內容,則使用ISO 14977的句法來定義語法。(沒有接觸過)
避免使用無符號整型,使用有符號整型但是負數(shù)含義特殊時應記錄在文檔,例如-1表示失敗。
在大多數(shù)返回時都可以進行一個返回前過濾,可選參數(shù)field。
出于對流量的考慮,一些請求資源的接口可以設計其輕量版本。(感覺和上面的field類似)
如果需要可以為資源創(chuàng)建指紋,在請求時附帶資源指紋以利用緩存。(不是很明白,進行弱校驗)
若客戶端的請求的參數(shù)多了,應忽略。若這些多的參數(shù)是服務器返回的參數(shù),應覆蓋。
在創(chuàng)建對象時,其屬性應該是正交的。
其他提醒
不要拼錯單詞。
保持方法的對稱性,有add就有delete或remove,有open就有close。
站在使用者視角,提高用戶體驗。
不要使用過多參數(shù)導致難以理解,反例
?QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");
正例
QSlider *slider = new QSlider(Qt::Vertical);
?slider->setRange(12, 18);
?slider->setPageStep(3);
?slider->setValue(13);
?slider->setObjectName("volume");
不推薦使用布爾值作為參數(shù),因為它會混淆否定操作還是否定資源或是否定某個參數(shù),反例
?widget->repaint(false); // 重繪還是不重繪?還是重繪窗口不擦除背景?
正例
?widget->repaint(); // 重繪
?widget->repaintWithoutErasing(); // 重繪不擦除