當前的計算機領(lǐng)域,無(wú)論廣度還是深度,已經(jīng)沒(méi)有一個(gè)人能完全掌握了。但是,通過(guò)各種中間層的組合使用,我們不需要了解其內部細節,也可以像搭積木一樣,開(kāi)發(fā)出各種有趣的服務(wù)和應用。
而各個(gè)中間層之所以能組合工作,正是因為大家都通過(guò)定義好的 API 交互和通信。每個(gè)模塊在對外提供經(jīng)過(guò)抽象 API 的同時(shí),也需要使用其他模塊的 API 作為自身運行的基礎。
今天我們來(lái)聊聊融云在設計 API 過(guò)程保障穩定性的一些實(shí)踐。
無(wú)處不在的 API
API(Application Programming Interface) 又稱(chēng)為應用編程接口。
而接口,本質(zhì)可以理解為契約,一種約定。
計算機接口的概念起源于硬件。早期各家研發(fā)的各種元器件都不通用也沒(méi)有標準,相互使用非常困難,于是大家約定了功能和規格,就產(chǎn)生了接口,后來(lái)蔓延到軟件中。
接口蔓延到軟件之后,又分為ABI(Application Binary Interface)和API(Application Programming Interface) 。
前者主要約定了二進(jìn)制的運行和訪(fǎng)問(wèn)的規則,后者則專(zhuān)注于邏輯模塊的交互。本文以下內容僅討論開(kāi)發(fā)者經(jīng)常接觸的 API。
很多人對 API 的印象只是包含一些函數的 Class 或 頭文件。但 API 在我們生活中無(wú)處不在,只是我們有時(shí)并沒(méi)有注意到。
比如,當我們在撥打電話(huà)時(shí),手機和基站通信的整個(gè)系統是非常復雜的。

好在我們不需要了解內部的細節,僅需要把 11 位的電話(huà)號碼傳給“電話(huà)系統”的接口就可以,而隱藏的國家區號(如+86)可以理解為接口的默認參數。
這個(gè)高度抽象的 API 背后,隱藏了非常多的細節。借助上面的中間層理論,我們可以系統性地討論設計一個(gè) API 所需要考慮哪些內容。

1. 模塊對上層暴露的 API 如何被使用?
API 從使用的耦合方式上,可以分為兩類(lèi):一種是通過(guò)協(xié)議調用,如調用 HTTP 接口;另一種是語(yǔ)言直接通過(guò)聲明調用。
如設計 HTTP Restful API 時(shí),并不需要關(guān)心使用者的操作系統、使用的編程語(yǔ)言、內存線(xiàn)程管理等,因此會(huì )比后者簡(jiǎn)單一些。
API 從使用者的規模和可控范圍上,可以分為 LSUD(Larget Set of Unkown Developers) 和 SSKD(Small Set of Kown Developers) 兩種。
前者一般都是公網(wǎng)開(kāi)放的云服務(wù),任何開(kāi)發(fā)者都可以使用,無(wú)法提前預知以何種姿勢被使用,版本也不可控制。融云提供的通信云就是這種 API。
后者用戶(hù)群有限,一般都在同一家公司或團隊內。比如前段時(shí)間比較火的組件化,即對內提供的模塊化 API,使用范圍和方式均可控,在更新時(shí)一般不用太糾結向后兼容。
API 的第一受眾是人,然后才是機器,所以“可理解性”在設計時(shí)需要優(yōu)先考慮。
而良好的 API 文檔、簡(jiǎn)單扼要的 Demo、關(guān)鍵的 log,可以提升 API 使用者的體驗。
2. API 所屬模塊對下層有什么依賴(lài)?
API 所屬模塊都運行在一定的地址空間中。而其中的環(huán)境變量、加載庫、內存和線(xiàn)程模型、系統和語(yǔ)言特性都需要考慮。
3. API 所屬模塊的內部實(shí)現對其他層有什么影響?
一般而言,設計良好的 API 在使用時(shí),并不需要理解其內部實(shí)現。但如果能了解其內部架構并輔助關(guān)鍵 log,有助于提升使用 API 的效率。
并且模塊的內部實(shí)現,有時(shí)也會(huì )影響到 API 設計的風(fēng)格。
如一個(gè)強依賴(lài) IO 的接口,可能需要使用異步的方式。大量異步的方式,就衍生出了 RxJava 等框架。
向后兼容
因為 API 如此重要,涉及的范圍又如此廣泛,廣大開(kāi)發(fā)者對 API 的向后兼容可以說(shuō)要求非常高。
畢竟誰(shuí)也不想在開(kāi)發(fā)過(guò)程中,頻繁的更新接口和代碼,想想《 swift 從入門(mén)到精通到再次入門(mén)到再再次入門(mén)》的慘案就心有余悸。
我們不僅問(wèn),為什么很多公司或者項目都無(wú)法向后兼容,僅僅是投入不夠或不夠重視,還是說(shuō) 100% 的向后兼容實(shí)際就是不可能的?
假設設計是理想和經(jīng)過(guò)論證的,正如一個(gè)完美的圓圈。
設計是要落實(shí)到編碼中的,而編碼的過(guò)程中總是不可避免的引入一些 bug,而帶著(zhù) bug 的某個(gè)版本實(shí)現,其實(shí)正如一個(gè) Amoeba 變形蟲(chóng),形態(tài)是不固定的。而隨著(zhù)版本不斷演進(jìn),不可避免會(huì )產(chǎn)生一定的差異。
第一個(gè)版本實(shí)現:

第二個(gè)版本實(shí)現:

所以說(shuō) 100% 向后兼容本身就是不可能的。
因此,大家平時(shí)在談?wù)?API 穩定性時(shí),其實(shí)默認是可以包含一定程度變更的。
但由于 API 涉及的范圍太廣泛,保障向后兼容都需要極大代價(jià)。
比如 Linux 就希望快速迭代,完全不保證 API 的穩定性。針對這個(gè)問(wèn)題,Linux 還特意寫(xiě)了 stable-api-nonsense 文檔。有興趣的可以點(diǎn)擊閱讀:
stable-api-nonsense.rst
漸進(jìn)式改進(jìn)
所以說(shuō),保障 API 的穩定性會(huì )面臨很多挑戰,比如:
- 業(yè)務(wù)形態(tài)還不穩定,還在高速發(fā)展
- 業(yè)務(wù)和 API 歷史包袱較重
- 多個(gè)平臺和語(yǔ)言的特性不一致
- 用戶(hù)群和使用方式不明確
我們回顧一下正常的開(kāi)發(fā)流程,看看是否能通過(guò)一些指標和工具,改善 API 的穩定性,主要涉及:需求、設計、編碼、Review、測試、發(fā)布、反饋等步驟。
需求
普通的產(chǎn)品開(kāi)發(fā),在啟動(dòng)的時(shí)候,用戶(hù)需求都比較明確,但對于 LSUD 的云服務(wù)而言,無(wú)法提前預知用戶(hù)群都有哪些,以及用戶(hù)在他的產(chǎn)品中如何使用 API。
這容易造成,沒(méi)有明確的用戶(hù)需求,API 就不好進(jìn)行設計和迭代,沒(méi)有設計就沒(méi)有用戶(hù),需求更無(wú)從談起。這是一個(gè)雞生蛋、蛋生雞的問(wèn)題。
建議可以在 API 發(fā)布之前,內部先針對典型的使用場(chǎng)景,設計幾個(gè)完整的 Demo,驗證 API 的設計和使用是否合理。
需要注意的是,Demo 需要有完整應用場(chǎng)景,達到上架地步,如果能內部使用, Eating your own dog food 最好,過(guò)于簡(jiǎn)單的 Demo 無(wú)法提前暴露 API 的使用問(wèn)題。
Demo 的開(kāi)發(fā)人員最好與 API 的設計者有所區分,避免思維固化,更多內容大家可以參照 Rust 語(yǔ)言開(kāi)發(fā)在自舉過(guò)程中的一些實(shí)踐。
設計
在設計 API 的時(shí)候,有很多需要注意的點(diǎn)和普通開(kāi)發(fā)不太一樣。
普通開(kāi)發(fā),快速實(shí)現功能始終被放在第一位。比如大家會(huì )用一些敏捷開(kāi)發(fā)的方式,優(yōu)先實(shí)現功能再快速迭代等。
但 API 設計時(shí),接口無(wú)法頻繁變更,所以首先需要考慮的是“少”,少即是多。
每個(gè) API 做的事情要少
一個(gè)接口只做一件事,把這個(gè)事情做好就足夠了。
需要避免為了討好某個(gè)場(chǎng)景,在一個(gè) API 上進(jìn)行復雜的組合邏輯,提供一個(gè)類(lèi)似語(yǔ)法糖的接口。否則,場(chǎng)景的業(yè)務(wù)自身在演進(jìn)時(shí),很難保證 API 的行為不變。
如果需要支持多種業(yè)務(wù),可以考慮將 API 分層,比如融云客戶(hù)端的 API 會(huì )分為下面幾層。

舉個(gè)例子,融云考慮通用性,基于訂閱分發(fā)的模型,抽象了 RTCLib,客戶(hù)端能處理媒體的任意流,非常的靈活,但是對于用戶(hù)而言開(kāi)發(fā)代價(jià)可能高些,要思考和做的工作比較多。
考慮到大量的用戶(hù),其實(shí)需要的是音視頻通話(huà)的業(yè)務(wù),基于 RTCLib,融云分裝了不帶 UI 的 CallLib 以及集成了 UI 的 CallKit。
如果一個(gè)用戶(hù),需求和微信的音視頻通話(huà)類(lèi)似,可以集成帶 UI 界面的 CallKit,開(kāi)發(fā)效率會(huì )非常高;
如果用戶(hù)對通話(huà)音視頻通話(huà) UI 的交互有大量需求,可以基于 CallLib 進(jìn)行開(kāi)發(fā),對 UI 可以進(jìn)行各種定制。
暴露的信息要少
成熟的 API 設計者都會(huì )盡可能的隱藏內部實(shí)現細節。
比如字段不應該直接暴露而是通過(guò) Getter/Setter 提供,不需要的類(lèi)、方法、字段都應該隱藏,都已經(jīng)成為各個(gè)語(yǔ)言的基礎要求,在此就不細述了。
但容易被忽略的一點(diǎn)需要提醒大家,應盡量隱藏技術(shù)棧的信息。
比如:API http://api.example.com/cgi-bin/
get_user.php?user=100,就明顯混入了很多無(wú)用的信息,并且以后技術(shù)切換升級想維持 API 穩定非常麻煩。
行為擴散要少
在語(yǔ)言直接調用的 API 中,需要避免基礎接口通過(guò)繼承導致行為擴散。
在普通的編碼過(guò)程中,抽象類(lèi)和繼承都是面向對象的強大武器。但是對于 API,更建議通過(guò)組合使用。
比如一個(gè)管理生命周期的類(lèi),如果被繼承,子類(lèi)有些行為就有可能被修改而導致出錯。這時(shí)候建議使用 Interface + 工廠(chǎng)的方法提供實(shí)例。
由于 Java 8 之前 interface 沒(méi)有 default 實(shí)現,為了避免增加功能需要頻繁修改接口,可以使用 final class。
Objetive-C則可以使用 __attribute__ ((objc_subclassing_restricted))和__attribute__((objc_requires_super))控制子類(lèi)繼承行為。
畫(huà)風(fēng)切換要少
API 命名要做到多個(gè)平臺的業(yè)務(wù)命名統一,與每個(gè)平臺的風(fēng)格統一。
這點(diǎn) HTTP 的接口要簡(jiǎn)單一些,只需要選定一種風(fēng)格即可,Restful 或者 GraphQL 或者自己定義。
語(yǔ)言調用的 API 命名,建議首先遵循平臺的風(fēng)格,然后再是參考語(yǔ)言標準,最后才考慮團隊的風(fēng)格。
比如:iOS 平臺的 API 開(kāi)發(fā),需要首先參照 iOS 的命名風(fēng)格,did 和 will 之類(lèi)的時(shí)態(tài)就非常有特色。
命名上細節較多,詞匯、時(shí)態(tài)、單復數、介詞、?小寫(xiě)、同步異步風(fēng)格等都需要考量,需要長(cháng)時(shí)間的積累。
理解成本要少
一般 API 每個(gè)接口都會(huì )有相應的注釋說(shuō)明,但是值得注意的是,大部分開(kāi)發(fā)者并不看注釋。
大部分開(kāi)發(fā)者對接口的了解,都僅源于 IDE 的補全和提醒。一個(gè)接口看著(zhù)像就直接用,不行再換一個(gè)試試,這其實(shí)是一種經(jīng)驗式編程的方式。也就意味著(zhù)接口命名需要提高可理解性。有一個(gè)辦法可以驗證,將接口的所有注釋抹掉,使用者能否非常直接的看懂每個(gè)接口的含義。如果很困難,則需要改進(jìn)。
API 設計還有一處和普通開(kāi)發(fā)不太一致。普通開(kāi)發(fā)設計好架構即可,每個(gè)模塊的開(kāi)發(fā)可能是同一個(gè)人,接口并不需要在設計時(shí)確定下來(lái)。但是 API 的設計階段,需要進(jìn)行 Review 并直接確定接口的設計,以保證多端在開(kāi)發(fā)時(shí)遵循完全一直的規則。
編碼
在 API 的編碼過(guò)程中,有以下幾點(diǎn)需要注意。
1、在 API 中,預定義好版本號。
這個(gè)主要是針對 HTTP API,如:
http://api.example.com/v1/users/12345?fields=name,age。
如果目前僅有一個(gè)版本,也可以暫時(shí)不加,第二版時(shí)再區分。
2、注意 API 版本檢查。
當分層提供多種 API 時(shí),每層 API 需要在啟動(dòng)時(shí),先校驗一下版本號,避免不匹配的情況。
比如在以下 Java 代碼中,大家可能覺(jué)得判斷版本號相等的代碼非常奇怪,應該永遠是 true 才對。

但是抽象類(lèi)和實(shí)現類(lèi)出現在不同的分層模塊中,并且實(shí)現類(lèi)先編譯,抽象類(lèi)版本更新后再編譯,就會(huì )出現不一致的情況。有很多語(yǔ)言或平臺能提供類(lèi)似的方式來(lái)確定版本。
3、提供規范性的 log 輸出。
普通開(kāi)發(fā)的log,主要用于自己定位問(wèn)題。但是 API 在編碼時(shí),最好針對性的添加一些 log,有利于 API 的使用者理解并簡(jiǎn)單排查問(wèn)題。但出于性能考慮,需要定義好 log 的級別并可以調整。
4、注意廢棄與遷移。
當一個(gè)以前設計的 API 不再符合要求或者有重大問(wèn)題時(shí),我們可以對外標記成已廢棄,并在注釋中建議使用者遷移到另一個(gè)接口。如果是類(lèi)似的被廢棄接口,內部編碼時(shí)最好能使用新的接口來(lái)實(shí)現,以降低向后兼容的維護成本。HTTP 的 API,需要預定義好遷移的錯誤碼,比如在 HTTP 規范中,可以使用 410 Gone 說(shuō)明已經(jīng)不再支持某個(gè)接口。
Review
API 的 Review 基于普通開(kāi)發(fā)的 Code Review。如果基礎的 Code Review 都沒(méi)有做好,肯定無(wú)法保障 API 的質(zhì)量和穩定性。
可以通過(guò)一些工具,為 API 的 Review 提供一些參考報告。比如可以使用 SonarLint 分析代碼復雜度,如果接口層的代碼復雜度較高,會(huì )是一個(gè)危險的信號。還可以借助 Java 反射、Clang 語(yǔ)法分析,獲取當前的 API 接口列表,生成接口變更報告,也有利于減少無(wú)用接口的暴露。另外,自動(dòng)化工具生成的接口文檔也是 Review 重要的一環(huán)。
測試
在測試環(huán)節,我們可以通過(guò) unit test 來(lái)關(guān)注 API 的穩定性。與敏捷開(kāi)發(fā)經(jīng)常修改 test case 不同,API 的 test case 基本代表了接口的穩定性。所以在修改舊 case 時(shí)需要特別明確,是 case 自身的 bug 還是接口行為發(fā)生了變更。
發(fā)布
我們可以通過(guò)區分 dev 和 stable 版本,為不同階段的開(kāi)發(fā)者提供更好的體驗。
dev 版本包含最新的功能,但是 API 接口有變更風(fēng)險。stable 版本 API 穩定,但功能不一定是最新的。如果開(kāi)發(fā)者還在開(kāi)發(fā)過(guò)程中,可以選用最新的 dev 版本,基于最新 API 開(kāi)發(fā)。如果應用已經(jīng)上線(xiàn),可以選擇升級直接到最新的 stable 版本。
反饋
由于前面提到的,云服務(wù)的 API 比較難確定用戶(hù)群和用戶(hù)的使用方式。可以參考 APM(Application Performance Management) 的方式,記錄熱點(diǎn) API 使用情況,為后續的優(yōu)化提供數據。
總結
上面的改進(jìn),讓保障 API 的穩定性變得更容易。下面以融云 IMLib iOS SDK 2.0 版本演進(jìn)為例,歷盡 2015至 2019 四年時(shí)間,從 2.2.5 到 2.9.16 共 98 個(gè)版本。API 接口數量翻了一番,考慮到接口更內聚,功能大約增加了 3 倍。

但是需要用戶(hù)遷移的接口非常少,即使遷移時(shí)開(kāi)發(fā)成本都非常低。


下面小編來(lái)考考大家,答題還有禮品拿哦
參與方式:
- 在文末下方留言區寫(xiě)下你的答案
- 獲得點(diǎn)贊排名前5的小伙伴,可獲得定制禮包(定制T恤和8G U盤(pán))
- 點(diǎn)贊沒(méi)那么高也不要灰心,小編會(huì )在精選留言中抽出5個(gè)小伙伴送出單項禮品(定制T恤)
活動(dòng)時(shí)間:
即刻起至 6 月 26 日 9:30
中獎公布:
中獎結果將于下周推文公布
快來(lái)答題吧:
① API 是不是可以做到 100% 的向后兼容?
A. 是
B. 不是
② HTTP API 如果不幸發(fā)生永久遷移,可以使用什么 HTTP 錯誤碼?
A. 302 Moved Temporarily
B. 404 Not Found
C. 410 Gone
③ 開(kāi)發(fā)者還在開(kāi)發(fā)過(guò)程中,可以選用融云的最新___版本,基于最新 API 開(kāi)發(fā)。
A. dev 版本
B. stable 版本
C. beta 版本