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

Photo by Danial Igdery
好啦其實大家都知道 MVC 是什麼。Model-View-Controller 模式的縮寫嘛。但到底這三個東西長什麼樣子?物件嗎?物件的種類嗎?層?還是不同的 DSL(domain specific language)?
對我來說,MVC 代表的是三種問題領域(problem domain),分別對應到三種需求:
- Model:使用者想要有內容。
- View:使用者想要看到內容。
- Controller:使用者想要編輯、控制內容。
也就是說,在這三個領域裡,程式碼應該要專注於處理相對應的問題。
- 在 Model 領域裡,處理內容的存有問題,包括新增、存檔、下載、上傳等等。
- 在 View 領域裡,處理內容與其它的 app 顯示問題。
- 在 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 的意義不一樣了。
這會有什麼問題?
- View Controller(Cocoa MVC 文件中的 mediator)職能橫跨 MVC 三領域。
- Model 與 View 職能縮減,越變越小。
結果就是,MVC 的目的——將不同的問題領域分開解決——完全失效。無怪乎 iOS 開發社群那麼多不同的架構跑出來,為的就是要解決 Cocoa MVC 再度創造出來的問題。
Cocoa MVC 是有救的,
因為真正的問題是在文件,不是在框架本身。框架本身並沒有那麼明顯的 mediator 痕跡,或至少我們可以選擇不去使用像是中介者架構的東西。
有幾樣事情可以幫助我們從假 MVC 架構回到真 MVC 架構:
- 建立一個負責管理 Model state 的 Model Manager,不管那是 UIDocument 還是 NetworkLayer 之類的。這個 Model Manager 要能自己將 state 對硬碟跟網路等地讀取跟儲存,不依靠 Controller。像 UIDocument 就可以自己追蹤 state 變動(透過它的
undoManager
)與訂閱 UIApplication 事件來找時機儲存內容。 - 讓 View 自己去訂閱 Model Manager 的資料更新,並自己顯示出來。重點是自己,所以不能依賴 View Controller。
- 把 View Controller 想成是一個手把或遙控器等控制器,所以它最主要的功能就是當 View 的 action target 與 delegate。Controller 可以有自己的 Controller state,但那跟 Model state 完全不一樣,是按鈕能不能按、標題是什麼之類的。Controller 不應該持有 Model state,而是要跟 1. 的 Model Manager 溝通去編輯跟取值。
- Controller 透過 Model Manager 更新 Model state 之後,不要再由 Controller 去更新 View 所顯示的 Model 值,讓 View 自己去更新。Controller 可以更新 View state,比如說 View 的顏色或文字大小之類的,但也就這樣而已。
- View 不能直接編輯 Model state,一定要轉介給 Controller,再由 Controller 去跟 Model Manager 說要編輯 Model state。
- Model 是核心,不被 View 或 Controller 所擁有,只是被參照。也就是說,就算 View 與 Controller 消滅,Model 應該還是要存在(在 SceneDelegate 裡或以單例的形式)。
跟其它 iOS 的 MVC best practices 很不一樣對吧?View 要跟 Model 直接溝通、Model 要自己處理資料保存跟網路層什麼的⋯⋯但這些從問題領域來看,卻是再理所當然不過的。Controller 負責的是使用者輸入,當然跟網路層一點關係都沒有,也跟顯示出來一點關係都沒有。它只要知道使用者按下的哪個鍵會造成 Model state 什麼樣的變動就好。
用這樣的 MVC 架構來寫 code,才有可能達到 separation of concerns,讓程式碼更平均的分散在不同的類型裡面,更不雜亂。