API是軟件系統的核心,而軟件系統的複雜度Complexity是大規模軟件系統可否成功最重要的因素。但複雜度Complexity並不是某一個單獨的問題能徹底敗壞的,而是在系統設計尤爲是API設計層面不少不少小的設計考量一點點疊加起來的(也即John Ousterhout老爺子說的Complexity is incremental【8】)。成功的系統不是有一些特別閃光的地方,而是設計時點點滴滴的努力積累起來的。css
所以,這裏咱們試圖思考並給出建議,一方面,什麼樣的API設計是__好__的設計?另外一方面,在設計中如何能作到?前端
API設計面臨的挑戰千差萬別,很難有到處適用的準則,因此在討論原則和最佳實踐時,不管這些原則和最佳實踐是什麼,必定有適應的場景和不適應的場景。所以咱們在下面爭取不只提出一些建議,也儘可能去分析這些建議在什麼場景下適用,這樣咱們也能夠有針對性的採起例外的策略。java
本文偏重於__通常性的API設計__,__並更適用於遠程調用(RPC或者HTTP/RESTful的API)__,可是這裏沒有特別討論RESTful API特有的一些問題。數據庫
另外,本文在討論時,假定了客戶端直接和遠程服務端的API交互。在阿里,因爲多種緣由,經過客戶端的SDK來間接訪問遠程服務的狀況更多一些。這裏並不討論SDK帶來的特殊問題,可是將SDK提供的方法看做遠程API的代理,這裏的討論仍然適用。json
在這一部分,咱們試圖總結一些好的API應該擁有的特性,或者說是設計的原則。這裏咱們試圖總結更加基礎性的原則。所謂基礎性的原則,是那些若是咱們很好的遵照了就可讓API在以後演進的過程當中避免多數設計問題的原則。後端
A good API設計模式
本部分則試圖討論一些更加詳細、具體的建議,可讓API的設計更容易知足前面描述的基礎原則。api
想一想優秀的API例子:POSIX File API緩存
若是說API的設計實踐只能列一條的話,那麼可能最有幫助的和最可操做的就是這一條。本文也能夠叫作「經過File API體會API設計的最佳實踐」。安全
因此整個最佳實踐能夠總結爲一句話:「想一想File API是怎麼設計的。」
首先回顧一下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爲何是經典的好API設計?
(open, close, read, write)
,清晰明瞭;例如一樣是打開文件的接口,底層實現徹底不一樣,可是經過徹底同樣的接口,不一樣的路徑以及Mount機制,實現了同時支持。其餘還有Procfs, pipe等。
int open(const char *path, int oflag, .../*,mode_t mode */);
例如這裏的cephfs和本地文件系統,底層對應徹底不一樣的實現,可是上層client能夠不用區分對待,採用一樣的接口來操做,只經過路徑不一樣來區分。
基於上面的這些緣由,咱們知道File API爲何可以如此成功。事實上,它是如此的成功以致於今天的*-nix操做系統,everything is filed based.
儘管咱們有了一個很是好的例子File API,可是__要設計一個可以長期保持穩定的API是一項及其困難的事情__,所以僅有一個好的參考還不夠,下面再試圖展開去討論一些更細節的問題。
Document well 寫詳細的文檔
寫詳細的文檔,並保持更新。 關於這一點,其實無需贅述,現實是,不少API的設計和維護者不重視文檔的工做。
在一個面向服務化/Micro-service化架構的今天,一個應用依賴大量的服務,而每一個服務API又在不斷的演進過程當中,__準確的記錄每一個字段和每一個方法,而且保持更新__,對於減小客戶端的開發踩坑、減小出問題的概率,提高總體的研發效率相當重要。
Carefully define the "resource" of your API 仔細的定義「資源」
若是適合的話,選用「資源」加操做的方式來定義。今天不少的API均可以採用這樣一個抽象的模式來定義,這種模式有不少好處,也適合於HTTP的RESTful API的設計。可是在設計API時,一個重要的前提是對Resource自己進行合理的定義。什麼樣的定義是合理的?Resource資源自己是對一套API操做核心對象的一個抽象Abstraction。
抽象的過程是__去除細節的過程__。在咱們作設計時,若是現實世界的流程或者操做對象是具體化的,抽象的Object的選擇可能不那麼困難,可是對於哪些細節應該包括,是須要不少思考的。例如對於文件的API,能夠看出,文件File這個Resource(資源)的抽象,是「能夠由一個字符串惟一標識的數據記錄」。這個定義去除了文件是如何標識的(這個問題留給了各個文件系統的具體實現),也去除了關於如何存儲的組織結構(again,留給了存儲系統)細節。
雖然咱們但願API簡單,可是更重要的是__選擇對的實體來建模__。在底層系統設計中,咱們傾向於更簡單的抽象設計。有的系統裏面,域模型自己的設計每每不會這麼簡單,須要更細緻的考慮如何定義Resource。通常來講,域模型中的概念抽象,若是能和現實中的人們的體驗接近,會有利於人們理解該模型。__選擇對的實體來建模__每每是關鍵。結合域模型的設計,能夠參考相關的文章,例如阿白老師的文章【2】。
Choose the right level of abstraction 選擇合適的抽象層
與前面的一個問題密切相關的,是在定義對象時須要選擇合適的Level of abstraction(抽象的層級)。不一樣概念之間每每相互關聯。仍然以File API爲例。在設計這樣的API時,選擇抽象的層級的可能的選項有多個,例如:
這些不一樣的層級的抽象方式,可能描述的是同一個東西,可是在概念上是不一樣層面的選擇。當設計一個API用於與數據訪問的客戶端交互時,「文件File「是更合適的抽象,而設計一個API用於文件系統內部或者設備驅動時,數據塊或者數據塊設備多是合適的抽象,當設計一個文檔編輯工具時,可能會用到「文本圖像混合對象」這樣的文件抽象層級。
又例如,數據庫相關的API定義,底層的抽象可能針對的是數據的存儲結構,中間是數據庫邏輯層須要定義數據交互的各類對象和協議,而在展現(View layer)的時候須要的抽象又有不一樣【3】。
Prefer using different model for different layers 不一樣層建議採用不一樣的數據模型
這一條與前一條密切關聯,可是強調的是不一樣層之間模型不一樣。
在服務化的架構下,數據對象在處理的過程當中每每經歷多層,例如上面的View-Logic model-Storage是典型的分層結構。在這裏咱們的建議是不一樣的Layer採用不一樣的數據結構。John Ousterhout 【8】書裏面則更直接強調:Different layer, different abstraction。
例如網絡系統的7層模型,每一層有本身的協議和抽象,是個典型的例子。而前面的文件API,則是一個Logic layer的模型,而不一樣的文件存儲實現(文件系統實現),則採用各自獨立的模型(如快設備、內存文件系統、磁盤文件系統等各自有本身的存儲實現API)。
當API設計傾向於不一樣的層採用同樣的模型的時候(例如一個系統使用後段存儲服務與自身提供的模型之間,見下圖),可能意味着這個Service自己的職責沒有定義清楚,是否功能其實應該下沉?
不一樣的層採用一樣的數據結構帶來的問題還在於API的演進和維護過程。一個系統演進過程當中可能須要替換掉後端的存儲,可能由於性能優化的關係須要分離緩存等需求,這時會發現將兩個層的數據綁定一塊兒(甚至有時候直接把前端的json存儲在後端),會帶來沒必要要的耦合而阻礙演進。
Naming and identification of the resource 命名與標識
當API定義了一個資源對象,下面通常須要的是提供命名/標識(Naming and identification)。在naming/ID方面,通常有兩個選擇(不是指系統內部的ID,而是會暴露給用戶的):
什麼時候選擇哪一個方法,須要具體分析。採用Free-form string的方式定義的命名,爲系統的具體實現留下了最大的自由度。帶來的問題是命名的內在結構(如路徑)自己並不是API強制定義的一部分,轉爲變成實現細節。若是命名自己存在結構,客戶端須要有提取結構信息的邏輯。這是一個須要作的平衡。
例如文件API採用了free-form string做爲文件名的標識方式,而文件的URL則是文件系統具體實現規定。這樣,就允許Windows操做系統採用"D:\Documents\File.jpg"
而Linux採用"/etc/init.d/file.conf"
這樣的結構了。而若是文件命名的數據結構定義爲
{
disk: string, path: string }
這樣結構化的方式,透出了"disk"
和"path"
兩個部分的結構化數據,那麼這樣的結構可能適應於Windows的文件組織方式,而不適應於其餘文件系統,也就是說泄漏了實現細節。
若是資源Resource對象的抽象模型天然包含結構化的標識信息,則採用結構化方式會簡化客戶端與之交互的邏輯,強化概念模型。這時犧牲掉標識的靈活度,換取其餘方面的優點。例如,銀行的轉帳帳號設計,能夠表達爲
{
account: number routing: number }
這樣一個結構化標識,由帳號和銀行間標識兩部分組成,這樣的設計含有必定的業務邏輯在內,可是這部分業務邏輯是__被描述的系統內在邏輯而非實現細節__,而且這樣的設計可能有助於具體實現的簡化以及避免一些非結構化的字符串標識帶來的安全性問題等。所以在這裏結構化的標識可能更適合。
另外一個相關的問題是,__什麼時候應該提供一個數字unique ID?__ 這是一個常常遇到的問題。有幾個問題與之相關須要考慮:
若是這些問題都有答案並且不是什麼阻礙,那麼使用數字ID是能夠的,__不然要慎用數字ID__。
Conceptually what are the meaningful operations on this resource? 對於該對象來講,什麼操做概念上是合理的?
在肯定下來了資源/對象之後,咱們還須要定義哪些操做須要支持。這時,考慮的重點是「__概念上合理(Conceptually reasonable)__」。換句話說,operation + resource
連在一塊兒聽起來天然而然合理(若是Resource自己命名也比較準確的話。固然這個「若是命名準確」是個big if,很是不容易作到)。操做並不老是CRUD(create, read, update, delete)。
例如,一個API的操做對象是額度(Quota),那麼下面的操做聽上去就比較天然:
Update quota
(更新額度),transfer quota
(原子化的轉移額度)可是若是試圖Create Quota
,聽上去就不那麼天然,因額度這樣一個概念彷佛表達了一個數量,概念上不須要建立。額外須要思考一下,這個對象是否真的須要建立?咱們真正須要作的是什麼?
For update operations, prefer idempotency whenever feasible 更新操做,儘可能保持冪等性
Idempotency冪等性,指的是一種操做具有的性質,具備這種性質的操做能夠被屢次實施而且不會影響到初次實施的結果「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在系統設計中會帶來不少便利性,例如客戶端能夠更安全的重試,從而讓複雜的流程實現更爲簡單。可是Idempotency實現並不老是很容易。
Update類型的Idempotency
更新值(update)類型的API,應該避免採用"Delta"語義,以便於實現冪等性。對於更新類的操做,咱們再簡化爲兩類實現方式
Incremental
(數量增減),如IncrementBy(3)
這樣的語義SetNewTotal
(設置新的總量)`IncrementBy` 這樣的語義重試的時候難以免出錯,而`SetNewTotal(3)`(總量設置爲x)語義則比較容易具有冪等性。 固然在這個例子裏面,也須要看到,`IncrementBy`也有有點,即多個客戶請求同時增長的時候,比較容易並行處理,而`SetTotal`可能致使並行的更新相互覆蓋(或者相互阻塞)。 這裏,能夠認爲*更新增量*和*設置新的總量*這兩種語義是不一樣的優缺點,須要根據場景來解決。若是必須優先考慮併發更新的情景,可使用*更新增量*的語義,並輔助以Deduplication token解決冪等性。
Compatibility 兼容
API的變動須要兼容,兼容,兼容!重要的事情說三遍。這裏的兼容指的是向後兼容,而兼容的定義是不會Break客戶端的使用,也即__老的客戶端可否正常訪問服務端的新版本(若是是同一個大版本下)不會有錯誤的行爲__。這一點對於遠程的API(HTTP/RPC)尤爲重要。關於兼容性,已經有很好的總結,例如【4】提供的一些建議。
常見的__不兼容__變化包括(但不限於)
方法名稱字段不改,可是語義和行爲的變化,也是不兼容的。這類比較容易被忽視。
更具體描述能夠參加【4】。
另外一個關於兼容性的重要問題是,__如何作不兼容的API變動__?一般來講,不兼容變動須要經過一個__Deprecation process,在大版本發佈時來分步驟實現__。關於Deprecation process,這裏不展開描述,通常來講,須要保持過去版本的兼容性的前提下,支持新老字段/方法/語義,並給客戶端足夠的升級時間。這樣的過程比較耗時,也正是由於如此,咱們才須要如此重視API的設計。
有時,一個面向內部的API升級,每每開發的同窗傾向於選擇高效率,採用一種叫」同步發佈「的模式來作不兼容變動,即通知已知的全部的客戶端,本身的服務API要作一個不兼容變動,你們一塊兒發佈,同時更新,切換到新的接口。這樣的方法是很是不可取的,緣由有幾個:
所以,對於在生產集羣已經獲得應用的API,強烈不建議採用「同步升級」的模式來處理不兼容API變動。
Batch mutations 批量更新
批量更新如何設計是另外一個常見的API設計決策。這裏咱們常見有兩種模式:
服務端實現批量更新。
以下圖所示。
API的設計者可能會但願實現一個服務端的批量更新能力,可是咱們建議要儘可能避免這樣作。__除非對於客戶來講提供原子化+事務性的批量頗有意義(all-or-nothing)__,不然實現服務端的批量更新有諸多的弊端,而客戶端批量更新則有優點:
Be aware of the risks in full replace 警戒全體替換更新模式的風險
所謂Full replacement更新,是指在Mutation API中,用一個全新的Object/Resource去替換老的Object/Resource的模式。API寫出來大概是這樣的
UpdateFoo(Foo newFoo);
這是很是常見的Mutation設計模式。可是這樣的模式有一些潛在的風險做爲API設計者必須瞭解。
使用Full replacement的時候,更新對象Foo
在服務端可能已經有了新的成員,而客戶端還沒有更新並不知道該新成員。服務端增長一個新的成員通常來講是兼容的變動,可是,若是該成員以前被另外一個知道這個成員的client設置了值,而這時一個不知道這個成員的client來作full-replace,該成員可能就會被覆蓋。
更安全的更新方式是採用Update mask,也即在API設計中引入明確的參數指明哪些成員應該被更新。
UpdateFoo { Foo newFoo; boolen update_field1; // update mask boolen update_field2; // update mask }
或者update mask能夠用repeated "a.b.c.d「
這樣方式來表達。
不過因爲這樣的API方式維護和代碼實現都複雜一些,採用這樣模式的API並很少。因此,本節的標題是「be aware of the risk「,而不是要求必定要用update mask。
Don't create your own error codes or error mechanism 不要試圖建立本身的錯誤碼和返回錯誤機制
API的設計者有時很想建立本身的Error code,或者是表達返回錯誤的不一樣機制,由於每一個API都有不少的細節的信息,設計者想表達出來並返回給用戶,想着「用戶可能會用到」。可是事實上,這麼作常常只會使API變得更復雜更難用。
Error-handling是用戶使用API很是重要的部分。爲了讓用戶更容易的使用API,最佳的實踐應該是用標準、統一的Error Code,而不是每一個API本身去創立一套。例如HTTP有規範的error code 【7】,Google Could API設計時都採用統一的Error code等【5】。
爲何不建議本身建立Error code機制?
更多的Design patterns,能夠參考[5] Google Cloud API guide,[6] Microsoft API design best practices等。很多這裏提到的問題也在這些參考的文檔裏面有涉及,另外他們還討論到了像versioning,pagination,filter等常見的設計規範方面考慮。這裏再也不重複。
【1】File wiki https://en.wikipedia.org/wiki/Computer_file
【2】阿白,域模型設計系列文章,https://yq.aliyun.com/articles/6383
【3】Idempotency, wiki https://en.wikipedia.org/wiki/Idempotence
【4】Compatibility https://cloud.google.com/apis/design/compatibility
【5】API Design patterns for Google Cloud, https://cloud.google.com/apis/design/design_patterns
【6】API design best practices, Microsoft https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design
【7】Http status code https://en.wikipedia.org/wiki/List_of_HTTP_status_codes【8】A philosophy of software design, John Ousterhout