Web 框架的架構模式探討

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

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

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

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

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

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

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

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

不好好寫代碼看哲學文章不是偶然,在文章落筆之前,我有思考過在 JavaScript 這門動態,多範式,單線程,基於事件 I/O 的語言環境下,甚至在當前時代,模式是否還有意義?

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

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

但是…… 模式這一稱謂仍然不斷出現,直到今天我們亦在大量使用。

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

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

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

Middleware 中間件模式

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

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

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

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

這些場景由來已久,很久以前 J2EE 總結了 Intercepting Filter 模式,其中最初級的方案代碼如下:

public class DebuggingFilter implements Processor {
  private Processor target;
  public DebuggingFilter(Processor myTarget) {
    target = myTarget;
  }
  public void execute(ServletRequest req,
  ServletResponse res) throws IOException,
    ServletException {
    // preprocess
    target.execute(req, res);
    // post-process
  }
}

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

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

Express 中間件模式

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

var express = require('express');
var app = express();

這個模式包含了一套聲明式的路由規則,和 middleware 函數上的 next 簽名,它們共同構成了整個中間件模式的控制流,如圖:

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

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

/*
   文件 bird.js
*/
var express = require('express')
var router = express.Router()


router.get('/'function (req, res) {
  res.send('Birds home page')
})


module.exports = router


/*
   文件 app.js
*/


var birds = require('./birds')


// ...


app.use('/birds', birds)

Koa 異步中間件模型

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

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

MVC 模式

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

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

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

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

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

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

模型視圖和控制器之間都是單向鏈接,所以整個系統的行爲非常可控且容易測試,單獨把路由分開是想強調 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 的基本思路是這樣,首先我們的代碼不能依賴具體的服務,需要總結歸納出一套抽象接口,業務實現依賴接口,而服務實現接口,最後通過框架專門負責創建和提供接口的實例。

這裏的 IoC 容器或者說 Ioc 框架,會在啓動的時候讀取配置文件,並在運行的時候根據需要創建實例提供給使用者,在靜態語言如 java,c# 需要用到反射等高級語法,而 JavaScript 本身是動態的,接口基於約定,並且使用的方式也更加靈活。比如 ThinkJS 3.0 裏面的 extend 和 adapter 就可以理解成接口和實現,如圖:

那之所以稱爲 extend,是因爲框架會直接把接口注入 (mixin) 到 controller 或者 think 對象中。這樣的好處是使用起來更方便,缺點是不同 extend 需要約定好不能重名。

最後

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7C0dQt8bV_RGszbKtRfpwg