MVC是一種設計模式(Design pattern),也就是一種解決問題的方法和思路, 是上世紀80年代提出的,到現在已經頗有歷史了。 MVC的意義在于指導開發者將數據與表現解耦,提高代碼,特別是模型部分代碼的復用性。
MVC不僅僅存在于Web設計中,在桌面程序開發中也是一種常見的方法。MVC的出現已經有一段歷史了。 記得我最早了解到MVC的時候,是在Microsoft的Visual C++ 中的MFC中。 當時年少無知,以為是MFC中特有的東西。后來隨之不斷學習,才發現自己的天真。 所以說,學得越多,就越覺得自己無知。越覺得自己無知,就越懂得敬畏和謙遜。 從這個角度講,同學們,最好不要看不起謙遜的人。
有個這么一個段子,說一天A君在圈內聚會時,朋友介紹了另一個人B君互相認識。 聚會場合嘛,這很正常,也很普遍。于是AB君小聊了一下。按國人的習慣,A君就問了“先生在哪高就?”。 B君只說了句,“談不上高就,炒炒股。” “哦,原來是炒股的。”A君心想,雖沒覺得什么不對,但心理覺得B有點low,只是沒說破,也沒表現出來。 過后了一段時間,一次偶然機會,發現原來B君是國內某上市公司的二股東,身家過億。 人家沒說慌,確實是炒股的……
話說遠了,我們還說正題。MVC是三個單詞的縮寫:Model, View, Controller。 MVC是一種設計模式,目前幾乎所有的Web開發框架都建立在MVC模式之上。 當然,最近幾年也出現了一些諸如MVP, MVVM之類的新的設計模式。 但從技術的成熟程度和使用的廣泛程度來講,MVC仍是主流。
Yii是一個Web框架,從Web開發的分工來講,Yii的開發工作中,承擔后端的內容多一些,畢竟主要就是PHP開發。 前端主要是在HTML、JavaScript、CSS上進行開發,然后通過Yii把前端的內容管起來,如通過Assets等。 這一章要講的MVC,主要是針對后端的。 前端的MVC嚴格來講不屬于Yii的范疇,這里我們就不作過多介紹。 如果想了解前端的MVC,可以看看Backbone.js Angular.js等前端框架。
## MVC的三要素[](http://www.digpage.com/mvc.html#id2 "Permalink to this headline")
MVC是模型(Model)、視圖(View)、控制器(Controller)3個單詞的縮寫。 下面我們從這3個方面來講解MVC中的三個要素。
* Model是指數據模型,是對客觀事物的抽象。 如一篇博客文章,我們可能會以一個Post類來表示,那么,這個Post類就是數據對象。 同時,博客文章還有一些業務邏輯,如發布、回收、評論等,這一般表現為類的方法,這也是model的內容和范疇。 對于Model,主要是數據、業務邏輯和業務規則。相對而言,這是MVC中比較穩定的部分,一般成品后不會改變。 開發初期的最重要任務,主要也是實現Model的部分。這一部分寫得好,后面就可以改得少,開發起來就快。
* View是指視圖,也就是呈現給用戶的一個界面,是model的具體表現形式,也是收集用戶輸入的地方。 如你在某個博客上看到的某一篇文章,就是某個Post類的表現形式。 View的目的在于提供與用戶交互的界面。換句話說,對于用戶而言,只有View是可見的、可操作的。 事實上也是如此,你不會讓用戶看到Model,更不會讓他直接操作Model。 你只會讓用戶看到你想讓他看的內容。 這就是View要做的事,他往往是MVC中變化頻繁的部分,也是客戶經常要求改來改去的地方。 今天你可能會以一種形式來展示你的博文,明天可能就變成別的表現形式了。
* Contorller指的是控制器,主要負責與model和view打交道。 換句話說,model和view之間一般不直接打交道,他們老死不相往來。view中不會對model作任何操作, model不會輸出任何用于表現的東西,如HTML代碼等。這倆甩手不干了,那總得有人來干吧,只能Controller上了。 Contorller用于決定使用哪些Model,對Model執行什么操作,為視圖準備哪些數據,是MVC中溝通的橋梁。
對于MVC中三者的劃分并沒有十分明晰的定義和界線。MVC設計模式只是一種指導思想, 讓你按照model, view, controller三個方面來描述你的應用,并通過三者的交互,使應用功能得以正常運轉。
其中,View的部分比較明確,就是負責顯示嘛。一切與顯示界面無關的東西,都不應該出現在View里面。 因此,View中一般不會出現復雜的判斷語句,不會出現復雜的運算過程。 對于PHP的Web應用而言,毫無疑問,HTML是View中的主要內容。這是關于View的幾個原則:
* 負責顯示界面,以HTML為主;
* 一般沒有復雜的判斷語句或運算過程,可以有簡單的循環語句、格式化語句。 比如,一般博客首頁的文章列表,就是一種循環結構;
* 從不調用Model的寫方法。也就是說,View只從Model獲取數據,而不直接改寫Model,所以我們說他們老死不相往來。
* 一般沒有任何準備數據的代碼,如查詢數據庫、組合成一定格式的字符串等。 這些一般放在Controller里面,并以變量的形式傳給視圖。 也就是說,視圖里面要用到的數據,都是拿來就能直接用的變量。
對于Model而言,最主要就是保存事物的信息,表征事物的行為和對他可以進行的操作。 比如,Post類必然有一個用于保存博客文章標題的title屬性,必然有一個刪除的操作,這都是Model的內容。 以下是關于Model的幾個原則:
* 數據、行為、方法是Model的主要內容;
* 實際工作中,Model是MVC中代碼量最大,邏輯最復雜的地方,因為關于應用的大量的業務邏輯也要在這里面表示。
* Model所提供的數據都是原始數據。也就是說,不帶有任何表現層的代碼。 比如,一般不會在輸出的數據中添加HTML標簽,這是View的工作。 但是Model可以提供有結構的數據,數組結構、隊列結構、乃至其他Model等。 這個結構并非是表現層的格式,而是數據在內存中的表現。
* 與輸出不同,Model的輸入數據,可以是帶有表現格式的數據。 如將一篇文章保存到Post里面,內容中必然包含各種HTML標簽。 因此,Model一般要對輸入數據作過濾、驗證和規范化等預處理。 特別是對于需要保存進數據庫的,一定要對所有的輸入數據作預處理。 這些預處理,有的其實是業務邏輯。比如只有主編才可以刪除文章,這一驗證規則既也是業務邏輯,也是權限控制。 而有些預處理,則不屬于業務邏輯,比如,文章標題前后的空格應去除。
* 注意與Controller區分開。Model是處理業務方面的邏輯,Controller只是簡單的協調Model和View之間的關系, 只要是與業務有關的,就該放在Model里面。好的設計,應當是胖Model,瘦Controller。
對于Controller,主要是響應用戶請求,決定使用什么視圖,需要準備什么數據用來顯示。 以下是有關Controller的設計原則:
* 用于處理用戶請求。 因此,對于reqeust的訪問代碼應該放在Controller里面,比如?$_GET$_POST?等。 但僅限于獲取用戶請求數據,不應該對數據有任何操作或預處理,這些工作應該交由Models來完成。
* 調用Models的讀方法,獲取數據,直接傳遞給視圖,供顯示。 當涉及到多個Model時,有關的邏輯應當交給Model來完成。
* 調用Models的類方法,對Models進行寫操作。
* 調用視圖渲染函數等,形成對用戶Reqeust的Response。
## Model設計參考[](http://www.digpage.com/mvc.html#model "Permalink to this headline")
在MVC中,Model排第一,是有一定暗示的。一是Model是整個架構中,代碼量最大,復用程度最高, 也是最體現程序員設計功力的地方。 二是View和Controller相對于Model而言,在實際開發中,復用程度不高,邏輯復雜程度較低。 可以說,Model設計得好,整個MVC就好,應用開發起就順。
因此,這一節將以Model為核心,來講MVC的設計。 實話說,MVC盡管提出了Model View Controller的劃分思想,但到了具體實操中,并不是很好把握的。 下面介紹的設計參考,也僅僅是個人在實際項目中的一些體會和想法,僅作參考。 在具體設計中,可以后把握這么幾點:
### Model應當集中整個應用的數據和業務邏輯[](http://www.digpage.com/mvc.html#id3 "Permalink to this headline")
應用當中涉及到的所有業務對象都應盡可能抽像成Model。 如,博客系統當中,文章要抽象成Post,評論要抽象成Comment。 而相關的業務邏輯,如發布新文章可以用?Post::create()?,刪除評論可以用?Comment::delete()?。 這樣子整個應用就顯得很清晰明了。
### 基礎Model應當盡可能細化[](http://www.digpage.com/mvc.html#id4 "Permalink to this headline")
在一個應用中,特別是對于大型復雜應用,Model間關系可能比較復雜。在構造應用時,特別是基礎Model時, 要從足夠小的粒度來設計。 此時,就要考慮采取繼承、封裝等措施了。 比如,一個博客文章Post,一般包含了若干標簽,在頁上一般寫在作者、日期等Post字段的旁邊。 從邏輯上來看,把標簽作為Post的一個屬性,是說得通的。 但是如果把標簽作為一個屬性像標題、正文等字段一樣依附于Post。那么在有的功能上,實現起來是有難度的。 比如,客戶要求,當一個Post含有標簽 “yii, model” 時,可以點擊 “yii” , 然后系統列出所有具標簽中含有 “yii” 的文章。
為了實現這個功能,正確的設計是單獨將標簽抽象成Tag。這樣,Post和Tag是多對多的關系, 即一個Post有多個Tag,一個Tag也對應多個Post。這個多對多關系可以通過一張數據表?tbl_post_tag?來表示。 接下來,為Post增加?Post::getTags()?方法,并通過?tbl_post_tag?表來查詢當前Post的所有標簽。 同時,為Tag增加?Tag::getPosts()?方法,也通過?tbl_post_tag?表來查詢當前Tag對應的文章。 這樣,就具備了實現客戶要求的新功能的基礎。
因此,在Model設計上,要以盡量小的粒度進行設計。一般而言,粒度越小,復用的可能性就越高。
有的讀者可能會問了,既然要求粒度盡可能地小,那么,Post是不是也應當再細化,把段落抽象為Model? 是否有這個必要,看客戶需求。一般情況確實沒有這必要,如果這么做,那是不是再以句子為單位進行抽象? 但如果客戶要求這個博客系統的評論是針對段落進行的評論的, 要將評論顯示在對應的段落旁邊,甚至顯示每個段落評論人次等功能。那么就需要把段落抽象成Model了。
### 分層次設計Model[](http://www.digpage.com/mvc.html#id5 "Permalink to this headline")
從設計流程上,數據庫結構設計與Model的設計是緊密相關的。先有數據庫結構設計,后有Model設計。 在設計數據庫結構的時候,也是在設計Model。 一般而言,最單元、粒度最小的Model就是根據每個數據庫表所生成的Model,這往往是個Active Record。
比如標簽的問題,在數據庫存儲過程中,Post和Tag是分開存的,而且這兩個表的字段,沒有冗余。tbl_post_tag?表也只記錄他們的ID,沒有實質內容。
在獲取數據渲染視圖,向用戶展現時,這兩個Model及他們的字段,是完全夠用,且沒有冗余的。
那么,能不能說 Post 和 Tag 這兩個Model是夠用的呢?顯然還不夠。
當用戶在創建文章、修改文章、審核文章時,需要采用一個表單來顯示來收集用戶輸入。 其中,對于標簽的采集,一般是一個長條的文本框,讓用戶一次性輸入多個標簽,并以?,?等進行分隔的。
但是,這個文本框沒有一個字段與之進行對應。我們也沒辦法對這個字段的用戶輸入進行任何的驗證、預處理。
因此,Post的功能是不夠用的。不夠用怎么辦?那就加吧。但直接在 Post 里面加個public?$tagString?并不好。 畢竟只是在使用表單時,才會有這個問題,其他場合,這個字段是沒用的。
這種情況下,一般使用繼承:
~~~
public class PostForm extends Post
{
public $tagString;
... ...
}
~~~
這樣,當控制器發現用戶在創建、修改、審核文章時,可以使用 PostForm Model來渲染視圖了, 而其他場合則仍使用Post。這樣就在需要時,增加了一個?tagString?的字段用于收集用戶輸入的標簽。
在具體設計過程中,由于Model本身就會包含很多代碼,因此,要多使用這繼承等手段,把代碼組織好。
### 仔細為Model方法命名[](http://www.digpage.com/mvc.html#id6 "Permalink to this headline")
由于Model的代碼量比較大,又集中了大量的邏輯,因此,會在一個Model中有大量的方法。仍然以Post為例, 會涉及到創建、審核、發布、回收等流程,相關的方法比較多,在命名上要用心。 可能會涉及到的、名字又比較接近的方法就有:
* getPrevPost(),前一篇文章,用于導航
* getNextPost(),下一篇文章,用于導航
* getRelatedPosts($n = 10),獲取相關的N篇文章,用于相關文章推薦列表
* getPostsOfAuthor($n = 10),獲取同一作者的N篇相關文章,用于作者文章列表
* getLatestPosts($n = 10),最新的N篇文章,靜態方法,用于文章列表或RSS輸出
* getHotestPosts($n = 10),最熱門的N篇文章,靜態方法,用于熱門文章列表
* getPublishPosts($n = -1),獲取已經發布的N篇文章,靜態方法,用于文章列表
* getDraftPosts($n = -1),獲取未發布的N篇文章,靜態方法,用于作者頁面
這里只是一些獲取其他Post的方法,命名比較合理,一看就知道意思。 而且全部寫成getter的形式,可以使用讀取屬性的方式進行訪問。
不單單是在Model方法的命名上要用心, 在變量名、類名、方法名等的命名上,也要養成習慣,形成規律。 不要圖一時之快,胡亂起名。否則,出來混,遲早要還的。
## MVC與前后端的配合[](http://www.digpage.com/mvc.html#id7 "Permalink to this headline")
從MVC的起源來講,是從桌面應用的開發中發展起來的。從本質來講,這是一種解決問題的思路和辦法。 從實踐來講,這是一種久經考驗的有效方式。但是如開頭我們講的,Yii更多的是側重于后端。 對于Web應用而言,包含Yii在內的許多Web開發框架,都是沒有辦法知道用戶的操作,如鼠標、鍵盤等操作的。 Web應用想要了解用戶的操作,只能依靠用戶發送Request。 而對于鼠標、鍵盤等的響應,只能依靠前端技術,如JavaScript等來實現。
再加上這幾年來Web瀏覽器的功能日臻強大。因此,Web應用開發出現了一個新的發展苗頭,就是功能從后端往前端轉移。
在前端,通過JavaScript捕獲用戶操作,進行相應處理。 或是發送回后端獲取響應后處理,如通過ajax請求后端數據,實現無刷新的局部頁面更新,向用戶進行反饋; 或直接在前端由瀏覽器進行處理,如使用backbone.js、Angular.js等前端框架的數據綁定功能等。 這些都使得Web應用表現得越來越像桌面應用。
后端MVC也在為前后端的發展而改變。 Controller的功能更多的變成了識別是ajax請求還是普通請求, 并根據請求的不同采取相應的視圖渲染方式。對于普通請求,正常渲染視圖,輸出HTML。 對于ajax請求,則返回局部渲染視圖,輸出HTML片段。有的甚至輸出XML或者JSON。 唯一在大潮流中,巍然不動的,還是我們的大Model。
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者