GUI應用程序架構的十年變遷:MVC,MVP,MVVM,Unidirectional,Clean
十年前,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并且認為集中式的狀態管理是正確的方向。但是必須要強調,GUI架構本身是無法脫離其所依托的平臺,下文筆者也會淺述由于Android與iOS本身SDK API的特殊性,生搬硬套其他平臺的架構模式也是邯鄲學步,沐猴而冠。不過總結而言,它山之石,可以攻玉,本身我們所處的開發環境一直在不斷變化,對于過去的精華自當應該保留,并且與新的環境相互印證,觸類旁通。
Introduction
Make everything as simple as possible, but not simpler — Albert Einstein
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這一小節)。
Terminology:名詞解釋
正文之前,我們先對一些概念進行闡述:
-
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內部的計數器加一操作:
// This is inside the Foo module
function onNetworkRequest() {
// ...
Bar.incrementCounter();
// ...
}
在這里將這種邏輯關系可以描述為Foo擁有著 網絡請求完成之后將Bar內的計數器加一 這個關系的控制權,也就是Foo占有主導性,而Bar相對而言是Passive被動的:
Bar是Passive的,它允許其他模塊改變其內部狀態。而Foo是主動地,它需要保證能夠正確地更新Bar的內部狀態,Passive模塊并不知道誰會更新到它。而另一種方案就是類似于控制反轉,由Bar完成對于自己內部狀態的更新:
在這種模式下,Bar監聽來自于Foo中的事件,并且在某些事件發生之后進行內部狀態更新:
// This is inside the Bar module
Foo.addOnNetworkRequestListener(() => {
self.incrementCounter(); // self is Bar
});
此時Bar就變成了Reactive Module,它負責自己的內部的狀態更新以響應外部的事件,而Foo并不知道它發出的事件會被誰監聽。
Declarative vs. Imperative:命令式編程與聲明式編程
前端攻略-從路人甲到英雄無敵二:JavaScript 與不斷演化的框架
形象地來描述命令式編程與聲明式編程的區別,就好像C#/JavaScript與類似于XML或者HTML這樣的標記語言之間的區別。命令式編程關注于 how to do what you want done ,即事必躬親,需要安排好每個要做的細節。而聲明式編程關注于 what you want done without worrying about how ,即只需要聲明要做的事情而不用將具體的過程再耦合進來。對于開發者而言,聲明式編程將很多底層的實現細節向開發者隱藏,而使得開發者可以專注于具體的業務邏輯,同時也保證了代碼的解耦與單一職責。譬如在Web開發中,如果你要基于jQuery將數據填充到頁面上,那么大概按照命令式編程的模式你需要這么做:
var options = $("#options");
$.each(result, function() {
options.append($("<option />").val(this.id).text(this.name));
});
而以Angular 1聲明式的方式進行編寫,那么是如下的標記模樣:
<div ng-repeat="item in items" ng-click="select(item)">{{item.name}}
</div>
而在iOS和Android開發中,近年來函數響應式編程(Functional Reactive Programming)也非常流行,參閱筆者關于響應式編程的介紹可以了解,響應式編程本身是基于流的方式對于異步操作的一種編程優化,其在整個應用架構的角度看更多的是細節點的優化。以 RxSwift 為例,通過響應式編程可以編寫出非常優雅的用戶交互代碼:
let searchResults = searchBar.rx_text
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { query -> Observable<[Repository]> in
if query.isEmpty {
return Observable.just([])
}
return searchGitHub(query)
.catchErrorJustReturn([])
}
.observeOn(MainScheduler.instance)
searchResults
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) {
(index, repository: Repository, cell) in
cell.textLabel?.text = repository.name
cell.detailTextLabel?.text = repository.url
}
.addDisposableTo(disposeBag)
其直觀的效果大概如下圖所示:
到這里可以看出,無論是從命令式編程與聲明式編程的對比還是響應式編程的使用,我們開發時的關注點都慢慢轉向了所謂的數據流。便如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之間開發體驗的差異,筆者感覺很類似于靜態類型語言與動態類型語言之間的差異。
功能的模塊化
老實說在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樹上,也是典型的將狀態與模板混合的用法。這就導致了我們并不能控制到底應該何時去進行重新渲染以及哪些狀態變更的操作才是必須的,
var Header = component(function (data) {
// First argument is h1 metadata
return h1(null, data.text);
});
// Render the component to our DOM
render(Header({text: 'Hello'}), document.body);
// Some time later, we change it, by calling the
// component once more.
setTimeout(function () {
render(Header({text: 'Changed'}), document.body);
}, 1000);
var hello = Header({ text: 'Hello' }); var bye = Header({ text: 'Good Bye' });
狀態管理
可變的與不可預測的狀態是軟件開發中的萬惡之源
上文提及,我們盡可能地希望組件的無狀態性,那么整個應用中的狀態管理應該盡量地放置在所謂High-Order Component或者Smart Component中。在React以及Flux的概念流行之后,Stateless Component的概念深入人心,不過其實對于MVVM中的View,也是無狀態的View。通過雙向數據綁定將界面上的某個元素與ViewModel中的變量相關聯,筆者認為很類似于HOC模式中的Container與Component之間的關聯。隨著應用的界面與功能的擴展,狀態管理會變得愈發混亂。這一點,無論前后端都有異曲同工之難,筆者在 基于Redux思想與RxJava的SpringMVC中Controller的代碼風格實踐 一文中對于服務端應用程序開發中的狀態管理有過些許討論。
Features of Good Architectural Pattern:何為好的架構模式
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.
-
By André Staltz
所謂的Fractal Architectures,即你的應用整體都可以像單個組件一樣可以方便地進行打包然后應用到其他項目中。而在Non-Fractal Architectures中,不可以被重復使用的部分被稱為層次化組合中的Orchestrators。譬如你在Web中編寫了一個登錄表單,其中的布局、樣式等部分可以被直接復用,而提交表單這個操作,因為具有應用特定性,因此需要在不同的應用中具有不同的實現。譬如下面有一個簡單的表單:
<form action="form_action.asp" method="get">
<p>First name: <input type="text" name="fname" /></p>
<p>Last name: <input type="text" name="lname" /></p>
<input type="submit" value="Submit" />
</form>
因為不同的應用中,form的提交地址可能不一致,那么整個form組件是不可直接重用的,即Non-Fractal Architectures。而form中的 input 組件是可以進行直接復用的,如果將 input 看做一個單獨的GUI架構,即是所謂的Fractal Architectures,form就是所謂的Orchestrators,將可重用的組件編排組合,并且設置應用特定的一些信息。
Reference
Overview
MV*
MVC
MVP
MVVM
Unidirectional Architecture
-
jedux :Redux architecture for Android
Viper/Clean Architecture
MV*:Fragmentary State 碎片化的狀態與雙向數據流
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:Monolithic Controller
相信每一個程序猿都會宣稱自己掌握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模式中典型的用戶場景為:
-
用戶交互輸入了某些內容
-
Controller將用戶輸入轉化為Model所需要進行的更改
-
Model中的更改結束之后,Controller通知View進行更新以表現出當前Model的狀態
根據上述流程,我們可知經典的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模式相結合,其典型的用戶流程為:
-
用戶交互輸入了某些內容
-
Controller將用戶輸入轉化為Model所需要進行的更改
-
View作為Observer會監聽Model中的任意更新,一旦有更新事件發出,View會自動觸發更新以展示最新的Model狀態
可知其與經典的MVC模式區別在于不需要Controller通知View進行更新,而是由Model主動調用View進行更新。這種改變提升了整體效率,簡化了Controller的功能,不過也導致了View與Model之間的緊耦合。
MVP:Decoupling View and Model 將視圖與模型解耦, View<->Presenter
維基百科將 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:Data Binding & Stateless View 數據綁定與無狀態的View,View<->ViewModels
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* in iOS
MVC
Cocoa MVC中往往會將大量的邏輯代碼放入ViewController中,這就導致了所謂的Massive ViewController,而且很多的邏輯操作都嵌入到了View的生命周期中,很難剝離開來。或許你可以將一些業務邏輯或者數據轉換之類的事情放到Model中完成,不過對于View而言絕大部分時間僅起到發送Action給Controller的作用。ViewController逐漸變成了幾乎所有其他組件的Delegate與DataSource,還經常會負責派發或者取消網絡請求等等職責。你的代碼大概是這樣的:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
上面這種寫法直接將View于Model關聯起來,其實算是打破了Cocoa MVC的規范的,不過這樣也是能夠減少些Controller中的中轉代碼呢。這樣一個架構模式在進行單元測試的時候就顯得麻煩了,因為你的ViewController與View緊密關聯,使得其很難去進行測試,因為你必須為每一個View創建Mock對象并且管理其生命周期。另外因為整個代碼都混雜在一起,即破壞了職責分離原則,導致了系統的可變性與可維護性也很差。經典的MVC的示例程序如下:
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
class GreetingViewController : UIViewController { // View + Controller
var person: Person!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
}
func didTapButton(button: UIButton) {
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Assembling of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;
上面這種代碼一看就很難測試,我們可以將生成greeting的代碼移到GreetingModel這個單獨的類中,從而進行單獨的測試。不過我們還是很難去在GreetingViewController中測試顯示邏輯而不調用UIView相關的譬如 viewDidLoad 、 didTapButton 等等較為費時的操作。再按照我們上文提及的優秀的架構的幾個方面來看:
-
Distribution:View與Model是分割開來了,不過View與Controller是緊耦合的
-
Testability:因為較差的職責分割導致貌似只有Model部分方便測試
-
易用性:因為程序比較直觀,可能容易理解。
MVP
Cocoa中MVP模式是將ViewController當做純粹的View進行處理,而將很多的ViewLogic與模型操作移動到Presenter中進行,代碼如下:
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingView: class {
func setGreeting(greeting: String)
}
protocol GreetingViewPresenter {
init(view: GreetingView, person: Person)
func showGreeting()
}
class GreetingPresenter : GreetingViewPresenter {
unowned let view: GreetingView
let person: Person
required init(view: GreetingView, person: Person) {
self.view = view
self.person = person
}
func showGreeting() {
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.view.setGreeting(greeting)
}
}
class GreetingViewController : UIViewController, GreetingView {
var presenter: GreetingViewPresenter!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
}
func didTapButton(button: UIButton) {
self.presenter.showGreeting()
}
func setGreeting(greeting: String) {
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
-
Distribution:主要的業務邏輯分割在了Presenter與Model中,View相對呆板一點
-
Testability:較為方便地測試
-
易用性:代碼職責分割的更為明顯,不過不像MVC那樣直觀易懂了
MVVM
import UIKit
struct 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 MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
-
Distribution:在Cocoa MVVM中,View相對于MVP中的View擔負了更多的功能,譬如需要構建數據綁定等等
-
Testability:ViewModel擁有View中的所有數據結構,因此很容易就可以進行測試
-
易用性:相對而言有很多的冗余代碼
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開發的世界。
MVC
-
聲明View中的組件對象或者Model對象
private Subscription subscription;
private RecyclerView reposRecycleView;
private Toolbar toolbar;
private EditText editTextUsername;
private ProgressBar progressBar;
private TextView infoTextView;
private ImageButton searchButton;
-
將組件與Activity中對象綁定,并且聲明用戶響應處理函數
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = (ProgressBar) findViewById(R.id.progress);
infoTextView = (TextView) findViewById(R.id.text_info);
//Set up ToolBar
toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
//Set up RecyclerView
reposRecycleView = (RecyclerView) findViewById(R.id.repos_recycler_view);
setupRecyclerView(reposRecycleView);
// Set up search button
searchButton = (ImageButton) findViewById(R.id.button_search);
searchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadGithubRepos(editTextUsername.getText().toString());
}
});
//Set up username EditText
editTextUsername = (EditText) findViewById(R.id.edit_text_username);
editTextUsername.addTextChangedListener(mHideShowButtonTextWatcher);
editTextUsername.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
String username = editTextUsername.getText().toString();
if (username.length() > 0) loadGithubRepos(username);
return true;
}
return false;
}
});
-
用戶輸入之后的更新流程
progressBar.setVisibility(View.VISIBLE);
reposRecycleView.setVisibility(View.GONE);
infoTextView.setVisibility(View.GONE);
ArchiApplication application = ArchiApplication.get(this);
GithubService githubService = application.getGithubService();
subscription = githubService.publicRepositories(username)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(application.defaultSubscribeScheduler())
.subscribe(new Subscriber<List<Repository>>() {
@Override
public void onCompleted() {
progressBar.setVisibility(View.GONE);
if (reposRecycleView.getAdapter().getItemCount() > 0) {
reposRecycleView.requestFocus();
hideSoftKeyboard();
reposRecycleView.setVisibility(View.VISIBLE);
} else {
infoTextView.setText(R.string.text_empty_repos);
infoTextView.setVisibility(View.VISIBLE);
}
}
@Override
public void onError(Throwable error) {
Log.e(TAG, "Error loading GitHub repos ", error);
progressBar.setVisibility(View.GONE);
if (error instanceof HttpException
&& ((HttpException) error).code() == 404) {
infoTextView.setText(R.string.error_username_not_found);
} else {
infoTextView.setText(R.string.error_loading_repos);
}
infoTextView.setVisibility(View.VISIBLE);
}
@Override
public void onNext(List<Repository> repositories) {
Log.i(TAG, "Repos loaded " + repositories);
RepositoryAdapter adapter =
(RepositoryAdapter) reposRecycleView.getAdapter();
adapter.setRepositories(repositories);
adapter.notifyDataSetChanged();
}
});
MVP
-
將Presenter與View綁定,并且將用戶響應事件綁定到Presenter中
//Set up presenter
presenter = new MainPresenter();
presenter.attachView(this);
...
// Set up search button
searchButton = (ImageButton) findViewById(R.id.button_search);
searchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.loadRepositories(editTextUsername.getText().toString());
}
});
-
Presenter中調用Model更新數據,并且調用View中進行重新渲染
public void loadRepositories(String usernameEntered) {
String username = usernameEntered.trim();
if (username.isEmpty()) return;
mainMvpView.showProgressIndicator();
if (subscription != null) subscription.unsubscribe();
ArchiApplication application = ArchiApplication.get(mainMvpView.getContext());
GithubService githubService = application.getGithubService();
subscription = githubService.publicRepositories(username)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(application.defaultSubscribeScheduler())
.subscribe(new Subscriber<List<Repository>>() {
@Override
public void onCompleted() {
Log.i(TAG, "Repos loaded " + repositories);
if (!repositories.isEmpty()) {
mainMvpView.showRepositories(repositories);
} else {
mainMvpView.showMessage(R.string.text_empty_repos);
}
}
@Override
public void onError(Throwable error) {
Log.e(TAG, "Error loading GitHub repos ", error);
if (isHttp404(error)) {
mainMvpView.showMessage(R.string.error_username_not_found);
} else {
mainMvpView.showMessage(R.string.error_loading_repos);
}
}
@Override
public void onNext(List<Repository> repositories) {
MainPresenter.this.repositories = repositories;
}
});
}
MVVM
-
XML中聲明數據綁定
<data>
<variable
name="viewModel"
type="uk.ivanc.archimvvm.viewmodel.MainViewModel"/>
</data>
...
<EditText
android:id="@+id/edit_text_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/button_search"
android:hint="@string/hit_username"
android:imeOptions="actionSearch"
android:inputType="text"
android:onEditorAction="@{viewModel.onSearchAction}"
android:textColor="@color/white"
android:theme="@style/LightEditText"
app:addTextChangedListener="@{viewModel.usernameEditTextWatcher}"/>
-
View中綁定ViewModel
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
mainViewModel = new MainViewModel(this, this);
binding.setViewModel(mainViewModel);
setSupportActionBar(binding.toolbar);
setupRecyclerView(binding.reposRecyclerView);
-
ViewModel中進行數據操作
public boolean onSearchAction(TextView view, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
String username = view.getText().toString();
if (username.length() > 0) loadGithubRepos(username);
return true;
}
return false;
}
public void onClickSearch(View view) {
loadGithubRepos(editTextUsernameValue);
}
public TextWatcher getUsernameEditTextWatcher() {
return new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
editTextUsernameValue = charSequence.toString();
searchButtonVisibility.set(charSequence.length() > 0 ? View.VISIBLE : View.GONE);
}
@Override
public void afterTextChanged(Editable editable) {
}
};
}
Unidirectional User Interface Architecture:單向數據流
Unidirectional User Interface Architecture架構的概念源于后端常見的CROS/Event Sourcing模式,其核心思想即是將應用狀態被統一存放在一個或多個的Store中,并且所有的數據更新都是通過可觀測的Actions觸發,而所有的View都是基于Store中的狀態渲染而來。該架構的最大優勢在于整個應用中的數據流以單向流動的方式從而使得有用更好地可預測性與可控性,這樣可以保證你的應用各個模塊之間的松耦合性。與MVVM模式相比,其解決了以下兩個問題:
-
避免了數據在多個ViewModel中的冗余與不一致問題
-
分割了ViewModel的職責,使得ViewModel變得更加Clean
Why not Bidirectional(Two-way DataBinding)?
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.
非死book強調,雙向數據綁定極不利于代碼的擴展與維護。
從具體的代碼實現角度來看,雙向數據綁定會導致更改的不可預期性(UnPredictable),就好像Angular利用Dirty Checking來進行是否需要重新渲染的檢測,這導致了應用的緩慢,簡直就是來砸場子的。而在采用了單向數據流之后,整個應用狀態會變得可預測(Predictable),也能很好地了解當狀態發生變化時到底會有多少的組件發生變化。另一方面,相對集中地狀態管理,也有助于你不同的組件之間進行信息交互或者狀態共享,特別是像Redux這種強調Single Store與SIngle State Tree的狀態管理模式,能夠保證以統一的方式對于應用的狀態進行修改,并且Immutable的概念引入使得狀態變得可回溯。
譬如非死book在 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主要由以下幾個部分構成:
-
Stores:存放業務數據和應用狀態,一個Flux中可能存在多個Stores
-
View:層次化組合的React組件
-
Actions:用戶輸入之后觸發View發出的事件
-
Dispatcher:負責分發Actions
根據上述流程,我們可知Flux模式的特性為:
-
Dispatcher:Event Bus中設置有一個單例的Dispatcher,很多Flux的變種都移除了Dispatcher依賴。
-
只有View使用可組合的組件:在Flux中只有React的組件可以進行層次化組合,而Stores與Actions都不可以進行層次化組合。React組件與Flux一般是松耦合的,因此Flux并不是Fractal,Dispatcher與Stores可以被看做Orchestrator。
-
用戶事件響應在渲染時聲明:在React的 render() 函數中,即負責響應用戶交互,也負責注冊用戶事件的處理器
下面我們來看一個具體的代碼對比,首先是以經典的Cocoa風格編寫一個簡單的計數器按鈕:
class ModelCounter
constructor: (@value=1) ->
increaseValue: (delta) =>
@value += delta
class ControllerCounter
constructor: (opts) ->
@model_counter = opts.model_counter
@observers = []
getValue: => @model_counter.value
increaseValue: (delta) =>
@model_counter.increaseValue(delta)
@notifyObservers()
notifyObservers: =>
obj.notify(this) for obj in @observers
registerObserver: (observer) =>
@observers.push(observer)
class ViewCounterButton
constructor: (opts) ->
@controller_counter = opts.controller_counter
@button_class = opts.button_class or 'button_counter'
@controller_counter.registerObserver(this)
render: =>
elm = $("<button class=\"#{@button_class}\">
#{@controller_counter.getValue()}</button>")
elm.click =>
@controller_counter.increaseValue(1)
return elm
notify: =>
$("button.#{@button_class}").replaceWith(=> @render())
上述代碼邏輯用上文提及的MVC模式圖演示就是:
而如果用Flux模式實現,會是下面這個樣子:
# Store
class CounterStore extends EventEmitter
constructor: ->
@count = 0
@dispatchToken = @registerToDispatcher()
increaseValue: (delta) ->
@count += 1
getCount: ->
return @count
registerToDispatcher: ->
CounterDispatcher.register((payload) =>
switch payload.type
when ActionTypes.INCREASE_COUNT
@increaseValue(payload.delta)
)
# Action
class CounterActions
@increaseCount: (delta) ->
CounterDispatcher.handleViewAction({
'type': ActionTypes.INCREASE_COUNT
'delta': delta
})
# View
CounterButton = React.createClass(
getInitialState: ->
return {'count': 0}
_onChange: ->
@setState({
count: CounterStore.getCount()
})
componentDidMount: ->
CounterStore.addListener('CHANGE', @_onChange)
componentWillUnmount: ->
CounterStore.removeListener('CHANGE', @_onChange)
render: ->
return React.DOM.button({'className': @prop.class}, @state.value)
)
其數據流圖為:
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中的基本組成部分有:
-
Model:定義狀態數據結構的類型
-
View:純函數,將狀態渲染為界面
-
Actions:以Mailbox的方式傳遞用戶事件的載體
-
Update:用于更新狀態的純函數
根據上述流程,我們可知Elm模式的特性為:
-
到處可見的層次化組合:Redux只是在View層允許將組件進行層次化組合,而MVU中在Model與Update函數中也允許進行層次化組合,甚至Actions都可以包含內嵌的子Action
-
Elm屬于Fractal架構:因為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特性
來自: https://segmentfault.com/a/1190000006016817