6 min read

MVC 是什麼?以及如何拯救 iOS MVC 架構

遊戲本身是 Model,電視螢幕是 View,而遊戲手把是 Controller(我記得手把就是 Controller 的名稱來源)。遊戲透過電視螢幕投射到我們的眼裡,而我們透過手把去操控遊戲。
MVC 是什麼?以及如何拯救 iOS MVC 架構

Photo by Danial Igdery

好啦其實大家都知道 MVC 是什麼。Model-View-Controller 模式的縮寫嘛。但到底這三個東西長什麼樣子?物件嗎?物件的種類嗎?層?還是不同的 DSL(domain specific language)?

對我來說,MVC 代表的是三種問題領域(problem domain),分別對應到三種需求:

  1. Model:使用者想要有內容。
  2. View:使用者想要看到內容。
  3. Controller:使用者想要編輯、控制內容。

也就是說,在這三個領域裡,程式碼應該要專注於處理相對應的問題。

  1. 在 Model 領域裡,處理內容的存有問題,包括新增、存檔、下載、上傳等等。
  2. 在 View 領域裡,處理內容與其它的 app 顯示問題。
  3. 在 Controller 領域裡,處理使用者的輸入問題。

這樣的模式最適合拿電視遊樂器來形象化:遊戲本身是 Model,電視螢幕是 View,而遊戲手把是 Controller(我記得手把就是 Controller 的名稱來源)。遊戲透過電視螢幕投射到我們的眼裡,而我們透過手把去操控遊戲。

可以看到,Model 其實是 MVC 裡的核心,View 跟 Controller 都只是 Model 的使用者介面而已。View 是輸出,Controller 是輸入。所以在不同的領域之間,也會存在有介面,讓 View 可以即時將 Model 的內容排版渲染出來,也讓 Controller 可以將使用者輸入轉譯成對 Model 跟 View 進行的操作。

回到一開始的問題——MVC 這三個東西長什麼樣子?我認為是只要能把程式碼好好整理成三個領域分開來,它們是不是各自的物件或者甚至程式語言,其實都可以。要把三個領域都放在同一個型別裡面也不是不行,整理好就好。

然而,滑鼠指標跟觸控螢幕出現了。

螢幕也具備輸入功能之後,漸漸取代掉實體的控制器了。這影響到 MVC 架構,就是 View 領域跟 Controller 領域變得容易混雜了。

在使用鍵盤輸入的情況下,使用者輸入事件可以直接傳送到 Controller 領域裡,轉譯後再由 Controller 去對 Model 做出更動的指令;Model 更動之後會通知 View,於是 View 領域會取得最新版的 Model 內容,並以其去更新畫面。

但用滑鼠指標按下的時候,Controller 必須要先去問 View 滑鼠是點到什麼虛擬按鈕,因為所有跟座標系相關的程式碼都歸屬於 View 領域。觸控螢幕也是一樣。Cocoa 與 Cocoa Touch 乾脆就讓 View 領域的東西也都具備接收使用者輸入事件的能力,也就是讓 View 與 Controller 的類型都繼承 NSResponder 與 UIResponder 兩個父類型。簡化使用者輸入事件的路徑,但混淆了 View 與 Controller 之間的責任分野。

原本應該是 Controller 接收到「螢幕在某點跟手指接觸」的事件後,去問 View 該點是什麼東西,然後再決定要如何反應。現在則是一開始就讓 View 去接收事件,再轉給 Controller 去做反應。

不過,這樣的混淆並不是很嚴重,只要確保 View 領域的程式碼只做「使用者輸入轉介」的事就好。簡單來說,就是告訴 Controller「使用者按了哪個虛擬按鍵」這樣的事。至於按下這樣的鍵會發生什麼樣的事,仍然是 Controller 領域要處理的問題。

還有萬惡的 Cocoa (假)MVC 架構⋯⋯

Cocoa 的 MVC 文件直接把原本的三角多邊架構換成中介者架構(mediator pattern),使 Controller 的意義從控制器變成「Model 與 View 的控制者」。這完全跟 MVC 的意義不一樣了。

這會有什麼問題?

  1. View Controller(Cocoa MVC 文件中的 mediator)職能橫跨 MVC 三領域。
  2. Model 與 View 職能縮減,越變越小。

結果就是,MVC 的目的——將不同的問題領域分開解決——完全失效。無怪乎 iOS 開發社群那麼多不同的架構跑出來,為的就是要解決 Cocoa MVC 再度創造出來的問題。

Cocoa MVC 是有救的,

因為真正的問題是在文件,不是在框架本身。框架本身並沒有那麼明顯的 mediator 痕跡,或至少我們可以選擇不去使用像是中介者架構的東西。

有幾樣事情可以幫助我們從假 MVC 架構回到真 MVC 架構:

  1. 建立一個負責管理 Model state 的 Model Manager,不管那是 UIDocument 還是 NetworkLayer 之類的。這個 Model Manager 要能自己將 state 對硬碟跟網路等地讀取跟儲存,不依靠 Controller。像 UIDocument 就可以自己追蹤 state 變動(透過它的 undoManager)與訂閱 UIApplication 事件來找時機儲存內容。
  2. 讓 View 自己去訂閱 Model Manager 的資料更新,並自己顯示出來。重點是自己,所以不能依賴 View Controller。
  3. 把 View Controller 想成是一個手把或遙控器等控制器,所以它最主要的功能就是當 View 的 action target 與 delegate。Controller 可以有自己的 Controller state,但那跟 Model state 完全不一樣,是按鈕能不能按、標題是什麼之類的。Controller 不應該持有 Model state,而是要跟 1. 的 Model Manager 溝通去編輯跟取值。
  4. Controller 透過 Model Manager 更新 Model state 之後,不要再由 Controller 去更新 View 所顯示的 Model 值,讓 View 自己去更新。Controller 可以更新 View state,比如說 View 的顏色或文字大小之類的,但也就這樣而已。
  5. View 不能直接編輯 Model state,一定要轉介給 Controller,再由 Controller 去跟 Model Manager 說要編輯 Model state。
  6. Model 是核心,不被 View 或 Controller 所擁有,只是被參照。也就是說,就算 View 與 Controller 消滅,Model 應該還是要存在(在 SceneDelegate 裡或以單例的形式)。

跟其它 iOS 的 MVC best practices 很不一樣對吧?View 要跟 Model 直接溝通、Model 要自己處理資料保存跟網路層什麼的⋯⋯但這些從問題領域來看,卻是再理所當然不過的。Controller 負責的是使用者輸入,當然跟網路層一點關係都沒有,也跟顯示出來一點關係都沒有。它只要知道使用者按下的哪個鍵會造成 Model state 什麼樣的變動就好。

用這樣的 MVC 架構來寫 code,才有可能達到 separation of concerns,讓程式碼更平均的分散在不同的類型裡面,更不雜亂。