rails, django, phoenix,你們錯了

寫這個題目估計會招人罵。

這三個著名的 MVC(或者 MTV) framework,分別對應 Ruby,Python,Elixir 三種語言。說他們是這幾門語言的頂樑柱毫不為過。很多人都是慕著 framework 的名而來,進而學了語言。典型的就是曾經大紅大紫(現在也算是一線明星)的 rails:很多 rails 工程師最初只知 rails,寫了 rails 後發現語言的短板才反過來學的 Ruby。Phoenix 和 Elixir 大抵也是如此。

在 django / phoenix 上能看得出 rails 的很多影子。rails 在 flickr / delicious 時代是工程師追捧的明星框架。其後有了很多其他語言的跟風者或者學習者,django 不算是第一個, phoenix 也不是最後一個。通過這些框架,工程師可以快速地創建一個 web 項目的腳手架,和資料庫(一般是 RDBMS)綁定,生成 model,controller 和 view,不消數日,一個可以運行部署的「網站」就攢出來了。

開發者的效率高么?很高。代碼的效率高么?rails / django 雖讓人詬病,但 phoenix 很高,在 benchmark 中狂勝各大 framework。

架構優秀么?似乎也很優秀 —— 如果讓你我從頭寫一套 web framework,決計趕不上它們的水平。

那它們錯在哪裡?

它們錯在給 web app 開發者帶來「人人都能寫 web app」的希望的同時,又把諸多程序員的思維禁錮在那一方小小的 MVC 中。

假設我們要做一個 MOOC 軟體。用戶可以瀏覽課程,可以註冊課程,收藏課程,在上課的過程中可以為課程評分,記筆記,並和別人互動,等等。

我們看通常情況下一個 rails 程序員如何開始構建其後端:

  • 設計資料庫結構:User / Content / Bookmark / Review / ...

  • rails new mooc

  • rails generate model

  • 把資料庫設計映射到 rails model 中

  • rails generate controller

  • 撰寫各個頁面的 controller 和 view

  • (如果有時間)撰寫 test

順著 framework 的思路,我們不知不覺地做了一些假設:

  • 所有的狀態都是存儲在一個或者若干個 database 中的

  • 如果某個 database 沒有 framework 的 adapter,那麼我們就無法使用

  • 數據是強耦合的,比如 User 和 Content 間有一張 enroll 的表作為用戶註冊哪個課程的憑證

  • 一個 controller 可以跨越多個 model 獲取數據,並提供給某個 view 把數據展現出來

有了這些假設,我們能夠很快地搭建出應用程序,卻付出了高耦合度的代價。

有同學疑惑了,MVC 設計模式的初衷不就是解耦么?為什麼反倒耦合度變高了呢?經典的 MVC 分層設計是一種縱向的解耦,數據有序流動,各層只管自己的工作,「上帝的歸上帝,凱撒的歸凱撒」,不必關心其他層次如何實現。然而它並不能避免橫向的耦合,比如 model 和 model 的耦合,controller 和多個 model 的耦合。而 web framework 卻有意無意地在倡導這種耦合。更令人髮指的是,它還將這種耦合做進了數據層面,使得日後無論是從代碼層面解耦,還是數據層面解耦,都困難重重。

在 rails 出現以前,我們知道寫代碼還有一個 business logic layer —— 業務層。在 rails 出現之後,在大家的實踐當中,業務層被莫名併入 model 層,有些功能還去了 controller,就此消失。然而,業務層被這樣揉進了一個 web framework 中,是不是哪裡不太對勁?

rails 們代表的 web 層並不是業務的全部。如果哪天我們要向第三方提供 API 呢?如果 web 的邏輯被大刀闊斧地改變怎麼辦?如果突然哪天公司被收購,用戶賬號整合到對方系統里,自己並不保留一個所謂的用戶表怎麼辦?

回到我們的 MOOC 軟體的例子里。課程的管理,排期,註冊等等,都是業務層的事情。一個用戶註冊一門課程,在業務層,應該表述成為:{:enroll, uid, cid} -> true/false,而非 controller 和 model 里那些繁雜的邏輯。而展示一個用戶訂閱的所有課程,應該表述為:{:show, uid} -> [a list of courses]。

所有這些,和 model 無關。User model 甚至不該看見 Content model,也看不見作為連接表的 enroll 表。

這是橫向的解耦。大家都是一個個黑盒的服務,user service 負責用戶的個人信息的維護和展示,auth service 負責驗證身份,content service 負責管理課程內容,content enroll service 處理 enroll 相關的事宜,等等。如下圖:

我們甚至還可以將這些服務按照屬性分成不同的部分,有些是核心服務,有些是社交服務,有些是交流服務。這些服務都有各自明確的介面,比如 auth 服務提供:

  • 用戶名密碼驗證:{user, pass} -> {:ok, access_token, refresh_token} or :error

  • token 驗證:access_token -> {:ok, token_info} or :error

  • token 刷新:refresh_token -> {:ok, access_token} or :error

  • 修改密碼:{refresh_token, old_pass, new_pass} -> {:ok, new_refresh_token}

auth service 存儲的數據只是用戶/密碼相關的信息,這信息只有 auth 服務自己知道,連 user service 都沒有訪問的許可權。

起初,這種解耦會帶來很多工作量,但隨著系統的發展,你會發現,這樣設計會為系統的擴展和可重用帶來很多的好處。添加新的服務並不會影響已有的服務,我們甚至可以撰寫一個已有服務的全新升級替代版,把部分流量導入新的服務,測試良好後把舊服務直接刪掉。

這樣做的另一個好處是重歸以業務為中心的正道。說句不太好聽的話,rails 等 framework 很容易引導人們走向一個 web 前端為中心的歧路。這裡所說的「前端」,是指後端的前端。我們應該根據需求,先把業務模型構建出來,各個服務構建妥當後,再使用 rails 等打造前端。我們可能需要一個面向用戶的前端,可能還要面向管理員的前端,每個獨立的服務可能也需要它們各自的管理前端,我們還要有統計分析的前端,用戶行為分析的前端等等。這些所有的前端基本都沒有所謂的 model,因為數據的存儲在各個服務中解決了。

如此這般,我們打破了上述的假設,數據變得弱耦合,每個服務有各自獨立的數據,它們只是在需要的時候被組裝起來。

至於這樣一個個服務嘛,你管它叫 micro service 也好,叫 application 也好,只要它們足夠獨立,能夠隨需而動就好。

延伸閱讀:

軟體設計雜談

系統開發之設計模式

推薦閱讀:

請問軟體開發與編程對電腦有什麼要求嗎?
談談到底什麼是抽象,以及軟體設計的抽象原則
學3d max 要有什麼基礎?
軟考中級的軟體設計師難考嗎?
Bash 的威力

TAG:软件设计 | 架构 | 网站架构 |