被濫用的 GUI 設計模式
原文來自ofollow,noindex">被濫用的 GUI 設計模式 。
隨便侃些個人對 GUI 設計模式的看法。
近些年來,隨著 Fronted 技術的火熱和推進,古老的(至少有幾十年歷史)用來解決 GUI 應用中程式碼組織問題的「GUI 設計模式」現在也成為了 Frontend 工程師的熱門話題,MVC、MVP、MVVN 等設計模式在網路上被議論不絕。有很多工程師開始通過寫博文來介紹它們、闡述自己對它們的理解,甚至在 Github 上開源了各種 GUI 設計模式的實現。
順著這種趨勢,很多 Frontend 工程師甚至把 GUI 設計模式當成一種「規範」乃至「教條」。然而糟糕的現實是,大多數人並沒有正確地、細緻地理解和運用 GUI 設計模式,反而因為 Tradeoff 導致它的缺點被放大。結果就是你用了大量精力、模板程式碼去設計它,反而讓它更復雜、更難維護了。
例如,當你開啟 Github 上大多數試圖實現 GUI 模式的倉庫時會發現,整個應用大概也就兩三個頁面、四五個網路介面,就可能已經建立了幾十個類和介面來承載那單薄的邏輯了。舉個更具體的例子,我個人曾經接觸過幾個用 MVP 模式設計的大型 Android 工程,在進行維護或者迭代的時候,各種帶有問題的設計反而讓 MVP 模式成為了累贅。
首先,工程中大多數 View 都是粒度大耦合度高的 Activity 類,而且很多 View 裡為了方便,會提供fun updateView(user: UserModel)
這樣的方法,導致 View 和領域/業務模型直接耦合了。再者,View 和 Model 中還會包含了跳轉頁面、傳送全域性訊息等各種帶有「副作用」的命令,這也讓面向介面程式設計成為了形式主義。
所以與其「捨本逐末」、「知其然而不知其所以然」,倒還不如理解問題的本質。於 GUI 設計模式而言,實際上最重要的思想是「分而治之」,通過把之前都寫在一處的程式碼按照職能分到不同的類,來讓它們實現「低耦合高內聚」。所以,我們更應該把 GUI 設計模式當成一思想而不是具體的手段,更也沒必要用各種所謂的模板來解決問題,只要你能把熱點、關鍵程式碼設計得足夠低耦合高內聚,那麼你完全可以無視所有 GUI 設計模式。
例如上面提到的fun updateView(user: UserModel)
問題,實際有兩種方式來讓 View 和業務模型 UserModel 解耦:
// 方法一 interface ViewA { fun updateText1(text: String) fun updateText2(text: String) // ... } class PresenterA { fun onSomeEvent() { val userModel = Apis.requestUser() viewA.updateText1(userModel.name) viewA.updateText2(userModel.age.toString()) } } // 方法二 interface ViewA { data class ViewAttributes( text1: String, text2: String, // ... ) fun updateView(view: ViewAttributes) } class PresenterA { fun onSomeEvent() { val viewAttributes = Apis.requestUser().mapTo ViewAttributes() viewA.updateView(viewAttributes) } } 複製程式碼
方法一更傾向於用「指令」來描述 View,方法二則更傾向於用「資料」。而我個人更喜歡方法二,因為資料是執行時可處理、可持久化的,甚至可以跨程序、跨語言、乃至跨機器共享的。講個題外話,Web Fronted 裡 Redux 等狀態管理工具捧起了一個很火的詞「時間旅行」。在我看來核心思想其實也是把指令下沉,用資料(/狀態)來描述上層邏輯,這樣就可以在執行時實現邏輯可記錄、可回放。
這裡還有一點需要注意的,ViewAttributes
必須是 View 的領域模型,欄位名稱應當僅和 View 本身相關,而不應該和其他領域有關係。
再回到之前提到過的另外一個問題:View 和 Model 裡的副作用。這個其實更容易解決,只需要把所有副作用移到外部(/呼叫方)就好了。例如:
// 有副作用 class ViewA { fun onTitleClick() { sendBroadcast("x") } } // 無副作用 class ViewA { fun onTitleClick() { caller.onTitleClick() } } class PresenterA { fun onTitleClick() { sendBroadcast("x") } } 複製程式碼
實際上,只懂得 OOP(面向物件程式設計)的工程師很容易造成前面提到的問題,因為他們習慣了依賴「外部狀態」來解決問題(類的例項本身也是一個狀態),但是在狀態數量不斷增加的情況下,狀態的管理反而會成為一個新的大難題。而 OOP 提倡類的「低耦合高內聚」實際上可以看成是在解決狀態管理的問題。
所以在文章的最後,我強烈推薦工程師們可以學習下 FP(函數語言程式設計)。相對於 OOP 而言,FP 的思想則是摒棄外部狀態,它實現的是粒度更小的函式級別的「低耦合高內聚」,你只需要保證你的函式是無副作用的然後管理好函式內部的狀態就可以了。而維持這種程式設計思想,能讓你輕鬆駕馭巨型、複雜的專案,甚至能讓你的程式碼更容易被除錯,更容易被並行執行。
對於 Android 工程師們來說,Kotlin 目前的火熱正是讓大家有了更瞭解 FP 的機會。之後我也會寫些和 Kotlin、FP 有關的文章。