MVC框架的對映和解耦

四火的嘮叨發表於2012-10-23

來源:四火的嘮叨

最近在寫一個業務上用到的框架,回想起接觸過的一些MVC框架,尤其是主要貢獻在後端表現層上的那些,它們之間有太多的相似,在不斷解耦的過程中,層數和模組數也越來越多,需要不斷引入層與層之間的對映邏輯將不同層次之間關聯起來,我們不妨來檢視一下這個過程,能否尋找一些MVC框架的共性和啟示。

MVC框架的對映和解耦

MVC 1到MVC 2模型的進化

這個話題有點老。MVC 1在桌面程式中應用較多,業務邏輯當然放在Model裡面,Controller負責將使用者的請求資料傳遞到Model去,之後就放手不管了,讓View通過觀察者模式不斷獲知Model的最新變化(可以是Model變化後通知View,也可以是View自己來獲取)。這個模式看起來很簡單,不過很容易發現一個嚴重的問題,View必須對Model瞭如指掌,要不然怎麼觀察它呢。這實在是一件不甚合理的事情。

MVC框架的對映和解耦

MVC 2則解開了Model和View的耦合,Controller變成了中介者一樣的角色,View接收了使用者輸入,Controller把處理請求分派給Model,處理完後,又把結果交回View來展現。不過,這樣的代價是Controller變成了一個百事通,如果它要關心Model和View的具體實現,耦合的問題只是換了一件外衣而已。所以,需要對Controller進一步解耦。下面的話題,也是藉由這一點展開的。

MVC框架的對映和解耦

從這個改變就可以看出在解耦方面的進化,但是依然沒有做足,後來ASP.NET又出了MVC 3MVC 4,我沒有去了解其中的變更。這只是關於解耦的一個前傳,下面讓我們回到正題,看看那些隨著解耦的進一步進行,新產生的對映邏輯和配置。

URL Mapping

也許最早會有人這樣寫程式碼:

但是現在應該已經不會有人這樣寫了,印象中即便是最早只是用JSP+Servlet寫程式的那一批程式設計師寫網站應用的時候,URL和控制器入口的對映邏輯也已經被獨立出來了,例如Tomcat的web.xml:

但是這樣的配置匹配的表示式不夠靈活(例如無法自定義匹配邏輯),而且配置過於冗長(通常來說,我是一個xml配置檔案的痛恨者),於是現今的MVC框架都提供了一套自己的對映匹配邏輯,例如Struts2:

還可以配置全域性的跳轉邏輯,傳遞引數等等,總之配置是靈活多了,可是每寫一段控制器的邏輯,還是需要配置一段到XML檔案中。這樣的問題也是可以解決的,將變化點獨立到Action裡,配置檔案中只寫這個變化的引數,這樣只需要一個配置就可以完成大部分跳轉了。

對於不同引數名稱和引數個數變化的情況,上面的辦法支援得又不好了,好在許多框架都提供了註解配置的辦法,把URL對映的邏輯變成短短的註解:

可是這幫難伺候的程式設計師啊,還是嫌麻煩,這就需要利用CoC原則(規約優於配置,Convention over Configuration)。在Spring MVC中,宣告瞭ControllerClassNameHandlerMapping以後,對於這樣沒有配置任何對映資訊的方法:

類名叫做ExampleController,方法名叫做e1,因此在使用如下URI進行訪問的時候,就自動mapping到這個example方法上:

好了,這總滿足懶惰的程式設計師你了吧。

Data Binding

這裡Data Binding(資料繫結)指的是將使用者請求提交上來的資料和領域模型繫結起來,即生成若干個攜帶資料的模型物件。

自不必說,最原始的方式應該是類似這樣的解決方法:

這當然不會入程式設計師的法眼了,於是框架替你把引數繫結到一個資料集合的物件上,你獲取起來就容易多了,比如在Grails框架中,可以這樣寫:

或者乾脆換成引數對映的配置檔案,可是還是好囉嗦,於是“規約優於配置”又來了,以Struts2為例:

這種情況下,只要提交這樣的請求:

這個name為Jim、age為18的User物件就自動被塞進這個Action去了。Spring MVC的情況類似,只不過粒度更小,引數注入的不是類Action例項的屬性,而是Controller方法的引數——當然,思想是一樣的。

檢視指向

你可能猜到我要說的內容了。程式設計師最原始的做法應該是類似這個樣子的:

之後進化成為配置檔案配置的形式、註解配置的形式,有了前文的介紹,這實在是沒什麼特別的。值得一提的是,我用過一個框架,它對於URL Mapping(front-controller做的事情)和View Routing(backend-controller做的事情)通過這樣一種有趣的機制來完成:

1.根據URL路徑和Controller返回的結果字串去尋找相應目錄下對應名稱的handler;

2.如果找不到就找defaultHandler;

3.如果還是找不到就往上一級目錄去找,依此類推。

舉例來說,Controller返回View的路徑為“user/admin/do”,就到…/user/admin目錄下尋找一個do.handler的檔案,找不到就尋找同目錄下的default.handler,再找不到就往父目錄去遞迴尋找。這種機制就使得URL Mapping和View Routing的過程變得具有天然的繼承性(比如公用的success.handler可以放在頂級目錄中)。

這種方式其實也是配置,但是既不是配置檔案,也不是註解,更不是程式碼,而是一個檔案和資料夾的組織結構。

最後,你肯定知道我還是要回到“規約優於配置”上面來。效果就是,例如訪問user/do預設完成後就去尋找…/user/success.jsp,異常後通過異常攔截器首先尋找…/user/failed.jsp。

頁面聚合

對於服務端頁面模板的組織在我看來一直是網站應用程式設計中比較薄弱的一塊(客戶端頁面聚合即前端頁面聚合我在此先不討論),直到現在,頁面模板的程式碼還是極容易陷入過於複雜和不易理解的境地。最開始追溯到JSP誕生以前的時代,頁面是可以由Servlet一行一行輸出的:

那個時候還沒有頁面模板的概念。於是JSP出現,可以把頁面HTML和頁面上用於展示的Java程式碼糅合在一起。至於JSP最初就容易被誤用做了更多的展示以外的事情,那其實並不是工具本身的錯。直到現在,還有許多人對於Servlet和JSP有相當的偏見,在程式設計師聊天的時候,你要是說你的網站是用Servlet+JSP做的,對方往往會直接鄙視你,用那麼老土的技術。其實技術本身並沒有任何錯,Servlet+JSP依然可以非常漂亮地解決很多實際問題。

對於頁面模板,無論你是使用JSP,還是FreeMarker、Velocity,你都會面對一個問題,一個和Java程式碼、C++程式碼一樣需要依賴和組織的問題。於是程式設計師就將頁面分為幾個子頁面,通過這樣的方式引入:

不過,對於一些公共的頁面而言,可能要被許多頁面引用,幾乎所有的結果頁面都要引入header.jsp、menu.jsp、footer.jsp……並傳遞一些類似的引數。這讓囉嗦的程式設計師又覺得不開心了,我應該把我有限的精力專注到業務特有的邏輯和頁面上去,這些通用的部分框架能不能替我聚合,而我就不需要關心了?

這和異常處理很像,很多專案都喜歡定義自己的總異常,繼承自RuntimeException,不需要宣告,而且在通用異常攔截器內統一處理這些未被捕獲的異常,完成通用的邏輯處理和頁面轉向;而錯誤資訊就通過異常攜帶出來了,程式設計師就不需要把精力分散到大量的異常資訊傳遞上面——比如通過返回碼這種需要單獨處理的形式,記得在老專案(特別是儲存過程)的業務邏輯中還經常看到錯誤資訊的返回碼,現在真是越來越少見了。

於是Tiles給了這樣的頁面聚合辦法,配置檔案:

並且可以靈活地使用繼承和引數傳遞,可是依舊不爽,每一個頁面跳轉都要配置這樣一塊豆腐乾,實在是很囉嗦。SiteMesh提供了一種更為簡潔的配置方式:

這樣一來,所有的請求(除了匹配“/admin/*”這樣的)全部都走到基於main.jsp聚合的邏輯中去了,通用的部分全部在main.jsp中完成,變化的頁面依然根據原有的View Routing的對映來尋找頁面,聚合這件事情,就真正對後續開發的程式設計師透明瞭

對於框架來說,還有進一步解耦的需求嗎?有。比如可配置的攔截器,對於不同的請求能夠使用配置為不同數量和不同個數攔截器的“攔截器棧”來響應,既可能有前置處理,也可能有後置處理。攔截器把原本在許多業務裡都要重複做的事情(比如許可權校驗)通過AOP這種形式橫向切一刀給做了。再比如序列化,如果要返回頁面,形式可能是text/html的,而要傳遞物件,形式可能就是application/json這樣的,將頁面或者物件轉換成html或者JSON響應的活兒,程式設計師當然也不想幹……

縱觀上面介紹的這些MVC框架在解耦和對映方面做的貢獻,我們很容易看到,在不斷地解耦過程中,層數、模組數不斷在增加,複雜性應該說也在增加,配置當然更復雜,可是愛偷懶的程式總有辦法讓複雜變得簡單。這個因解耦引起層與層之間對映的配置便是如此:

1.程式設計師自己實現;

2.框架實現,但是需要手動配置;

3.規約優於配置。

正是程式設計師對於懶惰的追求,造就了一個又一個好用的MVC框架,現在開發一個網站對於十多年前來說,實在是簡便太多太多了,在今天談論的角度上,未來MVC框架還會有怎樣的發展趨勢呢?還有哪一些通用的部分會被解耦出來,你又怎麼看?

相關文章