design patterns - 從頭講解MVC模式

RecoReco發表於2018-01-10

和一般文章不同,本文不依賴於任何現有的框架,也不試圖陷入冗長的發展歷史,而是完全從頭開始,以一個儘可能小但是可以說明問題的案例,以此講清楚MVC這個歷史悠久、變型極多的技術理念。MVC是一種非常普及的,基礎的設計套路,在不同的語言社群內都有著大量的應用。理解了MVC,學習接下來的MVVM、MVP等才能成為可能。

MVC把一個系統的類分為三種:Model,View和Controller。它們也遵循著職責分離原則:

  1. 由Controller來處理訊息
  2. 由Model掌管資料來源
  3. 由View負責資料顯示

儘管MVC看起來複雜,其實用程式碼表達最簡單的MVC是可能的:

/** 模擬 Model, View, Controller */
var M = {}, V = {}, C = {};
/** Model 負責資料 */
M.data = "hello world";
/** View 負責輸出 */
V.render = (M) => { alert(M.data); }
/** Controller 搭橋*/
C.handleOnload = () => { V.render(M); }
window.onload = C.handleOnload;
複製程式碼

只要是和使用者互動的都是View,所以使用alert,或者直接輸出到console,都是一種View。

接下來,我希望用一個完整但是極簡的程式,來驗證MVC如何從一個日常的程式進化而來的。這個一個小型程式只不過是如此:

1

點選按鈕會讓span加1或者減1。它簡單到你不需要分心關注,但是由足夠說明典型的html場景——就是既有資料呈現也有按鈕操作。
<div id="app">
<p><span id="count">0</span>
    <button id="inc">+</button>
    <button id="dec">-</button>
  </p>
</div>
<script>
    var counter = document.getElementById('count');
    var btn1 = document.getElementById('inc');
    var btn2 = document.getElementById('dec');
    var count = 0;
    btn1.addEventListener('click',function (){
                counter.innerHTML = ++count;
            }
    )
    btn2.addEventListener('click',function (){
                counter.innerHTML = --count;
            }
    )
</script>
複製程式碼

當前的小型程式,所有的程式碼,無論資料還是邏輯還是UI程式碼,都是混合在一起的,並沒有所謂的任何的職責分離。因為還小,問題不大。但是產品程式碼都是從這一的基礎上逐步長大的。比如說資料就不太可能只有一個count,程式碼逐步增大,一個物件的資料屬性會越來越多,隨著來的是運算元據的函式也會越來越多。同理包括使用者介面和業務邏輯。

如果使用MVC的眼光來看,在此微觀模式下,其實可以使用MVC的模式做程式碼的職責分離。其中所有的UI元素物件,都應該放置到View型別內,其中的事件處理都是應該放到Controller內,而資料,也就是這裡的count變數和對它的操作(減一加一),應該放置到Model類內,組裝Model和View則是Controller的職責。這樣的思路下,程式碼可以改成:

<div id="app">
<p><span id="count">1</span>
    <button id="inc">+</button>
    <button id="dec">-</button>
  </p>
</div>
<script>
    class Model{
      constructor(){
        this.count = 1
      }
      inc(){
        return this.count++
      }
      dec(){
        return this.count--
      }
    }
    class View{
      constructor(){
        this.counter = document.getElementById('count')
        this.btn1 = document.getElementById('inc')
        this.btn2 = document.getElementById('dec')
      }
      setCount(c){
       this.counter.innerHTML = c 
      }
      attachInc(cb){
        this.btn1.addEventListener('click',cb)
      }
      attachDec(cb){
        this.btn2.addEventListener('click',cb)
      }
    }
    class Controller {
      constructor(){
        this.m = new Model()
        this.v = new View()
        this.v.attachInc(this.onInc.bind(this))
        this.v.attachDec(this.onDec.bind(this))
      }
      onInc(){
        this.v.setCount(this.m.inc())
      }
      onDec(){
        this.v.setCount(this.m.dec())
      }
    }
    var c = new Controller()
</script>
複製程式碼

將應用程式劃分為三種元件,模型 - 檢視 - 控制器(MVC)設計定義它們之間的相互作用。

Model用於封裝資料以及對資料的處理方法。Model不依賴“View”和“Controller”,也就是說, Model 不關心它會被如何顯示或是如何被操作。Model 中資料的變化一般會通過一種重新整理機制被公佈。為此,Model需要提供被監聽的機制。

View能夠實現顯示。在 View 中一般沒有程式上的邏輯。為了實現Model變化後的響應View 上的重新整理功能,View需要監聽Model的變化。

控制器(Controller)起到不同層面間的組織作用,用於控制應用程式的流程。它處理事件並作出響應。“事件”包括使用者的行為和資料 Model 上的改變。

實際上,通過觀察者模式,可以把Model的修改重新整理到多個檢視中,只要檢視做一個Model變化的訂閱即可。可以先做一個簡單的觀察者程式碼:

var Event = function (sender) {
    this._sender = sender;
    this._listeners = [];
}
Event.prototype = {
    attach: function (listener) {
        this._listeners.push(listener);
    },
    notify: function (args) {
        for (var i = 0; i < this._listeners.length; i += 1) {
            this._listeners[i](this._sender, args);
        }
    }
};
複製程式碼

然後把應用稍作修改,在加上一個span,其中為第一個span的值乘以2。介面看起來是這樣:

1|2

這就意味著,一個count的模型值,先做有兩個span需要消費它,因此無論何種原因導致count的修改,兩個span都應該同步的被修改。此時我們可以利用Event物件,讓View訂閱count修改的時間,當count修改時,就會通知檢視,做相應的修改。完整的程式碼如下:
<div id="app">
<p><span id="count">1</span>|<span id="count2">2</span>
    <button id="inc">+</button>
    <button id="dec">-</button>
  </p>
</div>
<script>
    class Model{
      constructor(e){
        this.e = e
        this.count = 1
      }
      inc(){
        this.count++
        this.e.notify(this.count)
        return this.count
      }
      dec(){
        this.count--
        this.e.notify(this.count)
        return this.count
      }
    }
    class View{
      constructor(e){
        this.e = e
        this.e.attach(this.f.bind(this))
        this.counter = document.getElementById('count')
        this.counter2 = document.getElementById('count2')
        this.btn1 = document.getElementById('inc')
        this.btn2 = document.getElementById('dec')
      }
      f(sender,c){
        this.counter2.innerHTML = c * 2
      }
      setCount(c){
       this.counter.innerHTML = c 
      }
      attachInc(cb){
        this.btn1.addEventListener('click',cb)
      }
      attachDec(cb){
        this.btn2.addEventListener('click',cb)
      }
    }
    var Event = function (sender) {
        this._sender = sender;
        this._listeners = [];
    }
    Event.prototype = {
        attach: function (listener) {
            this._listeners.push(listener);
        },
        notify: function (args) {
            for (var i = 0; i < this._listeners.length; i += 1) {
                this._listeners[i](this._sender, args);
            }
        }
    };
    class Controller {
      constructor(){
        this.e = new Event()
        this.m = new Model(this.e)
        this.v = new View(this.e)
        this.v.attachInc(this.onInc.bind(this))
        this.v.attachDec(this.onDec.bind(this))
      }
      onInc(){
        this.v.setCount(this.m.inc())
      }
      onDec(){
        this.v.setCount(this.m.dec())
      }
    }
    var c = new Controller()
</script>
複製程式碼

此應用的最後一個實現,看起來更加具備了一個MVC的多個方面:

  1. 類職責分類為模型檢視控制器
  2. 有了事件的釋出和訂閱的機制,可以更好的釋出一個模型的變化到多個檢視去
  3. Model並不依賴於View,而是通過事件釋出訂閱的方式通知檢視變化

相關文章