計算機行業有句名言 —— 計算機科學領域的任何問題,均可以經過增長一個間接的中間層來解決。php
當前的計算機領域,不管廣度仍是深度,已經沒有一我的能徹底掌握了。可是,經過各類中間層的組合使用,咱們不須要了解其內部細節,也能夠像搭積木同樣,開發出各類有趣的服務和應用。
而各個中間層之因此能組合工做,正是由於你們都經過定義好的 API 交互和通訊。每一個模塊在對外提供通過抽象 API 的同時,也須要使用其餘模塊的 API 做爲自身運行的基礎。linux
今天咱們來聊聊融雲在設計 API 過程保障穩定性的一些實踐。git
無處不在的 API
API(Application Programming Interface) 又稱爲應用編程接口。github
而接口,本質能夠理解爲契約,一種約定。
計算機接口的概念起源於硬件。早期各家研發的各類元器件都不通用也沒有標準,相互使用很是困難,因而你們約定了功能和規格,就產生了接口,後來蔓延到軟件中。編程
接口蔓延到軟件以後,又分爲 ABI(Application Binary Interface) 和 API(Application Programming Interface) 。
前者主要約定了二進制的運行和訪問的規則,後者則 專一於邏輯模塊的交互。本文如下內容僅討論開發者常常接觸的 API。swift
不少人對 API 的印象只是包含一些函數的 Class 或 頭文件。但 API 在咱們生活中無處不在,只是咱們有時並無注意到。api
好比,當咱們在撥打電話時,手機和基站通訊的整個系統是很是複雜的。微信
好在咱們不須要了解內部的細節,僅須要把 11 位的電話號碼傳給「電話系統」的接口就能夠,而隱藏的國家區號(如+86)能夠理解爲接口的默認參數。
這個高度抽象的 API 背後,隱藏了很是多的細節。藉助上面的中間層理論,咱們能夠系統性地討論設計一個 API 所須要考慮哪些內容。架構
模塊對上層暴露的 API 如何被使用?框架
API 從使用的耦合方式上,能夠分爲兩類:一種是經過協議調用,如調用 HTTP 接口;另外一種是語言直接經過聲明調用。
如設計 HTTP Restful API 時,並不須要關心使用者的操做系統、使用的編程語言、內存線程管理等,所以會比後者簡單一些。
API 從使用者的規模和可控範圍上,能夠分爲 LSUD(Larget Set of Unkown Developers) 和 SSKD(Small Set of Kown Developers) 兩種。
前者通常都是公網開放的雲服務,任何開發者均可以使用,沒法提早預知以何種姿式被使用,版本也不可控制。融雲提供的通訊雲就是這種 API。
後者用戶羣有限,通常都在同一家公司或團隊內。好比前段時間比較火的組件化,即對內提供的模塊化 API,使用範圍和方式都可控,在更新時通常不用太糾結向後兼容。
API 的第一受衆是人,而後纔是機器,因此「可理解性」在設計時須要優先考慮。
而良好的 API 文檔、簡單扼要的 Demo、關鍵的 log,能夠提高 API 使用者的體驗。
API 所屬模塊對下層有什麼依賴?
API 所屬模塊都運行在必定的地址空間中。而其中的環境變量、加載庫、內存和線程模型、系統和語言特性都須要考慮。
API 所屬模塊的內部實現對其餘層有什麼影響?
通常而言,設計良好的 API 在使用時,並不須要理解其內部實現。但若是能瞭解其內部架構並輔助關鍵 log,有助於提高使用 API 的效率。
而且模塊的內部實現,有時也會影響到 API 設計的風格。
如一個強依賴 IO 的接口,可能須要使用異步的方式。大量異步的方式,就衍生出了 RxJava 等框架。
向後兼容
由於 API 如此重要,涉及的範圍又如此普遍,廣大開發者對 API 的向後兼容能夠說要求很是高。
畢竟誰也不想在開發過程當中,頻繁的更新接口和代碼,想一想《 swift 從入門到精通到再次入門到再再次入門》的慘案就心有餘悸。
咱們不只問,爲何不少公司或者項目都沒法向後兼容,僅僅是投入不夠或不夠重視,仍是說 100% 的向後兼容實際就是不可能的?
假設設計是理想和通過論證的,正如一個完美的圓圈。
設計是要落實到編碼中的,而編碼的過程當中老是不可避免的引入一些 bug,而帶着 bug 的某個版本實現,其實正如一個 Amoeba 變形蟲,形態是不固定的。而隨着版本不斷演進,不可避免會產生必定的差別。
第一個版本實現:
第二個版本實現:
因此說 100% 向後兼容自己就是不可能的。
所以,你們平時在談論 API 穩定性時,其實默認是能夠包含必定程度變動的。
但因爲 API 涉及的範圍太普遍,保障向後兼容都須要極大代價。
好比 Linux 就但願快速迭代,徹底不保證 API 的穩定性。針對這個問題,Linux 還特地寫了 stable-api-nonsense 文檔。
有興趣的能夠點擊閱讀:stable-api-nonsense.rst
漸進式改進
因此說,保障 API 的穩定性會面臨不少挑戰,好比:
咱們回顧一下正常的開發流程,看看是否能經過一些指標和工具,改善 API 的穩定性,主要涉及:需求、設計、編碼、Review、測試、發佈、反饋等步驟。
※需求
普通的產品開發,在啓動的時候,用戶需求都比較明確,但對於 LSUD 的雲服務而言,沒法提早預知用戶羣都有哪些,以及用戶在他的產品中如何使用 API。
這容易形成,沒有明確的用戶需求,API 就很差進行設計和迭代,沒有設計就沒有用戶,需求更無從談起。這是一個雞生蛋、蛋生雞的問題。
建議能夠在 API 發佈以前,內部先針對典型的使用場景,設計幾個完整的 Demo,驗證 API 的設計和使用是否合理。
須要注意的是,Demo 須要有完整應用場景,達到上架地步,若是能內部使用, Eating your own dog food 最好,過於簡單的 Demo 沒法提早暴露 API 的使用問題。
Demo 的開發人員最好與 API 的設計者有所區分,避免思惟固化,更多內容你們能夠參照 Rust 語言開發在自舉過程當中的一些實踐。
※設計
在設計 API 的時候,有不少須要注意的點和普通開發不太同樣。
普通開發,快速實現功能始終被放在第一位。好比你們會用一些敏捷開發的方式,優先實現功能再快速迭代等。
但 API 設計時,接口沒法頻繁變動,因此首先須要考慮的是「少」,少便是多。
l 每一個 API 作的事情要少
一個接口只作一件事,把這個事情作好就足夠了。
須要避免爲了討好某個場景,在一個 API 上進行復雜的組合邏輯,提供一個相似語法糖的接口。不然,場景的業務自身在演進時,很難保證 API 的行爲不變。
若是須要支持多種業務,能夠考慮將 API 分層,好比融雲客戶端的 API 會分爲下面幾層。
舉個例子,融雲考慮通用性,基於訂閱分發的模型,抽象了 RTCLib,客戶端能處理媒體的任意流,很是的靈活,可是對於用戶而言開發代價可能高些,要思考和作的工做比較多。
考慮到大量的用戶,其實須要的是音視頻通話的業務,基於 RTCLib,融雲分裝了不帶 UI 的 CallLib 以及集成了 UI 的 CallKit。
若是一個用戶,需求和微信的音視頻通話相似,能夠集成帶 UI 界面的 CallKit,開發效率會很是高;
若是用戶對通話音視頻通話 UI 的交互有大量需求,能夠基於 CallLib 進行開發,對 UI 能夠進行各類定製。
l 暴露的信息要少
成熟的 API 設計者都會盡量的隱藏內部實現細節。
好比字段不該該直接暴露而是經過 Getter/Setter 提供,不須要的類、方法、字段都應該隱藏,都已經成爲各個語言的基礎要求,在此就不細述了。
但容易被忽略的一點須要提醒你們,應儘可能隱藏技術棧的信息。
好比:API http://api.example.com/cgi-bi...,就明顯混入了不少無用的信息,而且之後技術切換升級想維持 API 穩定很是麻煩。
l 行爲擴散要少
在語言直接調用的 API 中,須要避免基礎接口經過繼承致使行爲擴散。
在普通的編碼過程當中,抽象類和繼承都是面向對象的強大武器。可是對於 API,更建議經過組合使用。
好比一個管理生命週期的類,若是被繼承,子類有些行爲就有可能被修改而致使出錯。這時候建議使用 Interface + 工廠的方法提供實例。
因爲 Java 8 以前 interface 沒有 default 實現,爲了不增長功能須要頻繁修改接口,可使用 final class。
Objetive-C 則可使用 __attribute__((objc_subclassing_restricted)) 和 __attribute__((objc_requires_super) 控制子類繼承行爲。
l 畫風切換要少
API 命名要作到多個平臺的業務命名統一,與每一個平臺的風格統一。
這點 HTTP 的接口要簡單一些,只須要選定一種風格便可,Restful 或者 GraphQL 或者本身定義。
語言調用的 API 命名,建議首先遵循平臺的風格,而後再是參考語言標準,最後才考慮團隊的風格。
好比:iOS 平臺的 API 開發,須要首先參照 iOS 的命名風格,did 和 will 之類的時態就很是有特點。
命名上細節較多,詞彙、時態、單複數、介詞、⼤小寫、同步異步風格等都須要考量,須要長時間的積累。
l 理解成本要少
通常 API 每一個接口都會有相應的註釋說明,可是值得注意的是,大部分開發者並不看註釋。
大部分開發者對接口的瞭解,都僅源於 IDE 的補全和提醒。一個接口看着像就直接用,不行再換一個試試,這實際上是一種經驗式編程的方式。
也就意味着接口命名須要提升可理解性。有一個辦法能夠驗證,將接口的全部註釋抹掉,使用者可否很是直接的看懂每一個接口的含義。若是很困難,則須要改進。
API 設計還有一處和普通開發不太一致。普通開發設計好架構便可,每一個模塊的開發多是同一我的,接口並不須要在設計時肯定下來。
可是 API 的設計階段,須要進行 Review 並直接肯定接口的設計,以保證多端在開發時遵循徹底一直的規則。
※編碼
在 API 的編碼過程當中,有如下幾點須要注意。
在 API 中,預約義好版本號。
這個主要是針對 HTTP API,如:http://api.example.com/v1/use...。 若是目前僅有一個版本,也能夠暫時不加,第二版時再區分。
注意 API 版本檢查。
當分層提供多種 API 時,每層 API 須要在啓動時,先校驗一下版本號,避免不匹配的狀況。
好比在如下 Java 代碼中,你們可能以爲判斷版本號相等的代碼很是奇怪,應該永遠是 true 纔對。
可是抽象類和實現類出如今不一樣的分層模塊中,而且實現類先編譯,抽象類版本更新後再編譯,就會出現不一致的狀況。有不少語言或平臺能提供相似的方式來肯定版本。
提供規範性的 log 輸出。
普通開發的log,主要用於本身定位問題。可是 API 在編碼時,最好針對性的添加一些 log,有利於 API 的使用者理解並簡單排查問題。
但出於性能考慮,須要定義好 log 的級別並能夠調整。
注意廢棄與遷移。
當一個之前設計的 API 再也不符合要求或者有重大問題時,咱們能夠對外標記成已廢棄,並在註釋中建議使用者遷移到另外一個接口。
若是是相似的被廢棄接口,內部編碼時最好能使用新的接口來實現,以下降向後兼容的維護成本。
HTTP 的 API,須要預約義好遷移的錯誤碼,好比在 HTTP 規範中,可使用 410 Gone 說明已經再也不支持某個接口。
※Review
API 的 Review 基於普通開發的 Code Review。
若是基礎的 Code Review 都沒有作好,確定沒法保障 API 的質量和穩定性。
能夠經過一些工具,爲 API 的 Review 提供一些參考報告。
好比可使用 SonarLint 分析代碼複雜度,若是接口層的代碼複雜度較高,會是一個危險的信號。
還能夠藉助 Java 反射、Clang 語法分析,獲取當前的 API 接口列表,生成接口變動報告,也有利於減小無用接口的暴露。
另外,自動化工具生成的接口文檔也是 Review 重要的一環。
※測試
在測試環節,咱們能夠經過 unit test 來關注 API 的穩定性。
與敏捷開發常常修改 test case 不一樣,API 的 test case 基本表明了接口的穩定性。因此在修改舊 case 時須要特別明確,是 case 自身的 bug 仍是接口行爲發生了變動。
※發佈
咱們能夠經過區分 dev 和 stable 版本,爲不一樣階段的開發者提供更好的體驗。
dev 版本包含最新的功能,可是 API 接口有變動風險。stable 版本 API 穩定,但功能不必定是最新的。
若是開發者還在開發過程當中,能夠選用最新的 dev 版本,基於最新 API 開發。
若是應用已經上線,能夠選擇升級直接到最新的 stable 版本。
※反饋
因爲前面提到的,雲服務的 API 比較難肯定用戶羣和用戶的使用方式。
能夠參考 APM(Application Performance Management) 的方式,記錄熱點 API 使用狀況,爲後續的優化提供數據。
總結
上面的改進,讓保障 API 的穩定性變得更容易。
下面以融雲 IMLib iOS SDK 2.0 版本演進爲例,歷盡 2015至 2019 四年時間,從 2.2.5 到 2.9.16 共 98 個版本。
API 接口數量翻了一番,考慮到接口更內聚,功能大約增長了 3 倍。
可是須要用戶遷移的接口很是少,即便遷移時開發成本都很是低。
更多幹貨內容請點擊註冊查看!