標籤:

再談 API 的撰寫 - 契約

現代社會是個契約社會,生活中大大小小的事情都在和契約打交道。去奧萊買件衣服,一紙小票,便是你跟商家的契約:你花錢買到了產品,產品的問題商家會承諾處理(退換貨)。如果你用信用卡交款,你和銀行之間,銀行和商家之間又達成了一系列契約:銀行會在未來的某個時刻扣除你的 credit,這 credit 你需要用錢來贖回;銀行同時欠下商戶幾乎等值的 credit,這 credit 會在月末付給商戶。

契約

契約在軟體上最基本的體現就是函數。當一個函數被定義出來時:它告訴它的使用者,你我之間應該如何合作。

比如說,一個函數可以是這樣定義的:如果你傳遞給我類型為 X 的數據,我會返回給你類型為 Y 的結果,而且如果你傳遞相同的值進來,我給你相同的結果。這是 pure function,也是程序員最喜歡的契約形式,因為黑紙白字,清清楚楚,童叟無欺。

更普遍的情況是不那麼純粹的函數:如果你傳遞給我類型為 X 的數據,我會返回給你類型為 Y 的結果,當然,如果結果不存在,我會給你個 null,愛誰誰;而且,如果我中間處理的過程出了差池,我會扔一顆或者若干顆叫「異常」的炸彈,咱倆要麼哥倆好(你處理異常),要麼同歸於盡(不處理)。此外,我不能保證你傳遞相同的值進來,都返回給你相同的結果(比如說資料庫操作)。有副作用的函數儘管有諸多含混不清的地方,任然不失為一種契約。

函數級別的契約的所有當事人都是程序員,契約更新的影響面有限,所以遇到問題,姐弟倆一商量,改!新的契約就出現了。然而,新的契約出現並不意味著舊的契約的終止,只有當所有使用舊契約的地方都改用新契約時,我們才能安全地廢除舊契約。就一個函數來說,如果是兩人之間的事,更換契約也就是個把小時的事情;然而,像 linux 這樣複雜的系統,你改一個 list_add_tail() 的介面試試(假設你有許可權),即使 Linus 不拍死你,我保證社區的口水也要淹死你。為啥?你觸動了很多人的乳酪。

鋪墊了這麼多,就是想說明一件事:一旦你制定了一紙契約,你必須遵守它,且不要輕易改動它;使用契約的人越多,改動的代價越大。我們定義一個陳氏指數 CEI —— 契約使用指數(Contract Employ Index),每百萬使用者記為 1。CEI 越高,表明使用者越多,同樣的,改動的代價就越大。

REST API(以下凡提到 API,都指 REST API)是什麼?REST API 是伺服器和客戶端之間的契約。這就意味著一個中小規模的 API,其 CEI 起碼在 0.1 以上。API 一旦發布,你基本失去了對其任意修改的權利,因為你無法期待脫離了掌控的客戶端能夠像我們希望的那樣,步調一致地升級系統。

所以,即便你習慣於隨心所欲地創建一個函數,然後在需要的時候重構之,做 API 時,你會受到很多掣肘。老子說:「夫輕諾必寡信,多易必多難」,你一開始隨意了,簡單了,會給之後的維護和更新帶來無窮無盡的痛苦。

所以我們需要好好進行設計 API 的介面。

定義和設計契約

我們知道,設計介面並不是一件輕鬆的活,我們要考慮:userability,simplicity,security,reliability 等等,設計好了還需要將其文檔化。所以我們最好藉助於工具的力量來設計 API,就像我們使用 visio 設計網路拓撲或者軟體架構一樣。目前比較流行的 API 介面設計工具有 swagger,API blueprint 和 RAML。

它們共同的特點是你可以很方便地描述 API 的輸入輸出,並生成互動式的 API 文檔。所謂互動式 API 文檔,是指用戶在讀 API 文檔的時候,可以在線運行 API,獲得結果。這樣,API 的設計者就可以在還沒有開始寫代碼的時候就反覆推演 API 的結構,直到產生一個健壯的,清晰明了,可用性強的介面。

Swagger

swagger 是最早也是最成熟的 API 介面設計工具。它可以使用 json/yaml 來描述 API 的介面,使用 swagger 來設計和描述 API 有很多好處:API 的文檔化,API 的介面的可視化,各種語言的客戶端類庫的自動生成,甚至服務端代碼也能夠自動生成。包括代碼生成工具在內的完整而成熟的工具鏈是 swagger 的殺手鐧,也是眾多 API 廠商優先選擇 swagger 的一個重要因素。

我們看 swagger 的一個例子(instagram API):

paths: /users/self/feed: get: tags: - Users description: See the authenticated users feed. parameters: - name: count in: query description: Count of media to return. type: integer - name: max_id in: query description: Return media earlier than this max_id.s type: integer - name: min_id in: query description: Return media later than this min_id. type: integer responses: 200: description: OK schema: type: object properties: data: type: array items: $ref: #/definitions/Media

這裡定義了一個 API endpint /users/self/feed,他接受三個 querystring 參數,並在請求成功時(200)返回一個這樣的對象:

{ "data": [...]}

swagger 的缺點是太繁雜,撰寫起來很麻煩。不過,這不是什麼大問題,所以我最終選擇了 swagger。

API Blueprint

API Blueprint 更偏向 API 的文檔化,所以它選擇的描述語言是 markdown。三者之間 API blueprint 的描述語言可讀性最強,更像是真的在撰寫文檔。

然而,markdown 的強項不在表述語法,validation 相關的內容用 markdown 描述不是很舒服,看別人寫的文檔很容易明白,自己寫起來就會錯漏百出。API blueprint 的工具鏈也是個薄弱環節,很多工具都沒有或者不成熟。如果說工具的缺乏還可以通過時間來彌補,使用 markdown 這種對機器不太友好的定義語言來定義各種語法,則是 API blueprint 犯下的大錯。因為,對比三者的語法,它們的學習曲線都很長,遺忘指數都很高(不是經常用),指望程序員來寫還不如指望機器幫你生成。而機器生成強語法結構的 json / yaml 相對簡單,生成弱語法結構的 markdown 則要填不少坑。

所以,權衡之下,三者之間,我最先淘汰的是 API blueprint。

RAML

RAML 使用 yaml 來描述 API。它被設計地很靈活,很容易把描述分解到多個文件里然後相互引用。

就描述語言來說,RAML 像是一個蓬勃向上的少年,精明而幹練;而 swagger 已經垂垂老矣,冗長而乏味。我一開始在 RAML 和 swagger 兩者間左右搖擺,寫了不少測試代碼,如果不是 swagger 的工具鏈過於吸引人,而 RAML 1.0 版本還處在 beta 階段,我可能會最終選擇 RAML。

契約和實現合二為一

如果我們從 swagger 出發,設計好 API 的介面,然後再用某種語言實現這個介面,顯得有些累贅,日後改介面時,得改代碼;改代碼後如果變動了介面,還得回頭改 swagger 的聲明,這樣太累心,遲早會出不一致的問題。一旦不一致,之前所做的所有努力就泡湯了:你提供了契約,卻沒有按照契約去行事。

swagger 考慮到了這一點,它能幫你生成客戶端的 SDK 和伺服器端的 stub。客戶端的 SDK 還好,客戶端的其他代碼都是單向調用 SDK,重新生成並不會影響太大;服務端的代碼需要 API 實現者實現,即便生成了 stub,肯定是要修改和添加功能的,所以如果修改 swagger 文檔後,你不能再重新生成伺服器端的 stub 了,因為這樣有可能覆蓋掉你已經修改的代碼。所以大家使用 swagger 的方式基本都是伺服器這端完全自己寫,不用 stub。這樣的話,上述的問題依舊存在。

另一種解決方案是通過 API 代碼反向生成 swagger 文檔。乍一看這似乎違背了 API 描述語言的初衷:我們竟然在沒開始設計之前,就開始寫代碼了。

不必過慮。我們可以把代碼的結構調整地更貼近描述語言。你可以先撰寫代碼把 API 的輸入輸出定義清楚,然後通過這個定義來生成 swagger 文檔,在 swagger-ui 裡面調試和驗證;當借口設計符合期望後,再完成具體的實現。比如說這樣用代碼描述 API:

拋開 action 是什麼不提,這段代碼幾乎和你用 YAML 描述 API 的介面如出一轍(這裡缺了描述 response 的內容)。我們可以使用它生成 swagger 文檔來驗證:

通過代碼反向生成 swagger 文檔的好處是代碼和文檔總是一致的,API 的實現和契約相互印證;缺點是程序員看見代碼就像看見九天仙女一樣,眼迷心蕩,剛定義好介面,還未細思,就忙不迭地去實現了。

尾聲

這個系列竟然寫出了五篇文章,大大出乎我的意料。韓非子說:善張網者,引其綱。不一一攝萬目而後得。做 API 是個提綱攜領的活,你要從紛擾的「萬目」中找到那根系網的大繩,牽動之,網就搭好了。這根繩,不消說,就是我在 再談 API 的撰寫 - 架構 那篇文章中所述的 Pipeline。以此為綱,自頂向下,層層遞進,你便豁然開朗。然而,API 寫得再好,沒有一個與之對應的契約,是萬萬不行的 —— 沒有文檔描述的 API 就如同沒有說明書的產品。文檔和代碼,如同泉水乾涸之後相呴以濕,相濡以沫的魚兒,誰也離不開誰。能否防止它們隨著時間的流逝功能的增加而相忘於江湖,是考量每個程序員能力和操守的一桿秤。


推薦閱讀:

石墨文檔是什麼?
如何讓onenote支持markdown?
如何把 Markdown 文件轉化為 PDF?
關於產品經理需要做哪些圖和哪些文檔,做這些圖片和文檔的魚骨流程是什麼樣的?如果有對於模版或例子就更好
Macbook上如何在A3紙上印兩張A4紙的內容?

TAG:API | 文档 |