前端架構最全總結——GUI 應用程式架構的十年變遷:MVC、MVP、MVVM、Unidirectional、Clea

2021-02-13 開發者全社區


熱文導讀 | 點擊標題閱讀

歡迎加入Java和Android架構社群,領取學習視頻

吊炸天!74款APP完整源碼!

再論Android最新架構—Google 官方Android開發新架構指南

隨著現代瀏覽器的日漸流行,Web 以及混合開發技術的發展,大前端的概念日漸成為某種共識;而無論 iOS、Android、Web 這樣的端開發還是 React Native、Weex 這樣的跨端開發,其術不同而道相似。筆者在日前總結的泛前端知識圖譜(Web/iOS/Android/RN) 一文中就對泛前端開發學習中可能會涉及到的知識點進行了總結與盤點,近日筆者打算為該知識圖譜編寫附帶的簡略說明,因此先對舊文 GUI 應用程式架構的十年變遷:MVC、MVP、MVVM、Unidirectional、Clean 進行了重製與補充,從屬於筆者的大前端開發技術相關倉庫,同樣向 Martin Fowler 及其撰寫的 GUI Architectures 致敬。其他關聯文章建議閱讀 2016-我的前端之路:工具化與工程化 By 王下邀月熊、2015-我的前端之路:數據流驅動的界面 By 王下邀月熊。另外,本文所有參考引用的文章統一聲明在這裡

前言

Make everything as simple as possible, but not simpler — Albert Einstein

十年前,Martin Fowler 撰寫了 GUI Architectures 一文,至今被奉為經典。本文所談的所謂架構二字,核心即是對於對於富客戶端的代碼組織/職責劃分。縱覽這十年內的架構模式變遷,大概可以分為 MV* 與 Unidirectional 兩大類,而 Clean Architecture 則是以嚴格的層次劃分獨闢蹊徑。從筆者的認知來看,從 MVC 到 MVP 的變遷完成了對於 View 與 Model 的解耦合,改進了職責分配與可測試性。而從 MVP 到 MVVM,添加了 View 與 ViewModel 之間的數據綁定,使得 View 完全的無狀態化。最後,整個從 MV* 到 Unidirectional 的變遷即是採用了消息隊列式的數據流驅動的架構,並且以 Redux 為代表的方案將原本 MV* 中碎片化的狀態管理變為了統一的狀態管理,保證了狀態的有序性與可回溯性。

筆者在撰寫本文的時候也不可避免的帶了很多自己的觀點,在漫長的 GUI 架構模式變遷過程中,很多概念其實是交錯複雜,典型的譬如 MVP 與 MVVM 的區別,筆者按照自己的理解強行定義了二者的區分邊界,不可避免的帶著自己的主觀想法。另外,鑑於筆者目前主要進行的是Web 方面的開發,因此在整體傾向上是支持 Unidirectional Architecture 並且認為集中式的狀態管理是正確的方向(註:MobX 也是極好的)。但是必須要強調,GUI 架構本身是無法脫離其所依託的平臺,下文筆者也會淺述由於 Android 與 iOS 本身 SDK API 的特殊性,生搬硬套其他平臺的架構模式也是邯鄲學步,沐猴而冠。不過總結而言,它山之石,可以攻玉,本身我們所處的開發環境一直在不斷變化,對於過去的精華自當應該保留,並且與新的環境相互印證,觸類旁通。

GUI 應用程式架構

Graphical User Interfaces一直是軟體開發領域的重要組成部分,從當年的MFC,到WinForm/Java Swing,再到WebAPP/Android/iOS引領的智能設備潮流,以及未來可能的AR/VR,GUI應用開發中所面臨的問題一直在不斷演變,但是從各種具體問題中抽象而出的可以復用的模式恆久存在。而這些模式也就是所謂應用架構的核心與基礎。對於所謂應用架構,空談誤事,不談誤己,筆者相信不僅僅只有自己想把那一團糟的代碼給徹底拋棄。往往對於架構的認知需要一定的大局觀與格局眼光,每個有一定經驗的客戶端程序開發者,無論是Web、iOS還是Android,都會有自己熟悉的開發流程習慣,但是筆者認為架構認知更多的是道,而非術。當你能夠以一種指導思想在不同的平臺上能夠進行高效地開發時,你才能真正理解架構。這個有點像張三丰學武,心中無招,方才達成。筆者這麼說只是為了強調,儘量地可以不拘泥於某個平臺的具體實現去審視GUI應用程式架構模式,會讓你有不一樣的體驗。譬如下面這個組裝Android機器人的圖:

怎麼去焊接兩個組件,屬於具體的術實現,而應該焊接哪兩個組件就是術,作為合格的架構師總不能把腳和頭直接焊接在一起,而忽略中間的連接模塊。對於軟體開發中任何一個方面,我們都希望能夠尋找到一個抽象程度適中,能夠在接下來的4,5年內正常運行與方便維護擴展的開發模式。引申下筆者在我的編程之路中的論述,目前在GUI架構模式中,無論是Android、iOS還是Web,都在經歷著從命令式編程到聲明式/響應式編程,從Passive Components到Reactive Components,從以元素操作為核心到以數據流驅動為核心的變遷(關於這幾句話的解釋可以參閱下文的Declarative vs. Imperative這一小節)。

基礎概念

正文之前,我們先對一些概念進行闡述:

User Events/用戶事件:即是來自於可輸入設備上的用戶操作產生的數據,譬如滑鼠點擊、滾動、鍵盤輸入、觸摸等等。

User Interface Rendering/用戶界面渲染:View這個名詞在前後端開發中都被廣泛使用,為了明晰該詞的含義,我們在這裡使用用戶渲染這個概念,來描述View,即是以HTML或者JSX或者XAML等等方式在屏幕上產生的圖形化輸出內容。

UI Application:允許接收用戶輸入,並且將輸出渲染到屏幕上的應用程式,該程序能夠長期運行而不只是渲染一次即結束

Passive Module & Reactive Module

箭頭表示的歸屬權實際上也是Passive Programming與Reactive Programming的區別,譬如我們的系統中有Foo與Bar兩個模塊,可以把它們當做OOP中的兩個類。如果我們在Foo與Bar之間建立一個箭頭,也就意味著Foo能夠影響Bar中的狀態:

譬如Foo在進行一次網絡請求之後將Bar內部的計數器加一操作:

在這裡將這種邏輯關係可以描述為Foo擁有著網絡請求完成之後將Bar內的計數器加一這個關係的控制權,也就是Foo佔有主導性,而Bar相對而言是Passive被動的:

Bar是Passive的,它允許其他模塊改變其內部狀態。而Foo是主動地,它需要保證能夠正確地更新Bar的內部狀態,Passive模塊並不知道誰會更新到它。而另一種方案就是類似於控制反轉,由Bar完成對於自己內部狀態的更新:

在這種模式下,Bar監聽來自於Foo中的事件,並且在某些事件發生之後進行內部狀態更新:

此時Bar就變成了Reactive Module,它負責自己的內部的狀態更新以響應外部的事件,而Foo並不知道它發出的事件會被誰監聽。

Declarative & Imperative

形象地來描述命令式編程與聲明式編程的區別,就好像C#/JavaScript與類似於XML或者HTML這樣的標記語言之間的區別。命令式編程關注於how to do what you want to do,即事必躬親,需要安排好每個要做的細節。而聲明式編程關注於what you want to do without worrying about how,即只需要聲明要做的事情而不用將具體的過程再耦合進來。對於開發者而言,聲明式編程將很多底層的實現細節向開發者隱藏,而使得開發者可以專注於具體的業務邏輯,同時也保證了代碼的解耦與單一職責。譬如在Web開發中,如果你要基於jQuery將數據填充到頁面上,那麼大概按照命令式編程的模式你需要這麼做:

而在iOS和Android開發中,近年來函數響應式編程(Functional Reactive Programming)也非常流行,參閱筆者關於響應式編程的介紹可以了解,響應式編程本身是基於流的方式對於異步操作的一種編程優化,其在整個應用架構的角度看更多的是細節點的優化。以RxSwift為例,通過響應式編程可以編寫出非常優雅的用戶交互代碼:

其直觀的效果大概如下圖所示:

到這裡可以看出,無論是從命令式編程與聲明式編程的對比還是響應式編程的使用,我們開發時的關注點都慢慢轉向了所謂的數據流。便如MVVM,雖然它還是雙向數據流,但是其使用的Data-Binding也意味著開發人員不需要再去以命令地方式尋找元素,而更多地關注於應該給綁定的對象賦予何值,這也是數據流驅動的一個重要體現。而Unidirectional Architecture採用了類似於Event Source的方式,更是徹底地將組件之間、組件與功能模塊之間的關聯交於數據流操控。

何謂架構

當我們談論所謂客戶端開發的時候,我們首先會想到怎麼保證向後兼容、怎麼使用本地存儲、怎麼調用遠程接口、如何有效地利用內存/帶寬/CPU等資源,不過最核心的還是怎麼繪製界面並且與用戶進行交互。而當我們提綱挈領、高屋建瓴地以一個較高的抽象的視角來審視總結這個知識點的時候會發現,我們希望的好的架構,便如在引言中所說,即是有好的代碼組織方式/合理的職責劃分粒度。筆者腦中會出現如下這樣的一個層次結構,可以看出,最核心的即為View與ViewLogic這兩部分:

實際上,對於富客戶端的代碼組織/職責劃分,從具體的代碼分割的角度,即是功能的模塊化界面的組件化狀態管理這三個方面。最終呈獻給用戶的界面,筆者認為可以抽象為如下等式:View=f(State,Template)。而 ViewLogic 中對於類/模塊之間的依賴關係,即屬於代碼組織,譬如 MVC 中的 View 與 Controller 之間的從屬關係。而對於動態數據,即所謂應用數據的管理,屬於狀態管理這一部分,譬如APP從後來獲取了一系列的數據,如何將這些數據渲染到用戶界面上使得用戶可見,這樣的不同部分之間的協同關係、整個數據流的流動,即屬於狀態管理。

不斷衍化的架構

兵無常勢,水無常形。實際上從MVC、MVP到MVVM,一直圍繞的核心問題就是如何分割ViewLogic與View,即如何將負責界面展示的代碼與負責業務邏輯的代碼進行分割。所謂分久必合,合久必分,從筆者自我審視的角度,發現很有趣的一點。Android與iOS中都是從早期的用代碼進行組件添加與布局到專門的XML/Nib/StoryBoard文件進行布局,Android中的Annotation/DataBinding、iOS中的IBOutlet更加地保證了View與ViewLogic的分割(這一點也是從元素操作到以數據流驅動的變遷,我們不需要再去編寫大量的 findViewById。而Web的趨勢正好有點相反,無論是WebComponent還是ReactiveComponent都是將ViewLogic與View置於一起,特別是JSX的語法將JavaScript與HTML混搭,很像當年的PHP/JSP與HTML混搭。這一點也是由筆者在上文提及的Android/iOS本身封裝程度較高的、規範的API決定的。對於Android/iOS與Web之間開發體驗的差異,筆者感覺很類似於靜態類型語言與動態類型語言之間的差異。(註:使用 TypeScript 與 Flow 同樣能為 Web 開發引入靜態類型語言的優勢)

功能的模塊化

老實說在AMD/CMD規範之前,或者說在ES6的模塊引入與Webpack的模塊打包出來之前,功能的模塊化依賴一直也是個很頭疼的問題。

SOLID中的接口隔離原則,大量的IOC或者DI工具可以幫我們完成這一點,就好像Spring中的@Autowire或者Angular 1中的@Injection,都給筆者很好地代碼體驗。

在這裡筆者首先要強調下,從代碼組織的角度來看,項目的構建工具與依賴管理工具會深刻地影響到代碼組織,這一點在功能的模塊化中尤其顯著。譬如筆者對於Android/Java構建工具的使用變遷經歷了從Eclipse到Maven再到Gradle,筆者會將不同功能邏輯的代碼封裝到不同的相對獨立的子項目中,這樣就保證了子項目與主項目之間的一定隔離,方便了測試與代碼維護。同樣的,在Web開發中從AMD/CMD規範到標準的ES6模塊與Webpack編譯打包,也使得代碼能夠按照功能儘可能地解耦分割與避免冗餘編碼。而另一方面,依賴管理工具也極大地方便我們使用第三方的代碼與發布自定義的依賴項,譬如Web中的NPM與Bower,iOS中的CocoaPods都是十分優秀的依賴發布與管理工具,使我們不需要去關心第三方依賴的具體實現細節即能夠透明地引入使用。因此選擇合適的項目構建工具與依賴管理工具也是好的GUI架構模式的重要因素之一。不過從應用程式架構的角度看,無論我們使用怎樣的構建工具,都可以實現或者遵循某種架構模式,筆者認為二者之間也並沒有必然的因果關係。

界面的組件化與無狀態組件

A component is a small piece of the user interface of our application, a view, that can be composed with other components to make more advanced components.

何謂組件?一個組件即是應用中用戶交互界面的部分組成,組件可以通過組合封裝成更高級的組件。組件可以被放入層次化的結構中,即可以是其他組件的父組件也可以是其他組件的子組件。根據上述的組件定義,筆者認為像Activity或者UIViewController都不能算是組件,而像ListView或者UITableView可以看做典型的組件。

我們強調的是界面組件的Composable&Reusable,即可組合性與可重用性。當我們一開始接觸到Android或者iOS時,因為本身SDK的完善度與規範度較高,我們能夠很多使用封裝程度較高的組件。譬如ListView,無論是Android中的RecycleView還是iOS中的UITableView或者UICollectionView,都為我們提供了。凡事都有雙面性,這種較高程度的封裝與規範統一的API方便了我們的開發,但是也限制了我們自定義的能力。同樣的,因為SDK的限制,真正意義上可復用/組合的組件也是不多,譬如你不能將兩個ListView再組合成一個新的ListView。在React中有所謂的controller-view的概念,即意味著某個React組件同時擔負起MVC中Controller與View的責任,也就是JSX這種將負責ViewLogic的JavaScript代碼與負責模板的HTML混編的方式。

界面的組件化還包括一個重要的點就是路由,譬如Android中的AndRouter、iOS中的JLRoutes都是集中式路由的解決方案,不過集中式路由在Android或者iOS中並沒有大規模推廣。iOS中的StoryBoard倒是類似於一種集中式路由的方案,不過更偏向於以UI設計為核心。筆者認為這一點可能是因為Android或者iOS本身所有的代碼都是存放於客戶端本身,而Web中較傳統的多頁應用方式還需要用戶跳轉頁面重新加載,而後在單頁流行之後即不存在頁面級別的跳轉,因此在Web單頁應用中集中式路由較為流行而Android、iOS中反而不流行。

無狀態的組件的構建函數是純函數(pure function)並且引用透明的(refferentially transparent),在相同輸入的情況下一定會產生相同的組件輸出,即符合View=f(State,Template)公式。筆者覺得Android中的ListView/RecycleView,或者iOS中的UITableView,也是無狀態組件的典型。譬如在Android中,可以通過動態設置Adapter實例來為RecycleView進行源數據的設置,而作為View層以IoC的方式與具體的數據邏輯解耦。

組件的可組合性與可重用性往往最大的阻礙就是狀態,一般來說,我們希望能夠重用或者組合的組件都是Generalization,而狀態往往是Specification,即領域特定的。同時,狀態也會使得代碼的可讀性與可測試性降低,在有狀態的組件中,我們並不能通過簡單地閱讀代碼就知道其功能。如果借用函數式編程的概念,就是因為副作用的引入使得函數每次回產生不同的結果。函數式編程中存在著所謂Pure Function,即純函數的概念,函數的返回值永遠只受到輸入參數的影響。譬如(x)⇒x⋅2這個函數,輸入的x值永遠不會被改變,並且返回值只是依賴於輸入的參數。而Web開發中我們也經常會處於帶有狀態與副作用的環境,典型的就是Browser中的DOM,之前在jQuery時代我們會經常將一些數據信息緩存在DOM樹上,也是典型的將狀態與模板混合的用法。這就導致了我們並不能控制到底應該何時去進行重新渲染以及哪些狀態變更的操作才是必須的,

狀態管理

可變的與不可預測的狀態是軟體開發中的萬惡之源

上文提及,我們儘可能地希望組件的無狀態性,那麼整個應用中的狀態管理應該儘量地放置在所謂High-Order Component或者Smart Component中。在React以及Flux的概念流行之後,Stateless Component的概念深入人心,不過其實對於MVVM中的View,也是無狀態的View。通過雙向數據綁定將界面上的某個元素與ViewModel中的變量相關聯,筆者認為很類似於HOC模式中的Container與Component之間的關聯。隨著應用的界面與功能的擴展,狀態管理會變得愈發混亂。這一點,無論前後端都有異曲同工之難,筆者在基於Redux思想與RxJava的SpringMVC中Controller的代碼風格實踐一文中對於服務端應用程式開發中的狀態管理有過些許討論。

何謂好的架構Balanced Distribution of Responsibilities: 合理的職責劃分

合理的職責劃分即是保證系統中的不同組件能夠被分配合理的職責,也就是在複雜度之間達成一個平衡,職責劃分最權威的原則就是所謂Single Responsibility Principle,單一職責原則。

Testability: 可測試性

可測試性是保證軟體工程質量的重要手段之一,也是保證產品可用性的重要途徑。在傳統的GUI程序開發中,特別是對於界面的測試常常設置於狀態或者運行環境,並且很多與用戶交互相關的測試很難進行場景重現,或者需要大量的人工操作去模擬真實環境。

Ease of Use: 易用性

代碼的易用性保證了程序架構的簡潔與可維護性,所謂最好的代碼就是永遠不需要重寫的代碼,而程序開發中儘量避免的代碼復用方法就是複製粘貼。

Fractal: 碎片化,易於封裝與分發

In fractal architectures, the whole can be naively packaged as a component to be used in some larger application.In non-fractal architectures, the non-repeatable parts are said to be orchestrators over the parts that have hierarchical composition.

所謂的Fractal Architectures,即你的應用整體都可以像單個組件一樣可以方便地進行打包然後應用到其他項目中。而在Non-Fractal Architectures中,不可以被重複使用的部分被稱為層次化組合中的Orchestrators。譬如你在Web中編寫了一個登錄表單,其中的布局、樣式等部分可以被直接復用,而提交表單這個操作,因為具有應用特定性,因此需要在不同的應用中具有不同的實現。譬如下面有一個簡單的表單:

因為不同的應用中,form的提交地址可能不一致,那麼整個form組件是不可直接重用的,即Non-Fractal Architectures。而form中的 input組件是可以進行直接復用的,如果將 input看做一個單獨的GUI架構,即是所謂的Fractal Architectures,form就是所謂的Orchestrators,將可重用的組件編排組合,並且設置應用特定的一些信息。

MV*: 碎片化的狀態與雙向數據流

MVC模式將有關於渲染、控制與數據存儲的概念有機分割,是GUI應用架構模式的一個巨大成就。但是,MVC模式在構建能夠長期運行、維護、有效擴展的應用程式時遇到了極大的問題。MVC模式在一些小型項目或者簡單的界面上仍舊有極大的可用性,但是在現代富客戶端開發中導致職責分割不明確、功能模塊重用性、View的組合性較差。作為繼任者MVP模式分割了View與Model之間的直接關聯,MVP模式中也將更多的ViewLogic轉移到Presenter中進行實現,從而保證了View的可測試性。而最年輕的MVVM將ViewLogic與View剝離開來,保證了View的無狀態性、可重用性、可組合性以及可測試性。總結而言,MV*模型都包含了以下幾個方面:

Models:負責存儲領域/業務邏輯相關的數據與構建數據訪問層,典型的就是譬如Person、PersonDataProvider。

Views:負責將數據渲染展示給用戶,並且響應用戶輸入

Controller/Presenter/ViewModel:往往作為Model與View之間的中間人出現,接收View傳來的用戶事件並且傳遞給Model,同時利用從Model傳來的最新模型控制更新View

MVC: 巨石型控制器

相信每一個程序猿都會宣稱自己掌握MVC,這個概念淺顯易懂,並且貫穿了從GUI應用到服務端應用程式。MVC的概念源自Gamma, Helm, Johnson 以及Vlissidis這四人幫在討論設計模式中的Observer模式時的想法,不過在那本經典的設計模式中並沒有顯式地提出這個概念。我們通常認為的MVC名詞的正式提出是在1979年5月Trygve Reenskaug發表的Thing-Model-View-Editor這篇論文,這篇論文雖然並沒有提及Controller,但是Editor已經是一個很接近的概念。大概7個月之後,Trygve Reenskaug在他的文章Models-Views-Controllers中正式提出了MVC這個三元組。上面兩篇論文中對於Model的定義都非常清晰,Model代表著an abstraction in the form of data in a computing system.,即為計算系統中數據的抽象表述,而View代表著capable of showing one or more pictorial representations of the Model on screen and on hardcopy.,即能夠將模型中的數據以某種方式表現在屏幕上的組件。而Editor被定義為某個用戶與多個View之間的交互接口,在後一篇文章中Controller則被定義為了a special controller ... that permits the user to modify the information that is presented by the view.,即主要負責對模型進行修改並且最終呈現在界面上。從我的個人理解來看,Controller負責控制整個界面,而Editor只負責界面中的某個部分。Controller協調菜單、面板以及像滑鼠點擊、移動、手勢等等很多的不同功能的模塊,而Editor更多的只是負責某個特定的任務。後來,Martin Fowler在2003開始編寫的著作Patterns of Enterprise Application Architecture中重申了MVC的意義:Model View Controller (MVC) is one of the most quoted (and most misquoted) patterns around.,將Controller的功能正式定義為:響應用戶操作,控制模型進行相應更新,並且操作頁面進行合適的重渲染。這是非常經典、狹義的MVC定義,後來在iOS以及其他很多領域實際上運用的MVC都已經被擴展或者賦予了新的功能,不過筆者為了區分架構演化之間的區別,在本文中僅會以這種最樸素的定義方式來描述MVC。
根據上述定義,我們可以看到MVC模式中典型的用戶場景為:

根據上述流程,我們可知經典的MVC模式的特性為:

View、Controller、Model中皆有ViewLogic的部分實現

Controller負責控制View與Model,需要了解View與Model的細節。

View需要了解Controller與Model的細節,需要在偵測用戶行為之後調用Controller,並且在收到通知後調用Model以獲取最新數據

Model並不需要了解Controller與View的細節,相對獨立的模塊

Observer Pattern:自帶觀察者模式的MVC

上文中也已提及,MVC濫觴於Observer模式,經典的MVC模式也可以與Observer模式相結合,其典型的用戶流程為:


可知其與經典的MVC模式區別在於不需要Controller通知View進行更新,而是由Model主動調用View進行更新。這種改變提升了整體效率,簡化了Controller的功能,不過也導致了View與Model之間的緊耦合。

MVP: 將視圖與模型解耦

維基百科將MVP稱為MVC的一個推導擴展,觀其淵源而知其所以然。對於MVP概念的定義,Microsoft較為明晰,而Martin Fowler的定義最為廣泛接受。MVP模式在WinForm系列以Visual-XXX命名的程式語言與Java Swing等系列應用中最早流傳開來,不過後來ASP.NET以及JFaces也廣泛地使用了該模式。在MVP中用戶不再與Presenter進行直接交互,而是由View完全接管了用戶交互,譬如窗口上的每個控制項都知道如何響應用戶輸入並且合適地渲染來自於Model的數據。而所有的事件會被傳輸給Presenter,Presenter在這裡就是View與Model之間的中間人,負責控制Model進行修改以及將最新的Model狀態傳遞給View。這裡描述的就是典型的所謂Passive View版本的MVP,其典型的用戶場景為:

用戶交互輸入了某些內容

View將用戶輸入轉化為發送給Presenter

Presenter控制Model接收需要改變的點

Model將更新之後的值返回給Presenter

Presenter將更新之後的模型返回給View

根據上述流程,我們可知Passive View版本的MVP模式的特性為:

View、Presenter、Model中皆有ViewLogic的部分實現

Presenter負責連接View與Model,需要了解View與Model的細節。

View需要了解Presenter的細節,將用戶輸入轉化為事件傳遞給Presenter

Model需要了解Presenter的細節,在完成更新之後將最新的模型傳遞給Presenter

View與Model之間相互解耦合

Supervising Controller MVP

簡化Presenter的部分功能,使得Presenter只起到需要複雜控制或者調解的操作,而簡單的Model展示轉化直接由View與Model進行交互:

MVVM: 數據綁定與無狀態的視圖

Model View View-Model模型是MV*家族中最年輕的一位,也是由Microsoft提出,並經由Martin Fowler布道傳播。MVVM源於Martin Fowler的Presentation Model,Presentation Model的核心在於接管了View所有的行為響應,View的所有響應與狀態都定義在了Presentation Model中。也就是說,View不會包含任意的狀態。舉個典型的使用場景,當用戶點擊某個按鈕之後,狀態信息是從Presentation Model傳遞給Model,而不是從View傳遞給Presentation Model。任何控制組件間的邏輯操作,即上文所述的ViewLogic,都應該放置在Presentation Model中進行處理,而不是在View層,這一點也是MVP模式與Presentation Model最大的區別。MVVM模式進一步深化了Presentation Model的思想,利用Data Binding等技術保證了View中不會存儲任何的狀態或者邏輯操作。在WPF中,UI主要是利用XAML或者XML創建,而這些標記類型的語言是無法存儲任何狀態的,就像HTML一樣(因此JSX語法其實是將View又有狀態化了),只是允許UI與某個ViewModel中的類建立映射關係。渲染引擎根據XAML中的聲明以及來自於ViewModel的數據最終生成呈現的頁面。因為數據綁定的特性,有時候MVVM也會被稱作MVB:Model View Binder。總結一下,MVVM利用數據綁定徹底完成了從命令式編程到聲明式編程的轉化,使得View逐步無狀態化。一個典型的MVVM的使用場景為:

用戶交互輸入

View將數據直接傳送給ViewModel,ViewModel保存這些狀態數據

在有需要的情況下,ViewModel會將數據傳送給Model

Model在更新完成之後通知ViewModel

ViewModel從Model中獲取最新的模型,並且更新自己的數據狀態

View根據最新的ViewModel的數據進行重新渲染

根據上述流程,我們可知MVVM模式的特性為:

ViewModel、Model中存在ViewLogic實現,View則不保存任何狀態信息

View不需要了解ViewModel的實現細節,但是會聲明自己所需要的數據類型,並且能夠知道如何重新渲染

ViewModel不需要了解View的實現細節(非命令式編程),但是需要根據View聲明的數據類型傳入對應的數據。ViewModel需要了解Model的實現細節。

Model不需要了解View的實現細節,需要了解ViewModel的實現細節

MV* 在端開中的實踐MV* in iOSMVC

Cocoa MVC中往往會將大量的邏輯代碼放入ViewController中,這就導致了所謂的Massive ViewController,而且很多的邏輯操作都嵌入到了View的生命周期中,很難剝離開來。或許你可以將一些業務邏輯或者數據轉換之類的事情放到Model中完成,不過對於View而言絕大部分時間僅起到發送Action給Controller的作用。ViewController逐漸變成了幾乎所有其他組件的Delegate與DataSource,還經常會負責派發或者取消網絡請求等等職責。你的代碼大概是這樣的:

上面這種寫法直接將View於Model關聯起來,其實算是打破了Cocoa MVC的規範的,不過這樣也是能夠減少些Controller中的中轉代碼呢。這樣一個架構模式在進行單元測試的時候就顯得麻煩了,因為你的ViewController與View緊密關聯,使得其很難去進行測試,因為你必須為每一個View創建Mock對象並且管理其生命周期。另外因為整個代碼都混雜在一起,即破壞了職責分離原則,導致了系統的可變性與可維護性也很差。經典的MVC的示例程序如下:

上面這種代碼一看就很難測試,我們可以將生成greeting的代碼移到GreetingModel這個單獨的類中,從而進行單獨的測試。不過我們還是很難去在GreetingViewController中測試顯示邏輯而不調用UIView相關的譬如viewDidLoad、didTapButton等等較為費時的操作。再按照我們上文提及的優秀的架構的幾個方面來看:

MVP

Cocoa中MVP模式是將ViewController當做純粹的View進行處理,而將很多的ViewLogic與模型操作移動到Presenter中進行,代碼如下:

MVVM

import UIKitstruct Person { // Model    let firstName: String    let lastName: String}protocol GreetingViewModelProtocol: class {    var greeting: String? { get }    var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change    init(person: Person)    func showGreeting()}class GreetingViewModel : GreetingViewModelProtocol {    let person: Person    var greeting: String? {        didSet {            self.greetingDidChange?(self)        }    }    var greetingDidChange: ((GreetingViewModelProtocol) -> ())?    required init(person: Person) {        self.person = person    }    func showGreeting() {        self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName    }}class GreetingViewController : UIViewController {    var viewModel: GreetingViewModelProtocol! {        didSet {            self.viewModel.greetingDidChange = { [unowned self] viewModel in                self.greetingLabel.text = viewModel.greeting            }        }    }    let showGreetingButton = UIButton()    let greetingLabel = UILabel()        override func viewDidLoad() {        super.viewDidLoad()        self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)    }    // layout code goes here}// Assembling of MVVMlet model = Person(firstName: "David", lastName: "Blaine")let viewModel = GreetingViewModel(person: model)let view = GreetingViewController()view.viewModel = viewModel

MV* in Android

此部分完整代碼在這裡,筆者在這裡節選出部分代碼方便對照演示。Android中的Activity的功能很類似於iOS中的UIViewController,都可以看做MVC中的Controller。在2010年左右經典的Android程序大概是這樣的:

TextView mCounterText;Button mCounterIncrementButton;int mClicks = 0;public void onCreate(Bundle b) {  super.onCreate(b);  mCounterText = (TextView) findViewById(R.id.tv_clicks);  mCounterIncrementButton = (Button) findViewById(R.id.btn_increment);  mCounterIncrementButton.setOnClickListener(new View.OnClickListener() {    public void onClick(View v) {      mClicks++;      mCounterText.setText(""+mClicks);    }  });}

後來2013年左右出現了ButterKnife這樣的基於註解的控制項綁定框架,此時的代碼看上去是這樣的:

@Bind(R.id.tv_clicks) mCounterText;@OnClick(R.id.btn_increment)public void onSubmitClicked(View v) {    mClicks++;    mCounterText.setText("" + mClicks);}

後來Google官方也推出了數據綁定的框架,從此MVVM模式在Android中也愈發流行:

<layout xmlns:android="http://schemas.android.com/apk/res/android">   <data>       <variable name="counter" type="com.example.Counter"/>       <variable name="counter" type="com.example.ClickHandler"/>   </data>   <LinearLayout       android:orientation="vertical"       android:layout_width="match_parent"       android:layout_height="match_parent">       <TextView android:layout_width="wrap_content"           android:layout_height="wrap_content"           android:text="@{counter.value}"/>       <Buttonandroid:layout_width="wrap_content"           android:layout_height="wrap_content"           android:text="@{handlers.clickHandle}"/>   </LinearLayout></layout>

後來Anvil這樣的受React啟發的組件式框架以及Jedux這樣借鑑了Redux全局狀態管理的框架也將Unidirectional 架構引入了Android開發的世界。

MVCMVPMVVM

XML中聲明數據綁定

View中綁定ViewModel

ViewModel中進行數據操作

Unidirectional User Interface Architecture: 單向數據流

Unidirectional User Interface Architecture架構的概念源於後端常見的CROS/Event Sourcing模式,其核心思想即是將應用狀態被統一存放在一個或多個的Store中,並且所有的數據更新都是通過可觀測的Actions觸發,而所有的View都是基於Store中的狀態渲染而來。該架構的最大優勢在於整個應用中的數據流以單向流動的方式從而使得有用更好地可預測性與可控性,這樣可以保證你的應用各個模塊之間的鬆耦合性。與MVVM模式相比,其解決了以下兩個問題:

雙向數據綁定的不足

This means that one change (a user input or API response) can affect the state of an application in many places in the code — for example, two-way data binding. That can be hard to maintain and debug.

Facebook強調,雙向數據綁定極不利於代碼的擴展與維護。從具體的代碼實現角度來看,雙向數據綁定會導致更改的不可預期性(UnPredictable),就好像Angular利用Dirty Checking來進行是否需要重新渲染的檢測,這導致了應用的緩慢,簡直就是來砸場子的。而在採用了單向數據流之後,整個應用狀態會變得可預測(Predictable),也能很好地了解當狀態發生變化時到底會有多少的組件發生變化。另一方面,相對集中地狀態管理,也有助於你不同的組件之間進行信息交互或者狀態共享,特別是像Redux這種強調Single Store與SIngle State Tree的狀態管理模式,能夠保證以統一的方式對於應用的狀態進行修改,並且Immutable的概念引入使得狀態變得可回溯。
譬如Facebook在Flux Overview中舉的例子,當我們希望在一個界面上同時展示未讀信息列表與未讀信息的總數目的時候,對於MV*就有點噁心了,特別是當這兩個組件不在同一個ViewModel/Controller中的時候。一旦我們將某個未讀信息標識為已讀,會引起控制已讀信息、未讀信息、未讀信息總數目等等一系列模型的更新。特別是很多時候為了方便我們可能在每個ViewModel/Controller都會設置一個數據副本,這會導致依賴連鎖更新,最終導致不可預測的結果與性能損耗。而在Flux中這種依賴是反轉的,Store接收到更新的Action請求之後對數據進行統一的更新並且通知各個View,而不是依賴於各個獨立的ViewModel/Controller所謂的一致性更新。從職責劃分的角度來看,除了Store之外的任何模塊其實都不知道應該如何處理數據,這就保證了合理的職責分割。這種模式下,當我們創建新項目時,項目複雜度的增長瓶頸也就會更高,不同於傳統的View與ViewLogic之間的綁定,控制流被獨立處理,當我們添加新的特性,新的數據,新的界面,新的邏輯處理模塊時,並不會導致原有模塊的複雜度增加,從而使得整個邏輯更加清晰可控。

這裡還需要提及一下,很多人應該是從React開始認知到單向數據流這種架構模式的,而當時Angular 1的緩慢與性能之差令人髮指,但是譬如Vue與Angular 2的性能就非常優秀。借用Vue.js官方的說法,

The virtual-DOM approach provides a functional way to describe your view at any point of time, which is really nice. Because it doesn’t use observables and re-renders the entire app on every update, the view is by definition guaranteed to be in sync with the data. It also opens up possibilities to isomorphic JavaScript applications.

Instead of a Virtual DOM, Vue.js uses the actual DOM as the template and keeps references to actual nodes for data bindings. This limits Vue.js to environments where DOM is present. However, contrary to the common misconception that Virtual-DOM makes React faster than anything else, Vue.js actually out-performs React when it comes to hot updates, and requires almost no hand-tuned optimization. With React, you need to implementshouldComponentUpdate everywhere and use immutable data structures to achieve fully optimized re-renders.

總而言之,筆者認為雙向數據流與單向數據流相比,性能上孰優孰劣尚無定論,最大的區別在於單向數據流與雙向數據流相比有更好地可控性,這一點在上文提及的函數響應式編程中也有體現。若論快速開發,筆者感覺雙向數據綁定略勝一籌,畢竟這種View與ViewModel/ViewLogic之間的直接綁定直觀便捷。而如果是注重於全局的狀態管理,希望維護耦合程度較低、可測試性/可擴展性較高的代碼,那麼還是單向數據流,即Unidirectional Architecture較為合適。一家之言,歡迎討論。

Flux:數據流驅動的頁面

Flux不能算是絕對的先行者,但是在Unidirectional Architecture中卻是最富盛名的一個,也是很多人接觸到的第一個Unidirectional Architecture。Flux主要由以下幾個部分構成:

根據上述流程,我們可知Flux模式的特性為:

Dispatcher:Event Bus中設置有一個單例的Dispatcher,很多Flux的變種都移除了Dispatcher依賴。

只有View使用可組合的組件:在Flux中只有React的組件可以進行層次化組合,而Stores與Actions都不可以進行層次化組合。React組件與Flux一般是鬆耦合的,因此Flux並不是Fractal,Dispatcher與Stores可以被看做Orchestrator。

用戶事件響應在渲染時聲明:在React的 render() 函數中,即負責響應用戶交互,也負責註冊用戶事件的處理器

下面我們來看一個具體的代碼對比,首先是以經典的Cocoa風格編寫一個簡單的計數器按鈕:

上述代碼邏輯用上文提及的MVC模式圖演示就是:

而如果用Flux模式實現,會是下面這個樣子:

其數據流圖為:

Redux:集中式的狀態管理

Redux是Flux的所有變種中最為出色的一個,並且也是當前Web領域主流的狀態管理工具,其獨創的理念與功能深刻影響了GUI應用程式架構中的狀態管理的思想。Redux將Flux中單例的Dispatcher替換為了單例的Store,即也是其最大的特性,集中式的狀態管理。並且Store的定義也不是從零開始單獨定義,而是基於多個Reducer的組合,可以把Reducer看做Store Factory。Redux的重要組成部分包括:

Singleton Store:管理應用中的狀態,並且提供了一個dispatch(action)函數。

Provider:用於監聽Store的變化並且連接像React、Angular這樣的UI框架

Actions:基於用戶輸入創建的分發給Reducer的事件

Reducers:用於響應Actions並且更新全局狀態樹的純函數

根據上述流程,我們可知Redux模式的特性為:

以工廠模式組裝Stores:Redux允許我以createStore()函數加上一系列組合好的Reducer函數來創建Store實例,還有另一個applyMiddleware()函數可以允許在dispatch()函數執行前後鏈式調用一系列中間件。

Providers:Redux並不特定地需要何種UI框架,可以與Angular、React等等很多UI框架協同工作。Redux並不是Fractal,一般來說Store被視作Orchestrator。

User Event處理器即可以選擇在渲染函數中聲明,也可以在其他地方進行聲明。

Model-View-Update

又被稱作Elm Architecture,上面所講的Redux就是受到Elm的啟發演化而來,因此MVU與Redux之間有很多的相通之處。MVU使用函數式程式語言Elm作為其底層開發語言,因此該架構可以被看做更純粹的函數式架構。MVU中的基本組成部分有:

根據上述流程,我們可知Elm模式的特性為:

Model-View-Intent

MVI是一個基於RxJS的響應式單向數據流架構。MVI也是Cycle.js的首選架構,主要由Observable事件流對象與處理函數組成。其主要的組成部分包括:

Intent:Observable提供的將用戶事件轉化為Action的函數

Model:Observable提供的將Action轉化為可觀測的State的函數

View:將狀態渲染為用戶界面的函數

Custom Element:類似於React Component那樣的界面組件


根據上述流程,我們可知MVI模式的特性為:

重度依賴於Observables:架構中的每個部分都會被轉化為Observable事件流

Intent:不同於Flux或者Redux,MVI中的Actions並沒有直接傳送給Dispatcher或者Store,而是交於正在監聽的Model

徹底的響應式,並且只要所有的組件都遵循MVI模式就能保證整體架構的fractal特性

Clean Architecture

Uncle Bob 提出 Clean Architecture 最早並不是專門面向於GUI應用程式,而是描述了一種用於構建可擴展、可測試軟體系統的概要原則。 Clean Architecture 可能運用於構建網站、Web 應用、桌面應用以及移動應用等不同領域場景的軟體開發中。其定義的基本原則保證了關注點分離以及整個軟體項目的模塊性與可組織性,也就是我們在上文提及的 GUI 應用程式架構中所需要考量的點。 Clean Architecture 中最基礎的理論當屬所謂的依賴原則(Dependency Rule),在依賴洋蔥圖中的任一內層模塊不應該了解或依賴於任何外層模塊。換言之,我們定義在外層模塊中的代碼不應該被內層模塊所引入,包括變量、函數、類等等任何的軟體實體。除此之外,Clean Architecture 還強制規定了所有鄰接圈層之間的交互與通信應當以抽象方式定義,譬如在 Android 中應該利用 Java 提供的 POJOs 以及 Interfaces,而 iOS 中應該使用 Protocols 或者標準類。這種強制定義也就保證了不同層之間的組件完全解耦合,並且能夠很方便地更改或者 Mock 測試,而不會影響到其他層的代碼。Clean Architecture 是非常理想化的架構定義模式,也僅是提出了一些基本的原則,其在 iOS 的具體實踐也就是所謂的 VIPER 架構。

iOS Viper Architecture

Viper架構中職責分割地更為細緻,大概分為了五層:

一般來說,一個VIPER模塊可以是單獨的某個頁面或者整個應用程式,經常會按照權限來劃分。

來源:https://zhuanlan.zhihu.com/p/26799645?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

更多學習資料點擊下面的「閱讀原文」獲取

Java和Android架構

歡迎關注我們,一起討論技術,掃描和長按下方的二維碼可快速關注我們。或搜索微信公眾號:JANiubility。

公眾號:JANiubility

相關焦點

  • 前端架構最全總結——GUI 應用程式架構的十年變遷:MVC、MVP、MVVM、Unidirectional、Clean
    筆者在日前總結的泛前端知識圖譜(Web/iOS/Android/RN) 一文中就對泛前端開發學習中可能會涉及到的知識點進行了總結與盤點,近日筆者打算為該知識圖譜編寫附帶的簡略說明,因此先對舊文 GUI 應用程式架構的十年變遷:MVC、MVP、MVVM、Unidirectional、Clean 進行了重製與補充,從屬於筆者的大前端開發技術相關倉庫,同樣向 Martin Fowler 及其撰寫的 GUI
  • 架構 MVC,MVP 和 MVVM 圖示
    來自:阮一峰的網絡日誌連結:http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html
  • 淺談框架模式 MVC、MVP 和 MVVM
    早些年由於UI程序還處在一個懵懂期, 邏輯不算太複雜,代碼量也不會太多。這樣的搞法似乎也沒有什麼問題。 畢竟到達A B兩點最短的距離就是直線,上述代碼可以說是實現某功能的最短路徑。p所以 mvp下 p 非常厚實。
  • 界面之下:還原真實的 MVC、MVP、MVVM 模式
    前言做客戶端開發、前端開發對MVC、MVP、MVVM這些名詞不了解也應該大致聽過,都是為了解決圖形界面應用程式複雜性管理問題而產生的應用架構模式
  • MVC、MVP、MVVM,談談我對Android應用架構的理解
    本篇來自 08_carmelo 的投稿,分享了他對 Android應用架構的理解 ,一起來看看!希望大家喜歡。08_carmelo 的博客地址:https://www.jianshu.com/u/b8dad3885e05android架構可能是論壇討論最多的話題了,mvc mvp和mvvm不絕於耳,後面又有模塊化和插件化。對此,關於哪種架構更好的爭論從未停止。
  • Android MVP && MVVM深度解析
    看一個最基礎的MVP##Modelclass LoginModel : BaseModel() {    fun login(userName: String, pwd: String): Int {        //...省略網絡請求        return 1    }}
  • 十分鐘上手MVC、MVP、MVVM
    但是對於一個沒有實戰過這三種架構模式的同學們,當你辛辛苦苦花了幾個小時讀了關於這三種模式的文章,看了一些彼此分離沒有對比性的示例代碼。你們真的會很快上手這三種架構模式的寫法嗎?本文通過一個三合一demo,關鍵點代碼進行注釋,十分鐘帶你入門。讓你分分鐘看懂三種模式的區別,快速上手三種寫法。
  • 淺談MVC、MVP、MVVM架構模式的區別和聯繫
    MVC、MVP、MVVM這些模式是為了解決開發過程中的實際問題而提出來的,目前作為主流的幾種架構模式而被廣泛使用
  • Web 應用程式的架構體系變遷
    在過去的幾年裡,Web應用程式開發環境,包括客戶端(前端)以及伺服器端(後端)在不斷地發生變化。
  • Android App的設計架構:MVC,MVP,MVVM與架構經驗談
    需要應用這些設計架構嗎?MVC,MVP等架構講的是什麼?區別是什麼?本文就來帶你分析一下這幾個架構的特性,優缺點,以及App架構設計中應該注意的問題。 1.架構設計的目的通過設計使程序模塊化,做到模塊內部的高聚合和模塊之間的低耦合。
  • 從最簡單的Android MVP講起
    所以我會寫一個最簡單的mvp demo。來幫助大家理解mvp的本質。大多數時候,問題都可以拆解為,WHTA,WHY,HOW;什麼是MVP,為什麼使用MVP,如何使用MVP。在基於傳統android架構的mvc模式中。model層很多時候只是一個bean類。而view層只是一個xml文件,controller層也就是activity層幾乎承擔了諸如網絡請求,資料庫,更新UI等所有的工作,全在activity裡完成。這也就導致了activity文件十分龐大臃腫。但是,問題接踵而至。
  • Android官方MVP架構解讀
    所以對於MVC架構並不很合適運用於Android的開發中。下面就來介紹一下MVP架構以及看一下google官方給出的MVP架構示例。MVP架構簡介  對於一個應用而言我們需要對它抽象出各個層面,而在MVP架構中它將UI界面和數據進行隔離,所以我們的應用也就分為三個層次。
  • Android開發中的MVP架構
    這是上面片文章的摘要:Enitities:可以是一個持有方法函數的對象可以是一組數據結構或方法函數它並不重要,能在項目中被不同應用程式使用即可那就是如何避免複雜混亂的代碼,讓執行單元測試變得容易,創造高質量應用程式。就這樣。當然,遠不止這三種架構模式。而且任何一種模式都不可能是銀彈,他們只是架構模式之一,不是解決問題的唯一途徑。這些只是方法、手段而不是目的、目標。
  • 正確認識 MVC/MVP/MVVM
    ,對 MVC、MVP、MVVM 這幾個名詞應該都不陌生,這是三個最常用的應用架構模式,目的都是為了將業務和視圖的實現代碼分離,從而使同一個程序可以使用不同的表現形式。不過,網上的文章對這方面的解說眾說紛紜,其中不乏有些錯誤的描述,導致有些人應用這些架構模式時陷入一些錯誤陷阱。本文將追根溯源,力求讓大夥對這三個架構模式形成正確認識。
  • MVVM和MVC有什麼區別
    1、mvvm各部分的通信是雙向的,而mvc各部分通信是單向的;2、mvvm是真正將頁面與數據邏輯分離放到js裡去實現,而mvc
  • MVVM架構使用之我見
    ,在改進和個性化定製界面及用戶交互的同時,不需要重新編寫業務邏輯Model:應用程式中用於處理應用程式數據邏輯的部分,它主要負責網絡請求,資料庫處理,I/O等的操作。View:應用程式中處理數據顯示的部分。在Android開發中,它一般對應著xml布局文件Controller:應用程式中處理用戶交互的部分,接受用戶的輸入並調用Model和View去完成用戶的需求。
  • 讀前端架構設計——我眼中的前端架構
    後來,在前後端一體的時代,「前端」寫頁面模板,後端讀取模板,生成靜態頁面發送給瀏覽器渲染,那時的架構是後端架構,採用的是後端MVC模式,前端只是MVC的V層。隨著Web2.0時代的到來,前端開始從刀耕火種的蠻荒時代向現代的工程化方向演進,前後端分離,前端開始慢慢建立完善的流程和體系,前端架構開始出現。架構的本質是什麼?有人說是管理,對機器和代碼的管理,那麼前端架構是管理什麼呢?
  • web架構和MVC架構
    關於B/S和C/S:管理軟體使用B/S架構,而遊戲因為要基於顯卡實現絢麗的效果所以使用C/S架構。因為B/S架構便於程序的維護、升級和修改,所以今後B/S還有很大的發展空間。但注意並不是說有瀏覽器的就一定是B/S架構,比如網頁上的小遊戲其實是C/S架構,只不過它是邊玩邊下載,B/S架構和C/S架構最本質的區別在於B/S是一種輕客戶端重伺服器的架構,它把所有的邏輯,頁面素材都放在伺服器上,瀏覽器上的所有東西都是從伺服器上下載下來的,所以說,並不是有瀏覽器的就是B/S架構,應該說滿足輕客戶端重伺服器的這種模式的就是B/S架構,再比如微信小程序雖然沒有瀏覽器,但它是一個B/S
  • MVC, MVP, MVVM比較以及區別
    軟體中最核心的,最基本的東西是什麼? 是的,是數據。我們寫的所有代碼,都是圍繞數據的。圍繞著數據的產生、修改等變化,出現了業務邏輯。圍繞著數據的顯示,出現了不同的界面技術。 沒有很好設計的代碼,常常就會出現數據層(持久層)和業務邏輯層還有界面代碼耦合的情況。
  • 架構變遷:企業正往ARM架構遷移
    架構變遷說到CPU架構,我們可能必然會提到CISC(複雜指令集,比如桌面端採用的X86系列)和RISC(精簡指令集,比如移動端廣泛採用的ARM系列)。過去,由於應用主要是跑在對功耗不敏感的X86架構CPU上,人們對該架構下的應用進行了大量的優化,ARM平臺的性能優勢並沒有充分的發揮出來。