標籤:

談談編譯和運行

[作者按] 今天(3/21) hacker news 爆炸性的新聞是我們敬愛的葛老頭:Andy Grove 去了。70後,80後大多聽過這個響噹噹的名字,也聽過(或者讀過)『只有偏執狂才能生存』這本書。在90年代,葛魯夫和蓋茨一樣,基本上等同於他們所締造的王國,以及他們創立的 Wintel 聯盟。如今90後創業者們所津津樂道的所謂創業大師們總結的:專註獨特10倍速因子等等其實是拾了老爺子牙慧的。老爺子還有一本據說更好的書:High output management ,Ben(a16z 的合伙人)對其大為推崇。後知後覺的我沒有讀過,趕緊去圖書館 hold 了一本。去年,老爺子參加了 Ben 主持的一個頒獎會,看他幾乎站都站不穩,話都快說不清楚的老態讓我心酸,接著,聽到他顫巍巍說出這句話,我幾乎都要淚奔了:

Lets remember that millions of young people who have had the misfortune of being born in the wrong national boundaries are going through all the horrors that Ben described. I made it. Lets try in a little way to help them make it.

老爺子此時已經病入膏肓,無葯可醫,但依然心系那些和他有著類似磨難的年輕人。這便是範文正所謂的「不以物喜,不以己悲,居廟堂之高則憂其民;處江湖之遠則憂其君」的寫照。

葛老師一路走好。

。。。

言歸正傳。本來今天要講講 API 系統的配置和 CLI 等子系統,但是很多同學對我上一篇文章 再談 API 的撰寫 - 架構 中所述的「編譯時」和「運行時」有不少困惑。這篇文章先講講這兩個概念。

在 上一篇 文章里,我講到:

通過這樣一個介面,我們把 API 系統區隔為「編譯時」和「運行時」。這個介面寫出來的 API,更像是一個等待編譯的源文件。在 API 系統啟動的時候,會經歷一個「編譯」的過程,把所有的 route 匯總起來,生成 restify 認識的路由形式,同時,收集裡面的各種信息(比如 validator,authentication),供框架的各個 middleware 使用。

「編譯」(compile)是軟體系統的一個非常非常重要的概念;很可惜,在 python / ruby / javascript 等解釋型語言大行其道的當下,很多人已經不知道編譯為何物。當然,即便你使用 c / go / java 等編譯型語言,有多少人又真正清楚「編譯」究竟是個什麼過程呢?

在 wikipedia,compile / compiler 的解釋如下:

A compiler is a computer program (or a set of programs) that transforms source code written in a programming language (the source language) into another computer language (the target language), with the latter often having a binary form known as object code.[1] The most common reason for converting source code is to create an executable program.

所以 compile 實際上是一種 transformation(我們又見到這個詞了):它把某個數據(如果你認為源代碼也是一種數據的話)從一種格式轉換成另外一種格式。在編譯型語言里,這種轉換是為了生成機器碼(如 c / go),或者 byte code(java / c#),方便機器執行(byte code 會進一步以 JIT 的方式 compile 成機器碼)。

(題外話:其實解釋型語言也是有一個 JIT 「編譯」的過程;現在純粹的,完全在運行時一句句解釋執行的語言,只能生存在象牙塔里)

有了這樣一層(indirection)編譯的過程,源數據和目標數據就被分離開,可以做很多事情,比如 wikipeidia 上說的 compiler 的一大功效:

Compilers enabled the development of programs that are machine-independent.

這裡面,這個 machine-independent 可以根據你的需要被換成 framework-independent,甚至 language-independent。

那麼,一份源代碼除了可以生成目標代碼(主產品)外,還能有什麼副產品?我們以 java 為例:

  • 如果你的注釋遵循 javadoc,那麼從代碼里可以生產出來漂亮的文檔(SDK)。

  • facebook/infer 可以對你的代碼做詳細的 static analysis。

  • jacoco 可以根據源碼和 test case 生成 coverage report。

  • ...

這些副產品帶啦的好處是顯而易見的:我們不用為了一些特定的目的而做一些額外的事情。

回到我們這幾天說的 API 系統。我提到了這樣的一個介面:

你可以將其看做是一段聲明 API 的代碼,但我更願意將其看做是一段描述 API 的數據。這個數據有:

  • method:API 使用何種 http 方法調用。

  • path:API 使用什麼樣的 endpoint。

  • description:API 的文檔。

  • validators:如果要驗證 API 的輸入數據,如何驗證。

  • action:API 具體做些什麼事情。

  • flags:API 有哪些屬性(需不需要驗證,支不支持某些特定的操作等)。

如果你以數據的眼光看待這段代碼,那麼,每一個 route() 的聲明都可以被聚合起來,放到一個數組裡。事實上,route 的實現就是如此:

function route(params) { const result = joi.validate(params, routeSchema); if (result.error) throw result.error; const value = result.value; value.tag = helper.app.getAppTag(); ... value.description = mustache.render(value.description, helper.doc); registeredRoutes.push(value);}

每當用戶撰寫一個 route 的時候,我們實際上在往一個 list 里 push 這個 route 的數據。這個 list 究竟怎麼用,是生成 restify 的 route,還是生成 hapi 的 route,我們在編譯時再具體決定。這便是 framework-independent。

那麼,什麼是編譯時,什麼又是運行時呢?

const app = new Application();app.compile(); // call app.setupRoutes(); etc.app.run(); // call app.listen(); etc.

就這麼簡單。app.compile() 把放在 route list 裡面的數據轉換成 restify 的 route,而 app.run() 開始進行網路監聽。很多同學看到這裡會想,有沒有搞錯,我還以為是什麼高深的東西呢,這代碼我也會寫啊。的確,這裡沒有任何高深的東西。然而,關鍵的是你會不會想到把一段代碼的運行分解成:compile() 和run() 兩個階段。只有你這麼去想了,你才會反過來考慮你的 API 的代碼能不能退一步,用一個數據結構封裝,你才會想這個數據結構該如何設置,你才會把 route() 的實現寫成類似的方式。

在「編譯時」你可以做很多繁雜的事情,就像高手過招前先養氣御劍一樣;這樣,在「運行時」,你才能打出行雲流水的招式。

再舉一個例子。就寫 blog 而言,你可以用 wordpress,也可用 jekyll 這樣的 static site generator。前者把編譯和運行混在一起,在請求頁面的時候生成博文;而後者則將二者完全分離,你得使用 jekyll 的工具把 markdown 撰寫的博文編譯成 html,才能被正常訪問。這樣分離之後,天地開闊了很多,你可以在「編譯時」為所有文章生成全文搜索所用的索引,可以根據文章的類別 / tag 生成目錄,相關文章,菜單等等,在為運行時提供了閃電般的速度外,還能提供 wordpress 才能提供的動態性和靈活性。

注意,這裡所說的分離完全是邏輯上的分離,就像上一篇文章中的 pipeline,每個 component 是邏輯上單獨存在,未必需要物理上完全分離。

把「編譯時」和「運行時」分離,是一項很重要的抽象能力。


推薦閱讀:

API 是如何工作的(傻瓜式教學)
做 2D 俄羅斯方塊用什麼繪圖 API 比較好?
了解Revit API
GraphQL,你準備好了么?
邀請你共同開發WeChat API

TAG:API | 编译 |