閒談軟體設計:MVC

閱讀時間約 20 分鐘

前言

很久前就曾想寫這個主題,但若寫的不好,恐怕又引起一番論戰,畢竟眾多 framework 實作 MVC 的方式都不一樣,從某 framework 入手認識 MVC 的開發者對 MVC 的認知可能就和從另一個 framework 了解 MVC 的開發者不同。
例如文中範例使用的 Java Swing,採用簡化版的 Model UI-Delegate 模型,UI-delegate 封裝 view (負責繪製元件) 和 controller (處理低階事件更新畫面並轉成高階事件給有興趣的 listener)。例如當使用者對一個按鈕按下滑鼠左鍵時,按鈕會轉成按下的狀態,我想使用 Java Swing 的開發者應該不會想處理這麼低階的事件,但我們仍可以呼叫 JButton 的 addActionListener 註冊高階事件 (action event 而非 mouse event) 處理按鈕被按下時要做些什麼事。
Figure 1 — Java Swing Model Delegate (圖片來源:Java Swing, 2nd Ed, O’Reilly)
會想回頭寫這個主題是因為最近面試 iOS 工程師時,在履歷中常常看到導入 MVVM,然後問為什麼要導入 MVVM 時,最常聽到的答案是這樣不會有很肥大的 view controller,但如果再問 view controller 是 MVC 的那一個部分,很多人卻回答不出個所以然,所以想聊聊這個很多種說法的 MVC pattern。

MVC 的歷史

MVC 是 Xerox Palo Alto Research Center (PARC) 在替 Smalltalk 設計 GUI 函式庫時所提出來的一個設計思維 (從論文發表於 1988 年可知,MVC 是一個歷史悠久的 pattern,後續也有多種變形),希望能滿足二個目標:(1) 建立一組系統元件支援互動式的開發流程 (例如,Visual Studio 的 Form Builder 或是 Xcode 的 Interface Builder);(2) 提供一組系統元件讓開發者能開發可攜的互動式圖形應用程式。
為了滿足上述的二個目標,勢必過去所有東西都攪和在一起的寫法是不可行的 (特別是第二點),因此將應用程式的商業邏輯及資料 (model)、使用者介面的繪製 (view)、以及如何與輸入介面互動 (controller) 拆開成三個獨立的元件,然後用相依 (dependent) 及廣播變動 (broadcasting change) 的方式組成一個互動的循環標準:
Figure 2 — MVC interaction (圖片出處:A Description of the Model-View-Controller User Interface Paradigm in the Smalltalk-80 System, Glenn E. Krasner and Stephen T. Pope, ParcPlace Systems, Inc.)
View 從 Model 取得資訊並在螢幕上進行繪製;Controller 接收到事件後,修改 Model 的狀態,有必要的話通知 View 重新繪製畫面;Model 的狀態改變後,會通知所有的相依 (View 通常是其中一個,Controller 也可能是);View 收到通知後會再次跟 Model 取得新的狀態,然後更新到畫面上;Controller 收到通知後可能改變處理事件的方式。這也就是為什麼 MVC 常和 Observer pattern 一起討論的原因了,廣播變動很適合用 Observer 來實現。

從設計一個 UI 元件開始

所以我們先從設計一個元件可以繪製像下方一樣的圓餅圖開始吧,依照剛剛所說的我們需要一個 Model。等等,Model 不是指商業邏輯嗎?我們又不是設計一個應用程式,怎麼會有 Model?有的!我們現在的 scope 就這麼小,這個元件就是一個應用程式,可以接受不同的資料輸入然後畫出圓餅圖。
Figure 3 — A simple pie chart (圖片出處:https://developer.apple.com/support/app-store/)
觀察 Java 2D 的 API 會發現,畫圓餅圖不難,只要建立對應的Arc2D物件然後呼叫 Graphis2D 填滿顏色就可以完成了,從下面的範例程式,很容易就能設計出一個 PieChartModel,提供 methods 能讓 PieChart 取得以下資訊:標題、圖中有幾個區塊、每個區塊的顏色、名稱、起始角度 (starting angle) 及延伸角度 (extent angle)。剩下的繪製邏輯像是高度、寬度和起始座標該怎麼計算,PieChart 會自己處理。
於是我們有第一個版本的 Model (Figure 4 中藍色的部分) 及 View (Figure 4 中綠色的部分) 了 。
Figure 4— PieChart View and Model (version 1)
等等,一般開發者應該不會期望他們得自己提供一個角度的實作吧?一般不是只要提供資料就好嗎?而且 Java Swing 的元件通常會提供預設的 Model 實作不是嗎?所以繼續優化第一個版本的設計,首先我們可以將資料來源與外表設定拆開成 PieChartDataModel 和 PieChartAppearanceModel,並可以透過 PieChart 的 setter 設定對應的實作。
從 Figure 5 可以看到 PieChartDataModel 引入了 NamedValue 資料結構,這其實是修正第一個版本中 PieChartModel 個別取得數值和名稱,數量可能不匹配的問題。由於 PieChartDataModel 只提供數值,缺少圓餅圖中個別區塊的起始角度和延伸角度的計算,因此多了一個類別 PieAngleCalculator,根據資料計算需要的角度 (會獨立拆出一個類別是為了測試方便,實務上計算的邏輯放入 PieChart 也是常見的做法)。此外,PieChartAppearanceModel 不但定義區塊的顏色,也定義標題和區塊副標題的字型與顏色,讓 PieChart 可以有更多外觀的設定。
Figure 5 — PieChart View and Model (version 2)
特別的是 DefaultPieChartDataModel 及 DefaultPieChartAppearanceModel,由於有這二個預設類別,PieChart 不只有需要提供實作的建構子,也有無參數的建構子 (Listing 2),這邊先賣個關子,為什麼在建構子裡是呼叫 setter 而不是直接 assign 參數給 member data?DefaultPieChartDataModel 的實作很簡單,內部有一個 NamedValue 的 List,讓開發者能新增或刪除數值。另一個外觀的實作可以很單純隨機產生一個顏色,並根據 PieChart 的大小決定字型。
現在 Model 和 View 都有了,廣播變動和相依呢?ok,剛剛有提到,這很容易用 Observer 的方式實現:新增二個 listener 介面 (符合 Java Swing 的命名慣例):PieChartDataModelListener 和 PieChartAppearanceModelListener,然後 PieChart 同時實作這二個介面,當有變動時,直接重繪 (repaint),另外在 Model 的介面上個別加上 add 和 remove 的函式管理相依。
Figure 6 — PieChart Model View with Observer
最後,誰替 Model 和 View 把相依建立起來呢?一般來說不會是 Model 主動做這件事,因為 Model 並不知道 View 的存在,所以剛剛提到的 setter 就可以派上用場了,如 Listing 6 所示,我們可以在 setter 中順便管理相依,view 將自己從舊的 model 移除然後新增到新的 model 中,除此之外,view 也更新 calculator 和 model 的關聯。
到目前為止,Controller 都還沒有出場,實際上,若沒有要處理任何事件的話,整體的設計就到此為止,沒有一定要有 Controller。但作為一個範例,我們還是替 PieChart 接上滑鼠事件,新增一個 PieChartMouseEventListener類別實作 MouseListener 和 MouseMotionListener,根據滑鼠座標判斷目前滑鼠停在哪個區塊上,或是選取哪個區塊,將低階的滑鼠事件轉成高階的事件,因此使用 PieChart 的開發者便可以實作並註冊 PieChartEventListener 以接收高階的事件。
Figure 7 — PieChart Model, View, and Controller
從 Figure 7 可以看到,大多數的邏輯應該是落在 Model (資料邏輯) 和 View (繪製邏輯) 身上,Controller 應該是若有似無 (只有一個橘色的類別),一個很薄的存在,所以如果發現有很多邏輯落在 Controller 身上,哪肯定有哪裡做錯了,檢查是否有邏輯應該是在 Model 身上卻在 Controller 身上。

應用程式的角度

現在我們有一個 PieChart 元件,也有 Java Swing 提供的 JTable 等眾多 UI 元件,該是將開發的 scope 放大了,假設我們要開發一個像 Figure 8 的應用程式,可以在第一排選取想顯示的年度,根據年度列出該年的有哪些選舉,若是全國性的選舉 (公投或是總統選舉),第二即使沒有選取,下方左側也會列出選舉結果 (以縣市作為群組),右側則是政黨比例。
若是地區選舉,第二排的第一個下拉式選單會預先選擇第一個直轄市或縣,然後下方左側一樣會列出選舉結果 (以區或縣轄市為群組),右側仍是政黨比例,若第二個或第三個下拉式選單有選擇時,下方會自動根據選取的條件進行更新。
Figure 8 — Election stats app prototype
即便是這樣單頁式的小應用程式,您會發現也有很多的邏輯在裡面,看似好像是 UI 的邏輯,其實不是,而是選舉結果如何過濾及統計的商業邏輯。若直接以目錄 (樹狀) 的資料結構出發,很容易可以想像成每個投開票所,每個候選人的得票是樹的末節點 (leaf node),然後一層一層往上群組起來,所以只要套個 composite pattern 就能輕鬆算出每一層的總得票。
但實務上,考量到資料庫的速度,通常不會撈出所有的結果後再來處理,因此,Model 層的資料結構和資料庫表格的結構可能會不一樣,表示可能會有像 Figure 9 這樣一個複雜的 Model (這只是一個示範用的簡單例子,實際可能更複雜,但這裡 Model 的完整性不是重點),ElectionStatsMainModel 是一個 Facade 介面提供多數查詢所需要的函式。
Figure 9 — Election Business Domain Model
有了 Business Domain Model 後就可以開始寫 View 和 Controller 了,真是如此?若仔細看一下 Figure 8 會發現,光是這樣的畫面也藏了許多邏輯,例如第一排選了什麼,第二排的選項和下方的內容要跟著更新,第二排的選項左邊又影響右邊,選擇後下方的內容也要更新。若比照 PieChart 把整個畫面也視為一個 UI 元件,似乎也應該要有個 Model 管理這些邏輯。
以 Java Swing 來說,我們可以提供 ComboBoxModel 給 JComboBox (下拉式選單),只要 ComboBoxModel 的內容或選取的項目有任何變動,JComboBox 都會立即反映在 UI 上,因此,這畫面的 Model 要能提供五個 ComboBoxModel 分別代表年度、選舉別、直轄市/縣、區/縣轄市和里五種不同的選項來源;接著選票數量的顯示可以 JTable 來呈現,此時會發現能提供一個 TableModel 給 JTable,和 JComboBox 一樣,JTable 能立即反應 TableModel 內的資料變動;最後,提供一個 PieChartDataModel,就能夠在 UI 上顯示圓餅圖,若想用政黨代表色,額外提供一個 PieChartAppearanceModel 即可。
Figure 10 — ElectionStatsDashboard and ElectionStatsDashboardModel
這時候會發現,ElectionStatsDashboard 真的只要負責將需要的 UI 元件建立出來,並用 layout 管理員把 UI 排好,幾乎沒有什麼邏輯在 view 身上,因為所有複雜的邏輯都委託給 ElectionStatsDashboardModel。剩下幾個小問題,誰實作 ElectionStatsDashboardModel?以及我們有兩個不同的 Model,怎麼串起來?答案其實還蠻單純的,提供一個實作 ElectionStatsDashboardModel的 adapter 就搞定了,最重要的一點是,這個 adapter 不需要用耗時的 UI 測試來驗證正確性,完全可以用單元測試框架進行所有邏輯的測試。
Figure 11 — ElectionStatsDashboardModel Adapter
不知道例子到這裡,大家是否有感受到 MVC 的好處?分離 Model 和 View 的邏輯,彼此能獨立開發,讓測試變容易,同時盡可能最大化可測試的程式至少,我當初是這樣學 MVC 的。

回到 iOS App 開發

討論 iOS App 開發前,我想可能得先回答 UIViewController 到底是 view 還是 controller?還是說它和 Java Swing 的 UI-Delegate 一樣呢?從官方文件列出的四個主要責任來看:
  • Updating the contents of the views, usually in response to changes to the underlying data.
  • Responding to user interactions with views.
  • Resizing views and managing the layout of the overall interface.
  • Coordinating with other objects — including other view controllers — in your app.
確實蠻接近 UI-Delegate:它既是 view 也是 controller (就命名來說,它已經告訴您答案了),主要的原因是 Xcode 提供的 Interface Builder 編輯後最終的結果是個 XML 檔案,是一種 passive view,只有 layout 描述,沒有其他繪製或是更新資料到 UI 上的邏輯,於是將資料反映到 UI 上的 view 邏輯就變成 view controller 最主要的工作之一。
加上 @IBAction 只能接到 view controller (file owner) 上,於是很多人就很像早期用 Visual Basic 寫程式一樣,在 Form Builder 拉一拉畫面,在按鈕上按兩下,然後就把邏輯寫在自動產生的 method 中一樣,最後所有邏輯集中在 view controller 身上。
這還不是最慘的,畫面中有使用到 UITableView 或 UICollectionView,通常對應的 view controller 會直接實作元件需要的 data source 和 delegate (這兩者通常會放在一起,因為彼此都需要對方的資料,就這一點來說,這兩個不算是良好設計的介面),於是很多人也就把 model 相關的邏輯放進來,這都導致 view controller 變肥大。
但 view controller 很肥大誰的錯?是 MVC 的錯?不是,是開發者的錯,把不該放在 view controller 的邏輯放到 view controller 身上了。MVC 從來就沒教您把 model、view 和 controller 三種不同概念的程式放在一起。
如果開發 iOS App 時,每個畫面都像是在設計PieChart一樣用心,把 model 和 view 仔細拆開,在串接每個畫面和實際的商業邏輯時,像是上述的選舉統計程式那樣,您會發現一點點 MVVM 的影子,即便從文章開始到現在,我都沒提到過 MVVM 長什麼樣子。

結論

曾有人問我,要不要在公司的專案,強制一律使用 MVVM,我當時的答案是沒必要,我個人現在寫程式的主要原則是 KISS (Keep It Simple Stupid),只堅持 model 要獨立出來,該是 model 的邏輯都要放到 model 中,model 要有測試,其他部分,要用 iOS 風格的 MVC、MVP 或是 MVVM,對我來說都只是 MVC 在不同 force 下所做的變形。
設計該留點彈性,有時候一個簡單畫面,卻用上一堆 design pattern,有點殺雞用牛刀,雖然這彈性也可能留下菜鳥工程師亂寫的後遺症,但搭配團隊有好的 code review 機制,其實這問題應該不會發生才對。
最後,程式寫的不好,在怪 MVC 之前,先想想您的 MVC 寫對了嗎?

後記

[1] MVC 的原始論文有 34 頁,對習慣看網路文章的人來說不算短,若只看概念,可以只看到 User Interface Component Hierarchy 一節就可以結束,剩下的是當初實作的一些細節討論 (其實也很有意思),這樣的話就是 12 頁的長度,蠻值得一看的,若 12 頁還嫌長,那...看這一篇文章就好了 XD
[2] 是否能把 PieChartDataModel 和 PieChartAppearanceModel合在一起,變回原本的 PieChartModel 呢?基本上沒什麼不可以,但 data 和 appearance 兩者的變動頻率差蠻多的,為了不互相影響,拆開還是比較合適的。假設有開發者考量 data model 讀取效率實作 PieChartModel,此時 PieChart 為了外觀新增一個 getter,會迫使開發者也得實作該 getter,這不是一個好現象,反之亦然。
[3] 想更深入了解 framework 如何實作 MVC 的話,我真心推薦從 Java Swing 著手,雖然文章一開始說 Java Swing 使用簡化版 MVC,但在 model 及 view 的設計卻十分的漂亮,就如同結論所述的 controller 是 model 和 view 之間的膠水,若有似無的存在,Java Swing 在這一方面也處理得很合適。
為什麼會看到廣告
52會員
102Content count
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
你可能也想看
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
我為什麼喜歡【怪醫豪斯】?很大一部分是因為這個主角應該是史上最迷人的角色之一。以現在角色來說,豪斯醫生就是個爛人,但是他的天才又讓人對他又愛又恨。
Thumbnail
一、從小觀察 從上電腦課學會用 Excel 這個文書處理軟體時開始注意到爸爸有用 Excel 記帳的習慣(簡單記錄,有時候會忘記金額所以不一定確實),每次爸爸發現我在觀察他記帳的時候一定會對我說:「固定支出越少每個月才能存到更多錢。」以及「不要小看雜支,常常花最多花得最莫名其妙的就是雜支。」 高
Thumbnail
通常,看大家聊職場話大多比較嚴肅,所以今天想聊些比較輕鬆的話題,是關於我職場上的LGBTQ族群間會出現的家常幹話
Thumbnail
看到有人因為這次的事情在說這是因為沒有校園沒有髮禁、禁止體罰、延後到校時間導致學生逐漸沒有紀律,覺得自己什麼都能做......我第一個想到的是我爸說他高中時畢業典禮教官只要走得慢一點一定會被憤怒的學生蓋布袋拖去打。發生這樣的事情當然得檢討,但希望不是以一種意氣用事、情緒主導的心態......
Thumbnail
「一群人所決定的,就是對的嗎?」 這是民主的盲點也是缺點
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
我為什麼喜歡【怪醫豪斯】?很大一部分是因為這個主角應該是史上最迷人的角色之一。以現在角色來說,豪斯醫生就是個爛人,但是他的天才又讓人對他又愛又恨。
Thumbnail
一、從小觀察 從上電腦課學會用 Excel 這個文書處理軟體時開始注意到爸爸有用 Excel 記帳的習慣(簡單記錄,有時候會忘記金額所以不一定確實),每次爸爸發現我在觀察他記帳的時候一定會對我說:「固定支出越少每個月才能存到更多錢。」以及「不要小看雜支,常常花最多花得最莫名其妙的就是雜支。」 高
Thumbnail
通常,看大家聊職場話大多比較嚴肅,所以今天想聊些比較輕鬆的話題,是關於我職場上的LGBTQ族群間會出現的家常幹話
Thumbnail
看到有人因為這次的事情在說這是因為沒有校園沒有髮禁、禁止體罰、延後到校時間導致學生逐漸沒有紀律,覺得自己什麼都能做......我第一個想到的是我爸說他高中時畢業典禮教官只要走得慢一點一定會被憤怒的學生蓋布袋拖去打。發生這樣的事情當然得檢討,但希望不是以一種意氣用事、情緒主導的心態......
Thumbnail
「一群人所決定的,就是對的嗎?」 這是民主的盲點也是缺點
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作