GUI应用程序架构的十年变迁:MVC,MVP,MVVM,Unidirectional,Clean
? ? ?十年前,Martin Fowler撰寫了GUI Architectures一文,至今被奉為經(jīng)典。本文所談的所謂架構(gòu)二字,核心即是對(duì)于富客戶端的代碼組織/職責(zé)劃分。縱覽這十年內(nèi)的架構(gòu)模式變遷,大概可以分為MV*與Unidirectional兩大類,而Clean Architecture則是以嚴(yán)格的層次劃分獨(dú)辟蹊徑。從筆者的認(rèn)知來看,從MVC到MVP的變遷完成了對(duì)于View與Model的解耦合,改進(jìn)了職責(zé)分配與可測(cè)試性。而從MVP到MVVM,添加了View與ViewModel之間的數(shù)據(jù)綁定,使得View完全的無狀態(tài)化。最后,整個(gè)從MV*到Unidirectional的變遷即是采用了消息隊(duì)列式的數(shù)據(jù)流驅(qū)動(dòng)的架構(gòu),并且以Redux為代表的方案將原本MV*中碎片化的狀態(tài)管理變?yōu)榱私y(tǒng)一的狀態(tài)管理,保證了狀態(tài)的有序性與可回溯性。
筆者在撰寫本文的時(shí)候也不可避免的帶了很多自己的觀點(diǎn),在漫長的GUI架構(gòu)模式變遷過程中,很多概念其實(shí)是交錯(cuò)復(fù)雜,典型的譬如MVP與MVVM的區(qū)別,筆者按照自己的理解強(qiáng)行定義了二者的區(qū)分邊界,不可避免的帶著自己的主觀想法。另外,鑒于筆者目前主要進(jìn)行的是Web方面的開發(fā),因此在整體傾向上是支持Unidirectional Architecture并且認(rèn)為集中式的狀態(tài)管理是正確的方向。但是必須要強(qiáng)調(diào),GUI架構(gòu)本身是無法脫離其所依托的平臺(tái),下文筆者也會(huì)淺述由于Android與iOS本身SDK API的特殊性,生搬硬套其他平臺(tái)的架構(gòu)模式也是邯鄲學(xué)步,沐猴而冠。不過總結(jié)而言,它山之石,可以攻玉,本身我們所處的開發(fā)環(huán)境一直在不斷變化,對(duì)于過去的精華自當(dāng)應(yīng)該保留,并且與新的環(huán)境相互印證,觸類旁通。
Introduction
Make everything as simple as possible, but not simpler?—?Albert Einstein
Graphical User Interfaces一直是軟件開發(fā)領(lǐng)域的重要組成部分,從當(dāng)年的MFC,到WinForm/Java Swing,再到WebAPP/Android/iOS引領(lǐng)的智能設(shè)備潮流,以及未來可能的AR/VR,GUI應(yīng)用開發(fā)中所面臨的問題一直在不斷演變,但是從各種具體問題中抽象而出的可以復(fù)用的模式恒久存在。而這些模式也就是所謂應(yīng)用架構(gòu)的核心與基礎(chǔ)。對(duì)于所謂應(yīng)用架構(gòu),空談?wù)`事,不談?wù)`己,筆者相信不僅僅只有自己想把那一團(tuán)糟的代碼給徹底拋棄。往往對(duì)于架構(gòu)的認(rèn)知需要一定的大局觀與格局眼光,每個(gè)有一定經(jīng)驗(yàn)的客戶端程序開發(fā)者,無論是Web、iOS還是Android,都會(huì)有自己熟悉的開發(fā)流程習(xí)慣,但是筆者認(rèn)為架構(gòu)認(rèn)知更多的是道,而非術(shù)。當(dāng)你能夠以一種指導(dǎo)思想在不同的平臺(tái)上能夠進(jìn)行高效地開發(fā)時(shí),你才能真正理解架構(gòu)。這個(gè)有點(diǎn)像張三豐學(xué)武,心中無招,方才達(dá)成。筆者這么說只是為了強(qiáng)調(diào),盡量地可以不拘泥于某個(gè)平臺(tái)的具體實(shí)現(xiàn)去審視GUI應(yīng)用程序架構(gòu)模式,會(huì)讓你有不一樣的體驗(yàn)。譬如下面這個(gè)組裝Android機(jī)器人的圖:
怎么去焊接兩個(gè)組件,屬于具體的術(shù)實(shí)現(xiàn),而應(yīng)該焊接哪兩個(gè)組件就是術(shù),作為合格的架構(gòu)師總不能把腳和頭直接焊接在一起,而忽略中間的連接模塊。對(duì)于軟件開發(fā)中任何一個(gè)方面,我們都希望能夠?qū)ふ业揭粋€(gè)抽象程度適中,能夠在接下來的4,5年內(nèi)正常運(yùn)行與方便維護(hù)擴(kuò)展的開發(fā)模式。引申下筆者在我的編程之路中的論述,目前在GUI架構(gòu)模式中,無論是Android、iOS還是Web,都在經(jīng)歷著從命令式編程到聲明式/響應(yīng)式編程,從Passive Components到Reactive Components,從以元素操作為核心到以數(shù)據(jù)流驅(qū)動(dòng)為核心的變遷(關(guān)于這幾句話的解釋可以參閱下文的Declarative vs. Imperative這一小節(jié))。
Terminology:名詞解釋
正文之前,我們先對(duì)一些概念進(jìn)行闡述:
-
User Events/用戶事件:即是來自于可輸入設(shè)備上的用戶操作產(chǎn)生的數(shù)據(jù),譬如鼠標(biāo)點(diǎn)擊、滾動(dòng)、鍵盤輸入、觸摸等等。
-
User Interface Rendering/用戶界面渲染:View這個(gè)名詞在前后端開發(fā)中都被廣泛使用,為了明晰該詞的含義,我們?cè)谶@里使用用戶渲染這個(gè)概念,來描述View,即是以HTML或者JSX或者XAML等等方式在屏幕上產(chǎn)生的圖形化輸出內(nèi)容。
-
UI Application:允許接收用戶輸入,并且將輸出渲染到屏幕上的應(yīng)用程序,該程序能夠長期運(yùn)行而不只是渲染一次即結(jié)束
Passive Module & Reactive Module
箭頭表示的歸屬權(quán)實(shí)際上也是Passive Programming與Reactive Programming的區(qū)別,譬如我們的系統(tǒng)中有Foo與Bar兩個(gè)模塊,可以把它們當(dāng)做OOP中的兩個(gè)類。如果我們?cè)贔oo與Bar之間建立一個(gè)箭頭,也就意味著Foo能夠影響B(tài)ar中的狀態(tài):
譬如Foo在進(jìn)行一次網(wǎng)絡(luò)請(qǐng)求之后將Bar內(nèi)部的計(jì)數(shù)器加一操作:
// This is inside the Foo modulefunction onNetworkRequest() {// ...Bar.incrementCounter();// ... }在這里將這種邏輯關(guān)系可以描述為Foo擁有著網(wǎng)絡(luò)請(qǐng)求完成之后將Bar內(nèi)的計(jì)數(shù)器加一這個(gè)關(guān)系的控制權(quán),也就是Foo占有主導(dǎo)性,而Bar相對(duì)而言是Passive被動(dòng)的:
Bar是Passive的,它允許其他模塊改變其內(nèi)部狀態(tài)。而Foo是主動(dòng)地,它需要保證能夠正確地更新Bar的內(nèi)部狀態(tài),Passive模塊并不知道誰會(huì)更新到它。而另一種方案就是類似于控制反轉(zhuǎn),由Bar完成對(duì)于自己內(nèi)部狀態(tài)的更新:
在這種模式下,Bar監(jiān)聽來自于Foo中的事件,并且在某些事件發(fā)生之后進(jìn)行內(nèi)部狀態(tài)更新:
// This is inside the Bar moduleFoo.addOnNetworkRequestListener(() => {self.incrementCounter(); // self is Bar});此時(shí)Bar就變成了Reactive Module,它負(fù)責(zé)自己的內(nèi)部的狀態(tài)更新以響應(yīng)外部的事件,而Foo并不知道它發(fā)出的事件會(huì)被誰監(jiān)聽。
Declarative vs. Imperative:命令式編程與聲明式編程
three-ds-of-web-development
前端攻略-從路人甲到英雄無敵二:JavaScript 與不斷演化的框架
形象地來描述命令式編程與聲明式編程的區(qū)別,就好像C#/JavaScript與類似于XML或者HTML這樣的標(biāo)記語言之間的區(qū)別。命令式編程關(guān)注于how to do what you want done,即事必躬親,需要安排好每個(gè)要做的細(xì)節(jié)。而聲明式編程關(guān)注于what you want done without worrying about how,即只需要聲明要做的事情而不用將具體的過程再耦合進(jìn)來。對(duì)于開發(fā)者而言,聲明式編程將很多底層的實(shí)現(xiàn)細(xì)節(jié)向開發(fā)者隱藏,而使得開發(fā)者可以專注于具體的業(yè)務(wù)邏輯,同時(shí)也保證了代碼的解耦與單一職責(zé)。譬如在Web開發(fā)中,如果你要基于jQuery將數(shù)據(jù)填充到頁面上,那么大概按照命令式編程的模式你需要這么做:
var options = $("#options"); $.each(result, function() {options.append($("<option />").val(this.id).text(this.name)); });而以Angular 1聲明式的方式進(jìn)行編寫,那么是如下的標(biāo)記模樣:
<div ng-repeat="item in items" ng-click="select(item)">{{item.name}} </div>而在iOS和Android開發(fā)中,近年來函數(shù)響應(yīng)式編程(Functional Reactive Programming)也非常流行,參閱筆者關(guān)于響應(yīng)式編程的介紹可以了解,響應(yīng)式編程本身是基于流的方式對(duì)于異步操作的一種編程優(yōu)化,其在整個(gè)應(yīng)用架構(gòu)的角度看更多的是細(xì)節(jié)點(diǎn)的優(yōu)化。以RxSwift為例,通過響應(yīng)式編程可以編寫出非常優(yōu)雅的用戶交互代碼:
let searchResults = searchBar.rx_text.throttle(0.3, scheduler: MainScheduler.instance).distinctUntilChanged().flatMapLatest { query -> Observable<[Repository]> inif query.isEmpty {return Observable.just([])}return searchGitHub(query).catchErrorJustReturn([])}.observeOn(MainScheduler.instance) searchResults.bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) {(index, repository: Repository, cell) incell.textLabel?.text = repository.namecell.detailTextLabel?.text = repository.url}.addDisposableTo(disposeBag)其直觀的效果大概如下圖所示:
到這里可以看出,無論是從命令式編程與聲明式編程的對(duì)比還是響應(yīng)式編程的使用,我們開發(fā)時(shí)的關(guān)注點(diǎn)都慢慢轉(zhuǎn)向了所謂的數(shù)據(jù)流。便如MVVM,雖然它還是雙向數(shù)據(jù)流,但是其使用的Data-Binding也意味著開發(fā)人員不需要再去以命令地方式尋找元素,而更多地關(guān)注于應(yīng)該給綁定的對(duì)象賦予何值,這也是數(shù)據(jù)流驅(qū)動(dòng)的一個(gè)重要體現(xiàn)。而Unidirectional Architecture采用了類似于Event Source的方式,更是徹底地將組件之間、組件與功能模塊之間的關(guān)聯(lián)交于數(shù)據(jù)流操控。
談到架構(gòu),我們關(guān)心哪些方面?
當(dāng)我們談?wù)撍^客戶端開發(fā)的時(shí)候,我們首先會(huì)想到怎么保證向后兼容、怎么使用本地存儲(chǔ)、怎么調(diào)用遠(yuǎn)程接口、如何有效地利用內(nèi)存/帶寬/CPU等資源,不過最核心的還是怎么繪制界面并且與用戶進(jìn)行交互,關(guān)于這部分詳細(xì)的知識(shí)點(diǎn)綱要推薦參考筆者的我的編程之路——知識(shí)管理與知識(shí)體系這篇文章或者這張知識(shí)點(diǎn)列表思維腦圖。
而當(dāng)我們提綱挈領(lǐng)、高屋建瓴地以一個(gè)較高的抽象的視角來審視總結(jié)這個(gè)知識(shí)點(diǎn)的時(shí)候會(huì)發(fā)現(xiàn),我們希望的好的架構(gòu),便如在引言中所說,即是有好的代碼組織方式/合理的職責(zé)劃分粒度。筆者腦中會(huì)出現(xiàn)如下這樣的一個(gè)層次結(jié)構(gòu),可以看出,最核心的即為View與ViewLogic這兩部分:
實(shí)際上,對(duì)于富客戶端的代碼組織/職責(zé)劃分,從具體的代碼分割的角度,即是功能的模塊化、界面的組件化、狀態(tài)管理這三個(gè)方面。最終呈獻(xiàn)給用戶的界面,筆者認(rèn)為可以抽象為如下等式:View = f(State,Template)。而ViewLogic中對(duì)于類/模塊之間的依賴關(guān)系,即屬于代碼組織,譬如MVC中的View與Controller之間的從屬關(guān)系。而對(duì)于動(dòng)態(tài)數(shù)據(jù),即所謂應(yīng)用數(shù)據(jù)的管理,屬于狀態(tài)管理這一部分,譬如APP從后來獲取了一系列的數(shù)據(jù),如何將這些數(shù)據(jù)渲染到用戶界面上使得用戶可見,這樣的不同部分之間的協(xié)同關(guān)系、整個(gè)數(shù)據(jù)流的流動(dòng),即屬于狀態(tài)管理。
分久必合,合久必分
實(shí)際上從MVC、MVP到MVVM,一直圍繞的核心問題就是如何分割ViewLogic與View,即如何將負(fù)責(zé)界面展示的代碼與負(fù)責(zé)業(yè)務(wù)邏輯的代碼進(jìn)行分割。所謂分久必合,合久必分,從筆者自我審視的角度,發(fā)現(xiàn)很有趣的一點(diǎn)。Android與iOS中都是從早期的用代碼進(jìn)行組件添加與布局到專門的XML/Nib/StoryBoard文件進(jìn)行布局,Android中的Annotation/DataBinding、iOS中的IBOutlet更加地保證了View與ViewLogic的分割(這一點(diǎn)也是從元素操作到以數(shù)據(jù)流驅(qū)動(dòng)的變遷,我們不需要再去編寫大量的findViewById)。而Web的趨勢(shì)正好有點(diǎn)相反,無論是WebComponent還是ReactiveComponent都是將ViewLogic與View置于一起,特別是JSX的語法將JavaScript與HTML混搭,很像當(dāng)年的PHP/JSP與HTML混搭。這一點(diǎn)也是由筆者在上文提及的Android/iOS本身封裝程度較高的、規(guī)范的API決定的。對(duì)于Android/iOS與Web之間開發(fā)體驗(yàn)的差異,筆者感覺很類似于靜態(tài)類型語言與動(dòng)態(tài)類型語言之間的差異。
功能的模塊化
老實(shí)說在AMD/CMD規(guī)范之前,或者說在ES6的模塊引入與Webpack的模塊打包出來之前,功能的模塊化依賴一直也是個(gè)很頭疼的問題。
SOLID中的接口隔離原則,大量的IOC或者DI工具可以幫我們完成這一點(diǎn),就好像Spring中的@Autowire或者Angular 1中的@Injection,都給筆者很好地代碼體驗(yàn)。
在這里筆者首先要強(qiáng)調(diào)下,從代碼組織的角度來看,項(xiàng)目的構(gòu)建工具與依賴管理工具會(huì)深刻地影響到代碼組織,這一點(diǎn)在功能的模塊化中尤其顯著。譬如筆者對(duì)于Android/Java構(gòu)建工具的使用變遷經(jīng)歷了從Eclipse到Maven再到Gradle,筆者會(huì)將不同功能邏輯的代碼封裝到不同的相對(duì)獨(dú)立的子項(xiàng)目中,這樣就保證了子項(xiàng)目與主項(xiàng)目之間的一定隔離,方便了測(cè)試與代碼維護(hù)。同樣的,在Web開發(fā)中從AMD/CMD規(guī)范到標(biāo)準(zhǔn)的ES6模塊與Webpack編譯打包,也使得代碼能夠按照功能盡可能地解耦分割與避免冗余編碼。而另一方面,依賴管理工具也極大地方便我們使用第三方的代碼與發(fā)布自定義的依賴項(xiàng),譬如Web中的NPM與Bower,iOS中的CocoaPods都是十分優(yōu)秀的依賴發(fā)布與管理工具,使我們不需要去關(guān)心第三方依賴的具體實(shí)現(xiàn)細(xì)節(jié)即能夠透明地引入使用。因此選擇合適的項(xiàng)目構(gòu)建工具與依賴管理工具也是好的GUI架構(gòu)模式的重要因素之一。不過從應(yīng)用程序架構(gòu)的角度看,無論我們使用怎樣的構(gòu)建工具,都可以實(shí)現(xiàn)或者遵循某種架構(gòu)模式,筆者認(rèn)為二者之間也并沒有必然的因果關(guān)系。
界面的組件化
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.
何謂組件?一個(gè)組件即是應(yīng)用中用戶交互界面的部分組成,組件可以通過組合封裝成更高級(jí)的組件。組件可以被放入層次化的結(jié)構(gòu)中,即可以是其他組件的父組件也可以是其他組件的子組件。根據(jù)上述的組件定義,筆者認(rèn)為像Activity或者UIViewController都不能算是組件,而像ListView或者UITableView可以看做典型的組件。
我們強(qiáng)調(diào)的是界面組件的Composable&Reusable,即可組合性與可重用性。當(dāng)我們一開始接觸到Android或者iOS時(shí),因?yàn)楸旧鞸DK的完善度與規(guī)范度較高,我們能夠很多使用封裝程度較高的組件。譬如ListView,無論是Android中的RecycleView還是iOS中的UITableView或者UICollectionView,都為我們提供了。凡事都有雙面性,這種較高程度的封裝與規(guī)范統(tǒng)一的API方便了我們的開發(fā),但是也限制了我們自定義的能力。同樣的,因?yàn)镾DK的限制,真正意義上可復(fù)用/組合的組件也是不多,譬如你不能將兩個(gè)ListView再組合成一個(gè)新的ListView。在React中有所謂的controller-view的概念,即意味著某個(gè)React組件同時(shí)擔(dān)負(fù)起MVC中Controller與View的責(zé)任,也就是JSX這種將負(fù)責(zé)ViewLogic的JavaScript代碼與負(fù)責(zé)模板的HTML混編的方式。
界面的組件化還包括一個(gè)重要的點(diǎn)就是路由,譬如Android中的AndRouter、iOS中的JLRoutes都是集中式路由的解決方案,不過集中式路由在Android或者iOS中并沒有大規(guī)模推廣。iOS中的StoryBoard倒是類似于一種集中式路由的方案,不過更偏向于以UI設(shè)計(jì)為核心。筆者認(rèn)為這一點(diǎn)可能是因?yàn)锳ndroid或者iOS本身所有的代碼都是存放于客戶端本身,而Web中較傳統(tǒng)的多頁應(yīng)用方式還需要用戶跳轉(zhuǎn)頁面重新加載,而后在單頁流行之后即不存在頁面級(jí)別的跳轉(zhuǎn),因此在Web單頁應(yīng)用中集中式路由較為流行而Android、iOS中反而不流行。
無狀態(tài)的組件
無狀態(tài)的組件的構(gòu)建函數(shù)是純函數(shù)(pure function)并且引用透明的(refferentially transparent),在相同輸入的情況下一定會(huì)產(chǎn)生相同的組件輸出,即符合View = f(State,Template)公式。筆者覺得Android中的ListView/RecycleView,或者iOS中的UITableView,也是無狀態(tài)組件的典型。譬如在Android中,可以通過動(dòng)態(tài)設(shè)置Adapter實(shí)例來為RecycleView進(jìn)行源數(shù)據(jù)的設(shè)置,而作為View層以IoC的方式與具體的數(shù)據(jù)邏輯解耦。
組件的可組合性與可重用性往往最大的阻礙就是狀態(tài),一般來說,我們希望能夠重用或者組合的組件都是
Generalization,而狀態(tài)往往是Specification,即領(lǐng)域特定的。同時(shí),狀態(tài)也會(huì)使得代碼的可讀性與可測(cè)試性降低,在有狀態(tài)的組件中,我們并不能通過簡單地閱讀代碼就知道其功能。如果借用函數(shù)式編程的概念,就是因?yàn)楦弊饔玫囊胧沟煤瘮?shù)每次回產(chǎn)生不同的結(jié)果。函數(shù)式編程中存在著所謂Pure Function,即純函數(shù)的概念,函數(shù)的返回值永遠(yuǎn)只受到輸入?yún)?shù)的影響。譬如(x)=>x*2這個(gè)函數(shù),輸入的x值永遠(yuǎn)不會(huì)被改變,并且返回值只是依賴于輸入的參數(shù)。而Web開發(fā)中我們也經(jīng)常會(huì)處于帶有狀態(tài)與副作用的環(huán)境,典型的就是Browser中的DOM,之前在jQuery時(shí)代我們會(huì)經(jīng)常將一些數(shù)據(jù)信息緩存在DOM樹上,也是典型的將狀態(tài)與模板混合的用法。這就導(dǎo)致了我們并不能控制到底應(yīng)該何時(shí)去進(jìn)行重新渲染以及哪些狀態(tài)變更的操作才是必須的,
var Header = component(function (data) {// First argument is h1 metadatareturn 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' });狀態(tài)管理
可變的與不可預(yù)測(cè)的狀態(tài)是軟件開發(fā)中的萬惡之源
-
Web開發(fā)中所謂狀態(tài)淺析:Domain State&UI State
上文提及,我們盡可能地希望組件的無狀態(tài)性,那么整個(gè)應(yīng)用中的狀態(tài)管理應(yīng)該盡量地放置在所謂High-Order Component或者Smart Component中。在React以及Flux的概念流行之后,Stateless Component的概念深入人心,不過其實(shí)對(duì)于MVVM中的View,也是無狀態(tài)的View。通過雙向數(shù)據(jù)綁定將界面上的某個(gè)元素與ViewModel中的變量相關(guān)聯(lián),筆者認(rèn)為很類似于HOC模式中的Container與Component之間的關(guān)聯(lián)。隨著應(yīng)用的界面與功能的擴(kuò)展,狀態(tài)管理會(huì)變得愈發(fā)混亂。這一點(diǎn),無論前后端都有異曲同工之難,筆者在基于Redux思想與RxJava的SpringMVC中Controller的代碼風(fēng)格實(shí)踐一文中對(duì)于服務(wù)端應(yīng)用程序開發(fā)中的狀態(tài)管理有過些許討論。
Features of Good Architectural Pattern:何為好的架構(gòu)模式
Balanced Distribution of Responsibilities:合理的職責(zé)劃分
合理的職責(zé)劃分即是保證系統(tǒng)中的不同組件能夠被分配合理的職責(zé),也就是在復(fù)雜度之間達(dá)成一個(gè)平衡,職責(zé)劃分最權(quán)威的原則就是所謂Single Responsibility Principle,單一職責(zé)原則。
Testability:可測(cè)試性
可測(cè)試性是保證軟件工程質(zhì)量的重要手段之一,也是保證產(chǎn)品可用性的重要途徑。在傳統(tǒng)的GUI程序開發(fā)中,特別是對(duì)于界面的測(cè)試常常設(shè)置于狀態(tài)或者運(yùn)行環(huán)境,并且很多與用戶交互相關(guān)的測(cè)試很難進(jìn)行場(chǎng)景重現(xiàn),或者需要大量的人工操作去模擬真實(shí)環(huán)境。
Ease of Use:易用性
代碼的易用性保證了程序架構(gòu)的簡潔與可維護(hù)性,所謂最好的代碼就是永遠(yuǎn)不需要重寫的代碼,而程序開發(fā)中盡量避免的代碼復(fù)用方法就是復(fù)制粘貼。
Fractal:碎片化,易于封裝與分發(fā)
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,即你的應(yīng)用整體都可以像單個(gè)組件一樣可以方便地進(jìn)行打包然后應(yīng)用到其他項(xiàng)目中。而在Non-Fractal Architectures中,不可以被重復(fù)使用的部分被稱為層次化組合中的Orchestrators。譬如你在Web中編寫了一個(gè)登錄表單,其中的布局、樣式等部分可以被直接復(fù)用,而提交表單這個(gè)操作,因?yàn)榫哂袘?yīng)用特定性,因此需要在不同的應(yīng)用中具有不同的實(shí)現(xiàn)。譬如下面有一個(gè)簡單的表單:
<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>因?yàn)椴煌膽?yīng)用中,form的提交地址可能不一致,那么整個(gè)form組件是不可直接重用的,即Non-Fractal Architectures。而form中的input組件是可以進(jìn)行直接復(fù)用的,如果將input看做一個(gè)單獨(dú)的GUI架構(gòu),即是所謂的Fractal Architectures,form就是所謂的Orchestrators,將可重用的組件編排組合,并且設(shè)置應(yīng)用特定的一些信息。
Reference
Overview
-
Martin Fowler-GUI Architectures
-
Comparison-of-Architecture-presentation-patterns
MV*
-
THE EVOLUTION OF ANDROID ARCHITECTURE
-
the-evolution-of-android-architecture
-
android-architecture
-
ios-architecture-patterns
-
Albert Zuurbier:MVC VS. MVP VS. MVVM
MVC
-
Model-View-Controller (MVC) in iOS: A Modern Approach
-
為什么我不再使用MVC框架
-
difference-between-mvc-mvp-mvvm-swapneel-salunkhe
MVP
-
presentation-model-and-passive-view-in-mvp-the-android-way
-
Repository that showcases 3 Android app architectures
MVVM
-
approaching-android-with-mvvm
Unidirectional Architecture
-
unidirectional-user-interface-architectures
-
[Facebook: MVC Does Not Scale, Use Flux Instead [Updated]](https://www.infoq.com/news/20...
-
mvvm-mvc-is-dead-is-unidirectional-a-mvvm-mvc-killer
-
flux-vs-mvc-design-patterns
-
jedux:Redux architecture for Android
-
writing-a-todo-app-with-redux-on-android
-
state-streams-and-react
Viper/Clean Architecture
-
Uncle Bob:the-clean-architecture
-
Android Clean Architecture
-
A sample iOS app built using the Clean Swift architecture
-
Introduction to VIPER
MV*:Fragmentary State 碎片化的狀態(tài)與雙向數(shù)據(jù)流
MVC模式將有關(guān)于渲染、控制與數(shù)據(jù)存儲(chǔ)的概念有機(jī)分割,是GUI應(yīng)用架構(gòu)模式的一個(gè)巨大成就。但是,MVC模式在構(gòu)建能夠長期運(yùn)行、維護(hù)、有效擴(kuò)展的應(yīng)用程序時(shí)遇到了極大的問題。MVC模式在一些小型項(xiàng)目或者簡單的界面上仍舊有極大的可用性,但是在現(xiàn)代富客戶端開發(fā)中導(dǎo)致職責(zé)分割不明確、功能模塊重用性、View的組合性較差。作為繼任者M(jìn)VP模式分割了View與Model之間的直接關(guān)聯(lián),MVP模式中也將更多的ViewLogic轉(zhuǎn)移到Presenter中進(jìn)行實(shí)現(xiàn),從而保證了View的可測(cè)試性。而最年輕的MVVM將ViewLogic與View剝離開來,保證了View的無狀態(tài)性、可重用性、可組合性以及可測(cè)試性。總結(jié)而言,MV*模型都包含了以下幾個(gè)方面:
-
Models:負(fù)責(zé)存儲(chǔ)領(lǐng)域/業(yè)務(wù)邏輯相關(guān)的數(shù)據(jù)與構(gòu)建數(shù)據(jù)訪問層,典型的就是譬如Person、PersonDataProvider。
-
Views:負(fù)責(zé)將數(shù)據(jù)渲染展示給用戶,并且響應(yīng)用戶輸入
-
Controller/Presenter/ViewModel:往往作為Model與View之間的中間人出現(xiàn),接收View傳來的用戶事件并且傳遞給Model,同時(shí)利用從Model傳來的最新模型控制更新View
MVC:Monolithic Controller
相信每一個(gè)程序猿都會(huì)宣稱自己掌握MVC,這個(gè)概念淺顯易懂,并且貫穿了從GUI應(yīng)用到服務(wù)端應(yīng)用程序。MVC的概念源自Gamma, Helm, Johnson 以及Vlissidis這四人幫在討論設(shè)計(jì)模式中的Observer模式時(shí)的想法,不過在那本經(jīng)典的設(shè)計(jì)模式中并沒有顯式地提出這個(gè)概念。我們通常認(rèn)為的MVC名詞的正式提出是在1979年5月Trygve Reenskaug發(fā)表的Thing-Model-View-Editor這篇論文,這篇論文雖然并沒有提及Controller,但是Editor已經(jīng)是一個(gè)很接近的概念。大概7個(gè)月之后,Trygve Reenskaug在他的文章Models-Views-Controllers中正式提出了MVC這個(gè)三元組。上面兩篇論文中對(duì)于Model的定義都非常清晰,Model代表著an abstraction in the form of data in a computing system.,即為計(jì)算系統(tǒng)中數(shù)據(jù)的抽象表述,而View代表著capable of showing one or more pictorial representations of the Model on screen and on hardcopy.,即能夠?qū)⒛P椭械臄?shù)據(jù)以某種方式表現(xiàn)在屏幕上的組件。而Editor被定義為某個(gè)用戶與多個(gè)View之間的交互接口,在后一篇文章中Controller則被定義為了a special controller ... that permits the user to modify the information that is presented by the view.,即主要負(fù)責(zé)對(duì)模型進(jìn)行修改并且最終呈現(xiàn)在界面上。從我的個(gè)人理解來看,Controller負(fù)責(zé)控制整個(gè)界面,而Editor只負(fù)責(zé)界面中的某個(gè)部分。Controller協(xié)調(diào)菜單、面板以及像鼠標(biāo)點(diǎn)擊、移動(dòng)、手勢(shì)等等很多的不同功能的模塊,而Editor更多的只是負(fù)責(zé)某個(gè)特定的任務(wù)。后來,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的功能正式定義為:響應(yīng)用戶操作,控制模型進(jìn)行相應(yīng)更新,并且操作頁面進(jìn)行合適的重渲染。這是非常經(jīng)典、狹義的MVC定義,后來在iOS以及其他很多領(lǐng)域?qū)嶋H上運(yùn)用的MVC都已經(jīng)被擴(kuò)展或者賦予了新的功能,不過筆者為了區(qū)分架構(gòu)演化之間的區(qū)別,在本文中僅會(huì)以這種最樸素的定義方式來描述MVC。
根據(jù)上述定義,我們可以看到MVC模式中典型的用戶場(chǎng)景為:
-
用戶交互輸入了某些內(nèi)容
-
Controller將用戶輸入轉(zhuǎn)化為Model所需要進(jìn)行的更改
-
Model中的更改結(jié)束之后,Controller通知View進(jìn)行更新以表現(xiàn)出當(dāng)前Model的狀態(tài)
根據(jù)上述流程,我們可知經(jīng)典的MVC模式的特性為:
-
View、Controller、Model中皆有ViewLogic的部分實(shí)現(xiàn)
-
Controller負(fù)責(zé)控制View與Model,需要了解View與Model的細(xì)節(jié)。
-
View需要了解Controller與Model的細(xì)節(jié),需要在偵測(cè)用戶行為之后調(diào)用Controller,并且在收到通知后調(diào)用Model以獲取最新數(shù)據(jù)
-
Model并不需要了解Controller與View的細(xì)節(jié),相對(duì)獨(dú)立的模塊
Observer Pattern:自帶觀察者模式的MVC
上文中也已提及,MVC濫觴于Observer模式,經(jīng)典的MVC模式也可以與Observer模式相結(jié)合,其典型的用戶流程為:
-
用戶交互輸入了某些內(nèi)容
-
Controller將用戶輸入轉(zhuǎn)化為Model所需要進(jìn)行的更改
-
View作為Observer會(huì)監(jiān)聽Model中的任意更新,一旦有更新事件發(fā)出,View會(huì)自動(dòng)觸發(fā)更新以展示最新的Model狀態(tài)
可知其與經(jīng)典的MVC模式區(qū)別在于不需要Controller通知View進(jìn)行更新,而是由Model主動(dòng)調(diào)用View進(jìn)行更新。這種改變提升了整體效率,簡化了Controller的功能,不過也導(dǎo)致了View與Model之間的緊耦合。
MVP:Decoupling View and Model 將視圖與模型解耦, View<->Presenter
維基百科將MVP稱為MVC的一個(gè)推導(dǎo)擴(kuò)展,觀其淵源而知其所以然。對(duì)于MVP概念的定義,Microsoft較為明晰,而Martin Fowler的定義最為廣泛接受。MVP模式在WinForm系列以Visual-XXX命名的編程語言與Java Swing等系列應(yīng)用中最早流傳開來,不過后來ASP.NET以及JFaces也廣泛地使用了該模式。在MVP中用戶不再與Presenter進(jìn)行直接交互,而是由View完全接管了用戶交互,譬如窗口上的每個(gè)控件都知道如何響應(yīng)用戶輸入并且合適地渲染來自于Model的數(shù)據(jù)。而所有的事件會(huì)被傳輸給Presenter,Presenter在這里就是View與Model之間的中間人,負(fù)責(zé)控制Model進(jìn)行修改以及將最新的Model狀態(tài)傳遞給View。這里描述的就是典型的所謂Passive View版本的MVP,其典型的用戶場(chǎng)景為:
-
用戶交互輸入了某些內(nèi)容
-
View將用戶輸入轉(zhuǎn)化為發(fā)送給Presenter
-
Presenter控制Model接收需要改變的點(diǎn)
-
Model將更新之后的值返回給Presenter
-
Presenter將更新之后的模型返回給View
根據(jù)上述流程,我們可知Passive View版本的MVP模式的特性為:
-
View、Presenter、Model中皆有ViewLogic的部分實(shí)現(xiàn)
-
Presenter負(fù)責(zé)連接View與Model,需要了解View與Model的細(xì)節(jié)。
-
View需要了解Presenter的細(xì)節(jié),將用戶輸入轉(zhuǎn)化為事件傳遞給Presenter
-
Model需要了解Presenter的細(xì)節(jié),在完成更新之后將最新的模型傳遞給Presenter
-
View與Model之間相互解耦合
Supervising Controller MVP
簡化Presenter的部分功能,使得Presenter只起到需要復(fù)雜控制或者調(diào)解的操作,而簡單的Model展示轉(zhuǎn)化直接由View與Model進(jìn)行交互:
MVVM:Data Binding & Stateless View 數(shù)據(jù)綁定與無狀態(tài)的View,View<->ViewModels
Model View View-Model模型是MV*家族中最年輕的一位,也是由Microsoft提出,并經(jīng)由Martin Fowler布道傳播。MVVM源于Martin Fowler的Presentation Model,Presentation Model的核心在于接管了View所有的行為響應(yīng),View的所有響應(yīng)與狀態(tài)都定義在了Presentation Model中。也就是說,View不會(huì)包含任意的狀態(tài)。舉個(gè)典型的使用場(chǎng)景,當(dāng)用戶點(diǎn)擊某個(gè)按鈕之后,狀態(tài)信息是從Presentation Model傳遞給Model,而不是從View傳遞給Presentation Model。任何控制組件間的邏輯操作,即上文所述的ViewLogic,都應(yīng)該放置在Presentation Model中進(jìn)行處理,而不是在View層,這一點(diǎn)也是MVP模式與Presentation Model最大的區(qū)別。
MVVM模式進(jìn)一步深化了Presentation Model的思想,利用Data Binding等技術(shù)保證了View中不會(huì)存儲(chǔ)任何的狀態(tài)或者邏輯操作。在WPF中,UI主要是利用XAML或者XML創(chuàng)建,而這些標(biāo)記類型的語言是無法存儲(chǔ)任何狀態(tài)的,就像HTML一樣(因此JSX語法其實(shí)是將View又有狀態(tài)化了),只是允許UI與某個(gè)ViewModel中的類建立映射關(guān)系。渲染引擎根據(jù)XAML中的聲明以及來自于ViewModel的數(shù)據(jù)最終生成呈現(xiàn)的頁面。因?yàn)閿?shù)據(jù)綁定的特性,有時(shí)候MVVM也會(huì)被稱作MVB:Model View Binder。總結(jié)一下,MVVM利用數(shù)據(jù)綁定徹底完成了從命令式編程到聲明式編程的轉(zhuǎn)化,使得View逐步無狀態(tài)化。一個(gè)典型的MVVM的使用場(chǎng)景為:
-
用戶交互輸入
-
View將數(shù)據(jù)直接傳送給ViewModel,ViewModel保存這些狀態(tài)數(shù)據(jù)
-
在有需要的情況下,ViewModel會(huì)將數(shù)據(jù)傳送給Model
-
Model在更新完成之后通知ViewModel
-
ViewModel從Model中獲取最新的模型,并且更新自己的數(shù)據(jù)狀態(tài)
-
View根據(jù)最新的ViewModel的數(shù)據(jù)進(jìn)行重新渲染
根據(jù)上述流程,我們可知MVVM模式的特性為:
-
ViewModel、Model中存在ViewLogic實(shí)現(xiàn),View則不保存任何狀態(tài)信息
-
View不需要了解ViewModel的實(shí)現(xiàn)細(xì)節(jié),但是會(huì)聲明自己所需要的數(shù)據(jù)類型,并且能夠知道如何重新渲染
-
ViewModel不需要了解View的實(shí)現(xiàn)細(xì)節(jié)(非命令式編程),但是需要根據(jù)View聲明的數(shù)據(jù)類型傳入對(duì)應(yīng)的數(shù)據(jù)。ViewModel需要了解Model的實(shí)現(xiàn)細(xì)節(jié)。
-
Model不需要了解View的實(shí)現(xiàn)細(xì)節(jié),需要了解ViewModel的實(shí)現(xiàn)細(xì)節(jié)
MV* in iOS
MVC
Cocoa MVC中往往會(huì)將大量的邏輯代碼放入ViewController中,這就導(dǎo)致了所謂的Massive ViewController,而且很多的邏輯操作都嵌入到了View的生命周期中,很難剝離開來。或許你可以將一些業(yè)務(wù)邏輯或者數(shù)據(jù)轉(zhuǎn)換之類的事情放到Model中完成,不過對(duì)于View而言絕大部分時(shí)間僅起到發(fā)送Action給Controller的作用。ViewController逐漸變成了幾乎所有其他組件的Delegate與DataSource,還經(jīng)常會(huì)負(fù)責(zé)派發(fā)或者取消網(wǎng)絡(luò)請(qǐng)求等等職責(zé)。你的代碼大概是這樣的:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCelluserCell.configureWithUser(user)上面這種寫法直接將View于Model關(guān)聯(lián)起來,其實(shí)算是打破了Cocoa MVC的規(guī)范的,不過這樣也是能夠減少些Controller中的中轉(zhuǎn)代碼呢。這樣一個(gè)架構(gòu)模式在進(jìn)行單元測(cè)試的時(shí)候就顯得麻煩了,因?yàn)槟愕腣iewController與View緊密關(guān)聯(lián),使得其很難去進(jìn)行測(cè)試,因?yàn)槟惚仨殲槊恳粋€(gè)View創(chuàng)建Mock對(duì)象并且管理其生命周期。另外因?yàn)檎麄€(gè)代碼都混雜在一起,即破壞了職責(zé)分離原則,導(dǎo)致了系統(tǒng)的可變性與可維護(hù)性也很差。經(jīng)典的MVC的示例程序如下:
import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}class GreetingViewController : UIViewController { // View + Controllervar 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.lastNameself.greetingLabel.text = greeting}// layout code goes here}// Assembling of MVClet model = Person(firstName: "David", lastName: "Blaine")let view = GreetingViewController()view.person = model;上面這種代碼一看就很難測(cè)試,我們可以將生成greeting的代碼移到GreetingModel這個(gè)單獨(dú)的類中,從而進(jìn)行單獨(dú)的測(cè)試。不過我們還是很難去在GreetingViewController中測(cè)試顯示邏輯而不調(diào)用UIView相關(guān)的譬如viewDidLoad、didTapButton等等較為費(fèi)時(shí)的操作。再按照我們上文提及的優(yōu)秀的架構(gòu)的幾個(gè)方面來看:
-
Distribution:View與Model是分割開來了,不過View與Controller是緊耦合的
-
Testability:因?yàn)檩^差的職責(zé)分割導(dǎo)致貌似只有Model部分方便測(cè)試
-
易用性:因?yàn)槌绦虮容^直觀,可能容易理解。
MVP
Cocoa中MVP模式是將ViewController當(dāng)做純粹的View進(jìn)行處理,而將很多的ViewLogic與模型操作移動(dòng)到Presenter中進(jìn)行,代碼如下:
import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}protocol GreetingView: class {func setGreeting(greeting: String)}protocol GreetingViewPresenter {init(view: GreetingView, person: Person)func showGreeting()}class GreetingPresenter : GreetingViewPresenter {unowned let view: GreetingViewlet person: Personrequired init(view: GreetingView, person: Person) {self.view = viewself.person = person}func showGreeting() {let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastNameself.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 MVPlet model = Person(firstName: "David", lastName: "Blaine")let view = GreetingViewController()let presenter = GreetingPresenter(view: view, person: model)view.presenter = presenter-
Distribution:主要的業(yè)務(wù)邏輯分割在了Presenter與Model中,View相對(duì)呆板一點(diǎn)
-
Testability:較為方便地測(cè)試
-
易用性:代碼職責(zé)分割的更為明顯,不過不像MVC那樣直觀易懂了
MVVM
import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}protocol GreetingViewModelProtocol: class {var greeting: String? { get }var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did changeinit(person: Person)func showGreeting()}class GreetingViewModel : GreetingViewModelProtocol {let person: Personvar 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 inself.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-
Distribution:在Cocoa MVVM中,View相對(duì)于MVP中的View擔(dān)負(fù)了更多的功能,譬如需要構(gòu)建數(shù)據(jù)綁定等等
-
Testability:ViewModel擁有View中的所有數(shù)據(jù)結(jié)構(gòu),因此很容易就可以進(jìn)行測(cè)試
-
易用性:相對(duì)而言有很多的冗余代碼
MV* in Android
此部分完整代碼在這里,筆者在這里節(jié)選出部分代碼方便對(duì)照演示。Android中的Activity的功能很類似于iOS中的UIViewController,都可以看做MVC中的Controller。在2010年左右經(jīng)典的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年左右出現(xiàn)了ButterKnife這樣的基于注解的控件綁定框架,此時(shí)的代碼看上去是這樣的:
@Bind(R.id.tv_clicks) mCounterText;@OnClick(R.id.btn_increment)public void onSubmitClicked(View v) {mClicks++;mCounterText.setText("" + mClicks);}后來Google官方也推出了數(shù)據(jù)綁定的框架,從此MVVM模式在Android中也愈發(fā)流行:
<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><LinearLayoutandroid: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啟發(fā)的組件式框架以及Jedux這樣借鑒了Redux全局狀態(tài)管理的框架也將Unidirectional 架構(gòu)引入了Android開發(fā)的世界。
MVC
-
聲明View中的組件對(duì)象或者M(jìn)odel對(duì)象
-
將組件與Activity中對(duì)象綁定,并且聲明用戶響應(yīng)處理函數(shù)
-
用戶輸入之后的更新流程
MVP
-
將Presenter與View綁定,并且將用戶響應(yīng)事件綁定到Presenter中
-
Presenter中調(diào)用Model更新數(shù)據(jù),并且調(diào)用View中進(jìn)行重新渲染
MVVM
-
XML中聲明數(shù)據(jù)綁定
-
View中綁定ViewModel
-
ViewModel中進(jìn)行數(shù)據(jù)操作
Unidirectional User Interface Architecture:單向數(shù)據(jù)流
Unidirectional User Interface Architecture架構(gòu)的概念源于后端常見的CROS/Event Sourcing模式,其核心思想即是將應(yīng)用狀態(tài)被統(tǒng)一存放在一個(gè)或多個(gè)的Store中,并且所有的數(shù)據(jù)更新都是通過可觀測(cè)的Actions觸發(fā),而所有的View都是基于Store中的狀態(tài)渲染而來。該架構(gòu)的最大優(yōu)勢(shì)在于整個(gè)應(yīng)用中的數(shù)據(jù)流以單向流動(dòng)的方式從而使得有用更好地可預(yù)測(cè)性與可控性,這樣可以保證你的應(yīng)用各個(gè)模塊之間的松耦合性。與MVVM模式相比,其解決了以下兩個(gè)問題:
-
避免了數(shù)據(jù)在多個(gè)ViewModel中的冗余與不一致問題
-
分割了ViewModel的職責(zé),使得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.
-
easier-reasoning-with-unidirectional-dataflow-and-immutable-data
Facebook強(qiáng)調(diào),雙向數(shù)據(jù)綁定極不利于代碼的擴(kuò)展與維護(hù)。
從具體的代碼實(shí)現(xiàn)角度來看,雙向數(shù)據(jù)綁定會(huì)導(dǎo)致更改的不可預(yù)期性(UnPredictable),就好像Angular利用Dirty Checking來進(jìn)行是否需要重新渲染的檢測(cè),這導(dǎo)致了應(yīng)用的緩慢,簡直就是來砸場(chǎng)子的。而在采用了單向數(shù)據(jù)流之后,整個(gè)應(yīng)用狀態(tài)會(huì)變得可預(yù)測(cè)(Predictable),也能很好地了解當(dāng)狀態(tài)發(fā)生變化時(shí)到底會(huì)有多少的組件發(fā)生變化。另一方面,相對(duì)集中地狀態(tài)管理,也有助于你不同的組件之間進(jìn)行信息交互或者狀態(tài)共享,特別是像Redux這種強(qiáng)調(diào)Single Store與SIngle State Tree的狀態(tài)管理模式,能夠保證以統(tǒng)一的方式對(duì)于應(yīng)用的狀態(tài)進(jìn)行修改,并且Immutable的概念引入使得狀態(tài)變得可回溯。
譬如Facebook在Flux Overview中舉的例子,當(dāng)我們希望在一個(gè)界面上同時(shí)展示未讀信息列表與未讀信息的總數(shù)目的時(shí)候,對(duì)于MV*就有點(diǎn)惡心了,特別是當(dāng)這兩個(gè)組件不在同一個(gè)ViewModel/Controller中的時(shí)候。一旦我們將某個(gè)未讀信息標(biāo)識(shí)為已讀,會(huì)引起控制已讀信息、未讀信息、未讀信息總數(shù)目等等一系列模型的更新。特別是很多時(shí)候?yàn)榱朔奖阄覀兛赡茉诿總€(gè)ViewModel/Controller都會(huì)設(shè)置一個(gè)數(shù)據(jù)副本,這會(huì)導(dǎo)致依賴連鎖更新,最終導(dǎo)致不可預(yù)測(cè)的結(jié)果與性能損耗。而在Flux中這種依賴是反轉(zhuǎn)的,Store接收到更新的Action請(qǐng)求之后對(duì)數(shù)據(jù)進(jìn)行統(tǒng)一的更新并且通知各個(gè)View,而不是依賴于各個(gè)獨(dú)立的ViewModel/Controller所謂的一致性更新。從職責(zé)劃分的角度來看,除了Store之外的任何模塊其實(shí)都不知道應(yīng)該如何處理數(shù)據(jù),這就保證了合理的職責(zé)分割。這種模式下,當(dāng)我們創(chuàng)建新項(xiàng)目時(shí),項(xiàng)目復(fù)雜度的增長瓶頸也就會(huì)更高,不同于傳統(tǒng)的View與ViewLogic之間的綁定,控制流被獨(dú)立處理,當(dāng)我們添加新的特性,新的數(shù)據(jù),新的界面,新的邏輯處理模塊時(shí),并不會(huì)導(dǎo)致原有模塊的復(fù)雜度增加,從而使得整個(gè)邏輯更加清晰可控。
這里還需要提及一下,很多人應(yīng)該是從React開始認(rèn)知到單向數(shù)據(jù)流這種架構(gòu)模式的,而當(dāng)時(shí)Angular 1的緩慢與性能之差令人發(fā)指,但是譬如Vue與Angular 2的性能就非常優(yōu)秀。借用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.
總而言之,筆者認(rèn)為雙向數(shù)據(jù)流與單向數(shù)據(jù)流相比,性能上孰優(yōu)孰劣尚無定論,最大的區(qū)別在于單向數(shù)據(jù)流與雙向數(shù)據(jù)流相比有更好地可控性,這一點(diǎn)在上文提及的函數(shù)響應(yīng)式編程中也有體現(xiàn)。若論快速開發(fā),筆者感覺雙向數(shù)據(jù)綁定略勝一籌,畢竟這種View與ViewModel/ViewLogic之間的直接綁定直觀便捷。而如果是注重于全局的狀態(tài)管理,希望維護(hù)耦合程度較低、可測(cè)試性/可擴(kuò)展性較高的代碼,那么還是單向數(shù)據(jù)流,即Unidirectional Architecture較為合適。一家之言,歡迎討論。
Flux:數(shù)據(jù)流驅(qū)動(dòng)的頁面
Flux不能算是絕對(duì)的先行者,但是在Unidirectional Architecture中卻是最富盛名的一個(gè),也是很多人接觸到的第一個(gè)Unidirectional Architecture。Flux主要由以下幾個(gè)部分構(gòu)成:
-
Stores:存放業(yè)務(wù)數(shù)據(jù)和應(yīng)用狀態(tài),一個(gè)Flux中可能存在多個(gè)Stores
-
View:層次化組合的React組件
-
Actions:用戶輸入之后觸發(fā)View發(fā)出的事件
-
Dispatcher:負(fù)責(zé)分發(fā)Actions
根據(jù)上述流程,我們可知Flux模式的特性為:
-
Dispatcher:Event Bus中設(shè)置有一個(gè)單例的Dispatcher,很多Flux的變種都移除了Dispatcher依賴。
-
只有View使用可組合的組件:在Flux中只有React的組件可以進(jìn)行層次化組合,而Stores與Actions都不可以進(jìn)行層次化組合。React組件與Flux一般是松耦合的,因此Flux并不是Fractal,Dispatcher與Stores可以被看做Orchestrator。
-
用戶事件響應(yīng)在渲染時(shí)聲明:在React的render()函數(shù)中,即負(fù)責(zé)響應(yīng)用戶交互,也負(fù)責(zé)注冊(cè)用戶事件的處理器
下面我們來看一個(gè)具體的代碼對(duì)比,首先是以經(jīng)典的Cocoa風(fēng)格編寫一個(gè)簡單的計(jì)數(shù)器按鈕:
class ModelCounterconstructor: (@value=1) ->increaseValue: (delta) =>@value += deltaclass ControllerCounterconstructor: (opts) ->@model_counter = opts.model_counter@observers = []getValue: => @model_counter.valueincreaseValue: (delta) =>@model_counter.increaseValue(delta)@notifyObservers()notifyObservers: =>obj.notify(this) for obj in @observersregisterObserver: (observer) =>@observers.push(observer)class ViewCounterButtonconstructor: (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 elmnotify: =>$("button.#{@button_class}").replaceWith(=> @render())上述代碼邏輯用上文提及的MVC模式圖演示就是:
而如果用Flux模式實(shí)現(xiàn),會(huì)是下面這個(gè)樣子:
# Storeclass CounterStore extends EventEmitterconstructor: ->@count = 0@dispatchToken = @registerToDispatcher()increaseValue: (delta) ->@count += 1getCount: ->return @countregisterToDispatcher: ->CounterDispatcher.register((payload) =>switch payload.typewhen ActionTypes.INCREASE_COUNT@increaseValue(payload.delta))# Actionclass CounterActions@increaseCount: (delta) ->CounterDispatcher.handleViewAction({'type': ActionTypes.INCREASE_COUNT'delta': delta})# ViewCounterButton = 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))其數(shù)據(jù)流圖為:
Redux:集中式的狀態(tài)管理
Redux是Flux的所有變種中最為出色的一個(gè),并且也是當(dāng)前Web領(lǐng)域主流的狀態(tài)管理工具,其獨(dú)創(chuàng)的理念與功能深刻影響了GUI應(yīng)用程序架構(gòu)中的狀態(tài)管理的思想。Redux將Flux中單例的Dispatcher替換為了單例的Store,即也是其最大的特性,集中式的狀態(tài)管理。并且Store的定義也不是從零開始單獨(dú)定義,而是基于多個(gè)Reducer的組合,可以把Reducer看做Store Factory。Redux的重要組成部分包括:
-
Singleton Store:管理應(yīng)用中的狀態(tài),并且提供了一個(gè)dispatch(action)函數(shù)。
-
Provider:用于監(jiān)聽Store的變化并且連接像React、Angular這樣的UI框架
-
Actions:基于用戶輸入創(chuàng)建的分發(fā)給Reducer的事件
-
Reducers:用于響應(yīng)Actions并且更新全局狀態(tài)樹的純函數(shù)
根據(jù)上述流程,我們可知Redux模式的特性為:
-
以工廠模式組裝Stores:Redux允許我以createStore()函數(shù)加上一系列組合好的Reducer函數(shù)來創(chuàng)建Store實(shí)例,還有另一個(gè)applyMiddleware()函數(shù)可以允許在dispatch()函數(shù)執(zhí)行前后鏈?zhǔn)秸{(diào)用一系列中間件。
-
Providers:Redux并不特定地需要何種UI框架,可以與Angular、React等等很多UI框架協(xié)同工作。Redux并不是Fractal,一般來說Store被視作Orchestrator。
-
User Event處理器即可以選擇在渲染函數(shù)中聲明,也可以在其他地方進(jìn)行聲明。
Model-View-Update
又被稱作Elm Architecture,上面所講的Redux就是受到Elm的啟發(fā)演化而來,因此MVU與Redux之間有很多的相通之處。MVU使用函數(shù)式編程語言Elm作為其底層開發(fā)語言,因此該架構(gòu)可以被看做更純粹的函數(shù)式架構(gòu)。MVU中的基本組成部分有:
-
Model:定義狀態(tài)數(shù)據(jù)結(jié)構(gòu)的類型
-
View:純函數(shù),將狀態(tài)渲染為界面
-
Actions:以Mailbox的方式傳遞用戶事件的載體
-
Update:用于更新狀態(tài)的純函數(shù)
根據(jù)上述流程,我們可知Elm模式的特性為:
-
到處可見的層次化組合:Redux只是在View層允許將組件進(jìn)行層次化組合,而MVU中在Model與Update函數(shù)中也允許進(jìn)行層次化組合,甚至Actions都可以包含內(nèi)嵌的子Action
-
Elm屬于Fractal架構(gòu):因?yàn)镋lm中所有的模塊組件都支持層次化組合,即都可以被單獨(dú)地導(dǎo)出使用
Model-View-Intent
MVI是一個(gè)基于RxJS的響應(yīng)式單向數(shù)據(jù)流架構(gòu)。MVI也是Cycle.js的首選架構(gòu),主要由Observable事件流對(duì)象與處理函數(shù)組成。其主要的組成部分包括:
-
Intent:Observable提供的將用戶事件轉(zhuǎn)化為Action的函數(shù)
-
Model:Observable提供的將Action轉(zhuǎn)化為可觀測(cè)的State的函數(shù)
-
View:將狀態(tài)渲染為用戶界面的函數(shù)
-
Custom Element:類似于React Component那樣的界面組件
根據(jù)上述流程,我們可知MVI模式的特性為:
-
重度依賴于Observables:架構(gòu)中的每個(gè)部分都會(huì)被轉(zhuǎn)化為Observable事件流
-
Intent:不同于Flux或者Redux,MVI中的Actions并沒有直接傳送給Dispatcher或者Store,而是交于正在監(jiān)聽的Model
-
徹底的響應(yīng)式,并且只要所有的組件都遵循MVI模式就能保證整體架構(gòu)的fractal特性
總結(jié)
以上是生活随笔為你收集整理的GUI应用程序架构的十年变迁:MVC,MVP,MVVM,Unidirectional,Clean的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IT职业图谱
- 下一篇: spring mvc事务没有生效的原因