1. 程式人生 > >也許後端MVC的說法已經過時了

也許後端MVC的說法已經過時了

呃,標題有點聳人聽聞,不過我並不是標題黨。考慮到談論大而虛的東西(比如最好的語言)容易引起爭論,所以還請諸君帶著看戲而不是庭辯的心態來看待本文。

依我個人所見,後端框架,類似於MVC這樣的組織方式已經顯得過氣了。

過去,在建立應用時通常會按MVC各建一個資料夾,每個資料夾就是一個模組。MVC三者的職責是這樣的:

  1. Controller繫結到某個路由上,接著處理請求引數,然後建立在整個請求中可見的物件,並進行一些業務邏輯上的工作。期間會從資料庫中構造Model,也有可能新建/修改Model後將它們儲存入資料庫。最後,Controller會通過View響應使用者的請求,或者返回重定向的報文。基本上業務邏輯都是實現在Controller裡面的。(下文為了闡述方便,都以“業務邏輯”特指Controller中的業務邏輯)

  2. 每個Model往往對應資料庫的一個實體。Model類除了裝資料之外,還提供了跟自己身份相關的一些方法,比如User類提供authenticate方法以用於驗證密碼。Model物件通常還會負責檢驗構造自己的引數是否正確。

  3. View負責使用給定的引數渲染模板,並作為響應返回給使用者。View一般是由該框架提供的模板語言寫成。

現在,一個後端應用如果還是按MVC的方式劃分,似乎有些不適應了。

後端MVC的歷史

我們先從後端MVC的歷史開始講起吧。MVC最初發端於客戶端程式的開發,旨在用Controller在Model和View之間進行解耦。如果我沒記錯的話,gang of four的《設計模式》裡面提到MVC,就是以客戶端程式作為例子。

跟客戶端開發類似的,後端程式一開始也只有View這一層。在很久很久以前,後端程式都是這樣執行的:使用者一個HTTP請求進來,伺服器通過cgi呼叫一個程式生成文字作為響應。這時期大部分後端程式,看上去都像是模板語言(見過初學者寫過的JSP/PHP嗎?)。因為它們主要做的事情,就是從使用者輸入和資料庫中獲取資料,並拼接字串生成文字。後來後端程式開始演化得越來越複雜,單單一層View已經不適應了。由於需要把邏輯從View中分割開來,後端程式開始走上客戶端程式走過的路,進行MVC的分離。於是,負責路由和業務邏輯處理的部分變成了Controller,負責資料處理和持久化的部分變成了Model。

雖然後端程式也是做了MVC的拆分,但是它跟客戶端的MVC其實是不同的。
在客戶端裡,Controller把View和Model分離開來,實現View和Model的解耦合。View上的變化,通過Controller傳遞給Model,然後再將Model最新的資料通過Controller傳遞迴View。View:Controller:Model的比例通常是N:1:N,其中每個View基本對應一個Model。

然而,在後端程式裡面,View和Model通常沒有很強的對應關係。一般意義上的CRUD,基本上是Controller(業務邏輯)圍繞著Model(資料層)在轉。View扮演的往往是跑龍套的角色。

還需要V層嗎

在客戶端MVC中,View扮演的是跟其他二者三足鼎立的角色。使用者的輸入經過View,底層資料的變更通過View反饋給使用者。

然而後端MVC中,View的地位搖搖欲墜、可有可無。前文提到,Controller繫結在路由上,接收請求;Controller渲染模板,傳送響應。跟客戶端不同的是,Controller只有在渲染模板時才用上View。View的戲份一下子被砍掉了一半。禍不單行,Controller並不一定需要渲染模板來發送響應,它可能直接就重定向了;或者更常見的是,Controller直接把一個物件JSON化,並把它響應給使用者。這麼一來,View的戲份還剩多少?

有些後端框架提供了JSON格式的模板,多多少少試圖挽救View的沒落地位。可惜並沒有什麼用。
過去,View通常由三部分組成:html程式碼,控制流程語句,渲染時的上下文。如果提供的是JSON格式的模板,那麼View的前兩部分基本不需要了,只需要渲染時的上下文。可是這麼一來,為什麼我還需要渲染一個JSON模板,直接用上下文的資料創建出個例項,由它生成JSON字串,不也行嗎?雖然我多了個用於響應的類,但是少了個JSON模板啊,而且說不定就不用給View留個資料夾。

最近我參與開發的幾個後端應用,根本沒有View的容身之地。所有的響應都是JSON格式,都是由特定的類JSON化出來的。
很多情況下,後端應用要麼僅僅是大後端系統中的一個元件,要麼需要跟多種來源的客戶端打交道。通常它們只是作為資料的守護者,API的執行人,僅響應以JSON資料。至於接收者想用這些資料做什麼,那是它們的事了。也許是拿來渲染前端頁面,也許是儲存在客戶端的資料庫裡,也許是拿去進一步分析處理。View已經退化到算不上一個層了。

M負責資料實體還是負責資料的訪問

說完搖搖欲墜的View,接著說地位尷尬的Model。Model是資料的化身,後端開發千變萬變,核心都是資料的處理。可以說,Model就是佔了個風水寶位。不過在我看來,當前常見的做法——只劃分一個Model包——並不夠清晰。

以我愚見,後端程式中的Model其實做了兩件事。一件事是表示了資料實體,另一件則是負責資料的訪問。按照單一職責原則,Model這樣一身飾兩角是不對的。資料實體是一回事,對應的資料實體的訪問是另一件事,兩者不能混起來。

假設儲存Account需要一個事務,在這個事務裡面要更新AccountBalance兩個實體。下面是Rails裡面的做法:

# always save Account in a transaction
Account.transaction do
  balance.save!
  account.save!
end

問題是,這段程式碼應該放到哪裡?一個做法是放到Controller裡面,但是儲存Account的方式,不應該放到Account裡面嗎?另一個做法是放到Account類裡面,但是為什麼不放到Balance裡面呢,這個事務也儲存了Balance。作為程式設計師,在這件事上可不能偏心哦。
如果提供了DAO作為中間層,那麼就不會這種“偏心”的顧忌了。而且這種帶事務的儲存,跟Account類自帶的save方法的差異,也從層級上體現出來。

此外,Model層裡面的類,不一定對應著資料庫上的表。每個Model都知道如何持久化自身資料,這種假定是無法一直保持下去的。如果沒有把資料實體和訪問資料實體的元件區分開來,總有一天會陷入名不符實的危機中。

一個好的例子是,SQLAlchemy提供了Session類來完成對具體資料(Model)的訪問操作(事務、儲存等等),這樣僅需稍加包裝,我們就能分離出一個數據訪問層出來,避免資料實體和資料訪問間糾纏不清。

session = Session()
try:
    account = session.query(Account).get(...)
    balance = session.query(Balance).get(...)
    ... # 對account和balance做些修改
    session.commit()
except:
    session.rollback()

C:什麼都往裡裝

調侃了View和Model,是時候對最後的Controller下手了。相對於View負責展示,Model負責資料,Controller的職責並不清晰。Controller是個筐,什麼都可以往裡面裝。凡是無法區分到View和Model的,都放到Controller裡面吧。所以,在MVC中,Controller往往是最臃腫的。

終於有一天,我們下定決心要整治下Controller亂七八糟的環境。一個通常的做法是,把某個路由上Controller的函式,拆分成若干個小函式。這些小函式不繫結路由,純粹就是業務邏輯的抽象。拆分之後,Controller不再臃腫了,抽象出來的業務邏輯也可以被複用。
其實往更深一點思考,也許Controller本來就可以拆成兩部分,一部分負責繫結路由,另一部分負責業務邏輯。

  • 繫結路由的部分,負責解決請求資料的完整性和正確性,及限流、鑑權等操作。在它的眼裡,看到的是HTTP報文。

  • 業務邏輯的部分,負責具體業務處理。在它的眼裡,看到的是使用者的操作。

這樣一來,業務邏輯的實現就跟路由繫結解耦合。我們可以給不同的路由提供一樣的業務邏輯處理的同時,保持在限流等方面上的區別對待。我們也可以以此解決API設計上的遺留問題——舊的API,就讓它們呼叫到新的業務邏輯上。

新的劃分方式

  • View消亡了

  • Model分離成兩層,一層負責資料實體,另一層負責資料的訪問。

  • Controller分離成兩層,一層負責繫結路由,另一層負責業務邏輯。

轉自:

http://segmentfault.com/a/1190000004213733