你真的理解了MVC, MVP, MVVM嗎?
前言: 準(zhǔn)備寫這篇文章的時(shí)候 , 我自認(rèn)為對(duì)MVC已經(jīng)有深刻理解了,可是畫圖的時(shí)候發(fā)現(xiàn),理解還是有漏洞,于是又閱讀,思考,整理,加深了理解, 寫了這篇文章, 估計(jì)還有漏洞,歡迎討論。
這再一次說(shuō)明了寫作的好處: 很多時(shí)候自以為理解了,實(shí)際上腦海中有很多想當(dāng)然的假設(shè),寫作會(huì)把這些假設(shè)給暴露出來(lái)。
大概是二三十年前, 人類逐漸從命令行界面時(shí)代走出來(lái),進(jìn)化到了GUI時(shí)代。
注: GUI(Graphic User Interface),即圖形用戶接口。
(一個(gè)命令行程序)
(一個(gè)帶有圖形界面的桌面應(yīng)用程序 ,自己畫的,有點(diǎn)丑?。?/p>
每當(dāng)人類努力地開(kāi)發(fā)新的桌面GUI程序的時(shí)候, 至少要搞定下面幾類工作:
1. 界面(以及界面中元素的)布局。
這是一件挺費(fèi)勁的工作, 要盡可能地美觀漂亮,要不然就賣不出去。
2. 界面上有些“邏輯”需要處理
比如上圖中那個(gè)薪水計(jì)算程序,“計(jì)算” 按鈕默認(rèn)是灰色的, 不能點(diǎn)擊,用戶輸入了稅前收入以后, “計(jì)算”按鈕就會(huì)被激活,表示計(jì)算了。
3. 所謂的業(yè)務(wù)邏輯。
用戶點(diǎn)擊了“計(jì)算”按鈕以后,計(jì)算五險(xiǎn)一金,個(gè)人所得稅和稅后收入。
這三者攪在一起,讓程序代碼凌亂不堪,稍微復(fù)雜點(diǎn)兒的程序就長(zhǎng)達(dá)幾千行, 不斷地挑戰(zhàn)著程序員的底線,修改別人的代碼,添加新的功能要比從頭寫難好多倍!
大家都在泥潭中掙扎。
桌面應(yīng)用程序的 MVC
程序越來(lái)越復(fù)雜,Bug越來(lái)越多,沒(méi)辦法, 大家只好去求編程上帝。
上帝說(shuō): 想從困境中走出來(lái),一定要實(shí)現(xiàn)關(guān)注點(diǎn)分離(Separation of concerns)。
沒(méi)人能夠理解。
上帝解釋道:“ 你們?nèi)四X同時(shí)能處理的東西是有限的, 所以要把一個(gè)大系統(tǒng)給分解,變成幾個(gè)相對(duì)獨(dú)立的部分,這樣我們的大腦每次只關(guān)注某一方面,暫時(shí)忽略其他的,就能夠掌控了。”
沒(méi)人知道該怎么分解。
上帝只好想了一個(gè)辦法, 把關(guān)注點(diǎn)分離的理論給具體化,這個(gè)辦法就是MVC。
上帝告訴人類:
M 表示 Model , 專門用來(lái)處理業(yè)務(wù)邏輯,不干別的事情。
例如在那個(gè)薪水計(jì)算系統(tǒng)中。計(jì)算一個(gè)人的薪水,五險(xiǎn)一金,個(gè)人所得稅等等。
V 表示View, 專注頁(yè)面布局和數(shù)據(jù)顯示。
例如把Button放置到某個(gè)位置,把總收入顯示到一個(gè)文本框,把稅金顯示到另外一個(gè)地方。
C 表示Controller 翻譯用戶的輸入,操作模型和視圖。
例如,用戶在界面點(diǎn)擊了一個(gè)“計(jì)算”的按鈕,View 把計(jì)算的請(qǐng)求傳遞給Controller (很明顯View需要知道Controller,換句話說(shuō),需要持有Controller的實(shí)例),Controller找到或者創(chuàng)建Model,執(zhí)行業(yè)務(wù)邏輯:計(jì)算薪水。
計(jì)算的結(jié)果該怎么展示呢? 人類問(wèn)道。
上帝胸有成竹: 可以讓Model 去通知View。
Model需要持有View的實(shí)例(當(dāng)然也可以通過(guò)觀察者模式),調(diào)用View對(duì)應(yīng)的方法。
例如: View中可能有一個(gè)onResult的方法, 讓Model去調(diào)用,在調(diào)用的時(shí)候把一個(gè)參數(shù)對(duì)象Salary傳遞過(guò)來(lái),不就可以展示數(shù)據(jù)了嗎?
- // View的方法,被Model調(diào)用:
 - public void onResult(Salary salary){
 - //把個(gè)人所得稅(salary.getTax()) 展示到一個(gè)文本框
 - //把凈收入(salary.getNetPay()) 展示到另外一個(gè)文本框
 - ......
 - }
 
畫成流程圖的話是這個(gè)樣子:
大家都覺(jué)得MVC大法好,紛紛開(kāi)始使用。
MVP
時(shí)間久了以后,人類就覺(jué)得不爽了,因?yàn)樵谶@個(gè)MVC中,依賴太多:
View 依賴Controller和Model
Controller依賴View和Model
Model 和View的關(guān)系雖然很弱, 但是也需要某種方式來(lái)通知View進(jìn)行數(shù)據(jù)更新。
人類說(shuō):“他們之間的耦合還是挺緊密的啊,親愛(ài)的上帝,能不能改改?”
上帝覺(jué)的人類還是挺有上進(jìn)心的,決定繼續(xù)施以援手: “這樣吧, 可以改變一下Controller, 把Model和View完全隔離開(kāi),讓他們單獨(dú)變化。”
上帝把Controller 改了個(gè)名稱,叫做Presenter, 把整體命名為MVP。
在MVP當(dāng)中,View只知道Presenter, 不知道Model 。
計(jì)算流程和MVC差不多,用戶點(diǎn)擊了“計(jì)算薪水”按鈕, View去調(diào)用Presenter, Presenter操作Model , Model 中進(jìn)行業(yè)務(wù)計(jì)算。 關(guān)鍵點(diǎn)是,Presenter去更新View。
- //Presenter 的方法,被View調(diào)用
 - public void calculateSalary(){
 - //調(diào)用Model計(jì)算薪水
 - view.showTax(xxx); // 調(diào)用View顯示所得稅
 - view.showNetPay(xxxx);// 調(diào)用View凈收入
 - ......
 - }
 
但是Presenter還是需要調(diào)用View的方法,也就是說(shuō)Presenter對(duì)View有依賴,這樣Presenter就沒(méi)辦法單獨(dú)做單元測(cè)試,非得等到界面做好以后才行。
于是上帝又做了一點(diǎn)改進(jìn),讓View層提取出接口,Presenter只依賴這個(gè)接口。
這樣Presenter不用依賴真正的界面就可以測(cè)試了,并且也增加了復(fù)用性,只要View實(shí)現(xiàn)了那個(gè)接口,Presenter就可以大發(fā)神威。
MVVM
使用了一段時(shí)間MVP以后,永不滿足的人類又覺(jué)得不爽了, 因?yàn)樽孭resenter調(diào)用View的方法去設(shè)置界面,仍然需要大量的、煩人的代碼,這實(shí)在是一件不舒服的事情。
人類突發(fā)奇想: 能不能告訴View一個(gè)數(shù)據(jù)結(jié)構(gòu),然后View就能根據(jù)這個(gè)數(shù)據(jù)結(jié)構(gòu)的變化而自動(dòng)隨之變化呢?
上帝看到人類思考了,表示了贊賞。
他說(shuō),我來(lái)送你們一個(gè)叫做ViewModel的東西,它可以和View層綁定。 ViewModel的變化,View立刻就會(huì)變化。
人類問(wèn): ViewModel? 里邊有什么東西?
上帝說(shuō): 拿你們的薪水計(jì)算為例, ViewModel 差不多這樣:
- public class SalaryViewModel{
 - String grossSalary; //稅前收入,和View中的相關(guān)字段對(duì)應(yīng)
 - String netSalary; //凈收入,和View中的相關(guān)字段對(duì)應(yīng)
 - String tax; //個(gè)人所得稅,和View中的相關(guān)字段對(duì)應(yīng)
 - ......
 - boolean isCalculating; // 一個(gè)標(biāo)志位,表示正在計(jì)算
 - String errMsg; // 如果出錯(cuò)的話,記錄出錯(cuò)消息。
 - }
 
當(dāng)用戶在界面上點(diǎn)擊“計(jì)算”按鈕的時(shí)候, 你們需要設(shè)置一個(gè)SalaryViewModel中的標(biāo)志位:
salaryViewModel.isCalculating = true;
這樣View 中就可以自動(dòng)給用戶展示一個(gè)消息:“正在計(jì)算....”
當(dāng)薪水計(jì)算完成的時(shí)候, 如果沒(méi)有錯(cuò)誤,SalaryViewModel 中g(shù)rossSalary, netSalary,tax等屬性就有了值。 與此同時(shí)View 中對(duì)應(yīng)的內(nèi)容也會(huì)更新, 不用你們手工去設(shè)置, 很方便吧?
如果計(jì)算過(guò)程出錯(cuò), SalaryViewModel 的errMsg 會(huì)保存出錯(cuò)消息, 同樣,View中會(huì)自動(dòng)把這個(gè)錯(cuò)誤消息給顯示出來(lái), 很智能吧?
人類說(shuō):“怎么可能這么智能呢? 這里的ViewModel 好像和View沒(méi)有什么關(guān)系?。?到底該怎么綁定????。。?rdquo;
上帝笑了: 你們可以開(kāi)發(fā)一個(gè)框架嘛? 讓兩者綁定起來(lái)不就行了?
人類沒(méi)有辦法,只好自己動(dòng)手。
(注:實(shí)際上微軟的WPF和Silverlight, Android等框架和系統(tǒng)都可以實(shí)現(xiàn)View和ViewModel之間的映射和綁定)
Web應(yīng)用程序的MVC
時(shí)間過(guò)得飛快,人類發(fā)明了互聯(lián)網(wǎng),Web應(yīng)用程序如雨后春筍般崛起,B/S(瀏覽器-服務(wù)器)開(kāi)始大行其道。
用戶通過(guò)瀏覽器發(fā)出GET,POST請(qǐng)求,服務(wù)器端進(jìn)行處理,處理完以后生成HTML給瀏覽器。
無(wú)論什么操作,都是對(duì)服務(wù)器端URL的訪問(wèn)。
人類突然發(fā)現(xiàn),整個(gè)編程模型發(fā)生了巨變, 不能簡(jiǎn)單地套用原來(lái)的MVC和MVP了。
如果把HTML頁(yè)面比作原來(lái)桌面應(yīng)用程序的View, 服務(wù)器無(wú)論是Controller還是Model都是無(wú)法遠(yuǎn)程遙控這個(gè)View進(jìn)行處理的。
人類這一次沒(méi)有去請(qǐng)教上帝,自己嘗試對(duì)MVC進(jìn)行改良,其中有個(gè)叫Rod Johnson 帶領(lǐng)一幫人搞出的SpringMVC很成功。
不像桌面應(yīng)用的MVC, 這里的Model沒(méi)法給View 發(fā)通知。
也不像MVP, 這里的Controller 也不會(huì)調(diào)用View的方法來(lái)設(shè)置界面。
實(shí)際上Controller 會(huì)選擇一個(gè)View, 然后把模型數(shù)據(jù)“丟過(guò)去”渲染。
原來(lái)的View 變成了 View Template(例如JSP , Velocity等等), 經(jīng)過(guò)渲染后變成HTML發(fā)給瀏覽器展示給用戶。
有人把這種MVC稱為 基于Web的 MVC,以便和之前的MVC區(qū)別開(kāi)來(lái)。
前后端的分離
人類早期的B/S應(yīng)用程序中, 每次訪問(wèn)服務(wù)器端, HTML就會(huì)整體發(fā)給瀏覽器,即所謂的整體刷新。
后來(lái)人類發(fā)明了AJAX, 可以做到局部刷新。
于是瀏覽器端的應(yīng)用變得越來(lái)越復(fù)雜,再后來(lái)人類竟然發(fā)明了Web上的SPA(單頁(yè)應(yīng)用程序),用起來(lái)的體驗(yàn)和最初的桌面應(yīng)用程序越來(lái)越像。
人類發(fā)現(xiàn),那些MVC, MVVM之類的模式完全可以用到瀏覽器端嘛!
例如在瀏覽器端使用MVVM , 在服務(wù)器端可以使用MVC, 兩者結(jié)合起來(lái):
前端和后端成功地分家了 !
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)作者微信公眾號(hào)coderising獲取授權(quán)】
























 
 
 













 
 
 
 