Web 框架的架構模式探討

發表於2018-05-23

在寫乾貨之前,我想先探(qiang)討(diao)兩個問題,模式的侷限性?模式有什麼用?

最近看到一篇文章對我啟發很大,許來西在知乎的回答《哲學和科學有什麼關聯?》,全篇較長,這裡摘錄我要引出的一點:

科學作為一種經驗主義的認識論,有著經驗主義的巨大缺陷:它永遠不能產生絕對正確的真理。這是歸納法的本質決定的。而且值得注意的是,歸納不具有唯一性。

舉一個簡單的例子,我們假設一個世界,如下圖:

一個青蛙世界

科學家很快有了兩種歸納方式:

  • 世界上所有的青蛙都戴眼鏡
  • 世界上所有戴眼鏡的都是青蛙

在沒有更多的資訊的時候,我們應該如何選擇正確的理論呢?答案是無法選擇。

舉個模式的例子,Scott Wlaschin 在《Functional Programming Design Patterns》(函式型程式設計模式)中對比了常用物件導向模式、原則,在函式型程式語言裡面等價實現:

OOP模式對比函式式

OOP 和 FP,到底哪種程式設計正規化更加先進呢?答案同樣是無法選擇。只能在不同的時候選用不同的假設和不同的理論來解釋問題,許來西的文章講到科學一定程度上通過放棄一貫性換取了實用性,放棄自洽性換取了它洽性。科學追求實用和工具(實用主義和工具主義)。當我看完許來西的文章,欣喜若狂,一直對程式設計技術理論的善變和不自洽感到恐懼和厭惡,其實只是經驗主義科學發展的必然過程,善變代表更好的理論(更方便)在替換基礎理論,代表蓬勃發展。

所以我想引入第一個觀點:

  • 模式是一套立足於特定背景,基於共性總結出的方案,它絕不是真理。

瞭解這些有助於幫助從對模式的盲目崇拜到探究它的實用性和工具性,也就是我要引出的第二個問題:模式有什麼用?

不好好寫程式碼看哲學文章不是偶然,在文章落筆之前,我有思考過在 JavaScript 這門動態,多正規化,單執行緒,基於事件I/O的語言環境下,甚至在當前時代,模式是否還有意義?顯然我不是唯一這樣想的,還有篇深度好文《20年前GoF提出的設計模式,對這個時代是否還有指導意義?》。這篇文章引經據典,摘錄了GoF(又稱Gang of Four,即Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides)在設計模式一書中觀點:

這本書的實際價值也許還值得商榷。畢竟它並沒有提出任何前所未有的演算法或者程式設計技術。它也沒能給出任何嚴格的系統設計方法或者新的設計開發理論——它只是對現有設計成果的一種審視。大家當然可以將其視為一套不錯的教程,但它顯然無法為經驗豐富的物件導向設計人員帶來多少幫助。

換言之,模式顯然毫無實際用處。

不僅如此,文章還列舉了一度模式濫用導致許多弊端,可謂警鐘長鳴。

但是……模式這一稱謂仍然不斷出現,直到今天我們亦在大量使用。為什麼?GoF實際早設計模式的書中做出了預言:

“設計模式為設計師們提供一種共通的詞彙儲備,幫助其溝通、編寫文件並探索設計方案。設計模式允許我們立足於高階抽象層面進行探討,而非設計標註或者程式語言,這就大大降低了系統複雜性。設計模式提升了我們設計及與同事進行設計探討時的切入點層級。”(第389頁)

簡言之,模式方便了我們的溝通,提升了思考問題的抽象層級。

這個意義非常巨大,想象一下沒有 MVC 架構模式,可能所有的 Web 框架必然的會實現一套幾乎解決同樣問題的方案,但是命名和文件卻各不一樣,當你去看一個新的框架文件的api 介面,從頭到尾看完以後才恍然大悟,這不就是之前用的框架裡面的 XXX 類似嗎,這樣的程式設計世界簡直地獄。慶幸的是,得益於電腦科學家(碼農)對問題和方案持續的抽象成模式,使得當前高度複雜的電腦科學也能得到合理分層和適配,大大簡化了學習和溝通的成本。

為了感謝模式,是時候學習一波了,本文要介紹的主要有三種架構模式:Middleware,MVC,DI。

Middleware 中介軟體模式

相信做過 Node.js 服務端開發的同學對這個模式一定不陌生,考慮如下 Web 應用的場景:

簡單Web框架

在一個簡單的 HTTP 請求響應週期裡,有如下條件處理,

  • 記錄開始時間
  • 需要驗證使用者的身份 authentication。
  • 解析cookie 並載入body
  • 根據路由返回不同的業務處理結果
  • 沒有命中路由則返回404頁面
  • 記錄日誌
  • 記錄總共花費時間
  • 處理異常並顯示頁面(開發環境)

有些處理會根據是否成功決定是否繼續後面的粗粒,有些處理會生成額外的資料,還有的要求攔截某些處理的開始和結束,最後異常處理和記錄日誌要求一定被執行。

一般的解決方法是用巢狀條件判斷結合 try catch finally return 等控制語句,但是這樣的方案會導致程式碼碎片化和複製貼上的編碼風格,因為控制流和邏輯耦合到了一起。理想的方案應當如下:

  • 中心化控制流
  • 解耦處理模組(重用性)
  • 宣告式、可配置的服務(配置和程式碼無關)

這些場景由來已久,很久以前J2EE總結了 Intercepting Filter 模式,有興趣大家可以看看這篇文(lun)章(wen),裡面由淺入深提到三種方案,其中最初級的方案程式碼如下:

這個和 express 和 Koa 的中介軟體模式極其相似,但是因為靜態語言本身一些特徵,導致最後形成的企業級程式碼極其繁瑣,並且有許多侷限性。最主要的問題是處理模組之間難以重用和共享資料,因為 ServletRequest ServletResponse 無法動態新增屬性。以至於 JavaEE 把這個模式的適用性加了許多限制,包括和核心處理邏輯分開。

在動態語言的世界裡面,我們可以很方便的往 req 和 res 裡面新增資料(基於約定),因為沒有了很多 OOP 世界裡面的”束縛“,Node.js 的實現通常更加優雅和通用。

Express 中介軟體模式

express 實現如今廣泛接受的 Middleware 中介軟體模式。中介軟體的意思是在 請求 和 響應 中間執行的函式(為了區分另一箇中介軟體),簽名如下:

Express 中介軟體

這個模式包含了一套宣告式的路由規則,和 middleware 函式上的 next 簽名,它們共同構成了整個中介軟體模式的控制流,如圖:

express 中介軟體模式

這個模式的核心構成不是許可權解析等中介軟體邏輯,而是路由判斷next中斷響應(驗證失敗、解析失敗),其作為中介軟體執行控制,解耦了具體的處理邏輯,使得更容易寫出通用的細粒度的中介軟體。express 內建的強大的宣告式路由,並且路由和 middleware 分離可以說是它最成功的設計之一。

然而在一些稍微複雜點的業務中,比如一個網站有管理端和使用者端,兩個端相當於獨立的app。express 4.0 提供了一個非常強大的功能 Router。Router 擴充了鏈式決策變成樹形決策,可以讓 express 更好的支援大型專案。

express 中介軟體模式

Koa 非同步中介軟體模型

Koa 的非同步中介軟體模式-洋蔥模型,相比 Express,其中介軟體函式返回 Promise,支援 async/await,並且可以輕鬆實現前置和後置的處理。毫無疑問這個模式更加先進,一些在 express 裡面不好實現的攔截處理邏輯,比如異常處理和統計時間,在 Koa 裡用一箇中介軟體就能搞定。然而遺憾的是 Koa 本身只提供了 Http 模組和洋蔥模型的最小封裝。

express 中介軟體模式

未來我看好 Koa,其實 express 也意識到這點,他們計劃在 5.0 版本里新增 Promise 的支援,然而作為一個老牌和完整生態的框架,要克服的困難遠不是技術層面上看似的簡單,直到目前仍然沒有看到 5.x 宣佈支援 Promise, 讓我們拭目以待。

MVC 模式

MVC 模式也需要介紹嗎,我們天天都在聊 MVC,不管前、後端框架,說一句 MVC,對一下眼神,基本確定對方懂你了。

事實是,前端框架已經不適合用 MVC 討論,這個模式從1979年提出以來,作為萬精油模式,在各個框架和場景中被套用,揹負了太多的歷史包袱,大家可以看 winter 的文章 談談UI架構設計的演化。撥亂反正我覺得有希望,討論前端框架大家以後統稱 MV 模式就好了,就是模型和檢視分離。

我們今天要講的 MVC 模式是指在伺服器上(後端) MVC 模式,它的定義經受了時間和實踐的檢驗,在許多企業級 Web 框架的實現中高度一致。先列舉場景:

如果你的網站只有幾個簡單的頁面,所有邏輯都寫在 Controller 裡面,是沒有問題的。隨後網站迅速的增長,你發現,

  • 許多頁面裡面的檢視是一致的,但是背後的資料模型不一致。比如:網站上幾乎沒有一個檢視或者元件是獨一無二的,表格,下拉框等。
  • 許多頁面裡面的資料模型是一樣的,但是展現的檢視不一致。比如:同時支援PC和移動端,國際化本地化。

我們做一個數學模型模擬極端情況,大家很容易能看到問題

系統剛好是內積的情況

假設左邊是我們的系統最終的樣子,它剛好可以表示成 M(模型)和 V(檢視)的內積,我們更傾向於右邊的表達,因為它更簡潔而且沒有重複。這裡的內積操作大家就可以理解成控制器,實際上不會如此巧合,但是分離模型和檢視幫助我們提高程式碼複用,降低設計複雜度的好處是很顯然的,一個更通用的表達

MVC 架構模式

模型檢視和控制器之間都是單向連結,所以整個系統的行為非常可控且容易測試,單獨把路由分開是想強調 Router 和 Controller 是兩個概念,Router 只是一個觸發器(或者提供了一種對映關係),在寫測試的時候,我們也可以跳開 Router 單獨呼叫 Controller。

看到上面的兩種模式,是不是已經開始想,那有沒有一個框架同時是 Koa + Router + MVC 呢,推薦大家一個非常好用的企業級 Web 框架 ThinkJS 3.0,最新版的 ThinkJS 整合了大量最佳實踐和完善的文件,不管是學習或者企業級開發都非常推薦。而且 ThinkJS 同樣實現了接下來要講的模式。

DI 依賴注入模式

還是先說場景,假如服務端需要實現session,前期考慮到成本和使用者量,單臺伺服器存到檔案就夠用了。後期如果使用者量大的時候,需要橫向擴充套件(Scale-out),就把 session 實現基於中心化的 Redis 服務。

我們系統設計目標是:

  • 不需要修改業務邏輯程式碼實現替換
  • 不需要關注服務的建立和生命週期

解決這類系統擴充套件性問題有一個非常著名的設計原則 控制反轉(IoC Inversion of control),而 依賴注入(DI dependency injection) 就是其中的一個實現模式。

DI 的基本思路是這樣,首先我們的程式碼不能依賴具體的服務,需要總結歸納出一套抽象介面,業務實現依賴介面,而服務實現介面,最後通過框架專門負責建立和提供介面的例項。

DI 模式實現

這裡的 IoC 容器或者說 Ioc 框架,會在啟動的時候讀取配置檔案,並在執行的時候根據需要建立例項提供給使用者,在靜態語言如 javac# 需要用到反射等高階語法,而 JavaScript 本身是動態的,介面基於約定,並且使用的方式也更加靈活。比如 ThinkJS 3.0 裡面的 extend 和 adapter 就可以理解成介面和實現,如圖:

ThinkJS DI 模式實現

那之所以稱為 extend,是因為框架會直接把介面注入到 controller 或者 think 物件中。這樣的好處是使用起來更方便,缺點是不同 extend 需要約定好不能重名。

最後

本文介紹的三個架構模式,你會發現幾乎在所有的Web框架實現都大同小異,這就是模式的好處。模式的意義類似於 IoC,我關注抽象和介面,抹平了具體語言特性下的細節問題,幫助我們更好的學習,溝通和思考。

相關文章