原創(chuàng): 張衛(wèi)濱 譯
Jean-Jacques Dubray是一名資深工程師,他最近引入了一個(gè)新的模式:狀態(tài)-行為-模(State-Action-Model,SAM)。SAM是一個(gè)函數(shù)式反應(yīng)型的編程模式,它致力于簡(jiǎn)化數(shù)據(jù)Model和View之間的交互。它究竟有何優(yōu)點(diǎn)值得作者棄用MVC呢?
話題起因
在我最近的工作中,最讓人抓狂的就是為前端開(kāi)發(fā)人員設(shè)計(jì)API。我們之間的對(duì)話大致就是這樣的:
開(kāi)發(fā)人員:這個(gè)頁(yè)面上有數(shù)據(jù)元素x,y,z…,你能不能為我創(chuàng)建一個(gè)API,響應(yīng)格式為{x: , y:, z: }我:好吧
我甚至沒(méi)有進(jìn)行進(jìn)一步的爭(zhēng)論。項(xiàng)目結(jié)束時(shí)會(huì)積累大量的API,這些API與經(jīng)常發(fā)生變化的頁(yè)面是關(guān)聯(lián)在一起的,按照“設(shè)計(jì)”,只要頁(yè)面改變,相應(yīng)的API也要隨之變化,而在此之前,我們甚至對(duì)此毫不知情,最終,由于形成因素眾多且各平臺(tái)之間存在些許差異,必須創(chuàng)建非常多的API來(lái)滿足這些需求。
Sam Newman甚至將這種制度化的過(guò)程稱之為BFF模式,這種模式建議為每種設(shè)備、平臺(tái)當(dāng)然還包含App版本開(kāi)發(fā)特定的API。 Daniel Jacobson在接受InfoQ的采訪時(shí)曾指出,Netflix頗為勉強(qiáng)地將“體驗(yàn)式API”與“臨時(shí)API(Ephemeral API)”劃上了等號(hào)。 唉……
幾個(gè)月前,我開(kāi)始思考是什么造成了如今的這種現(xiàn)象,該做些什么來(lái)應(yīng)對(duì)它,這個(gè)過(guò)程使我開(kāi)始質(zhì)疑應(yīng)用架構(gòu)中最強(qiáng)大的理念,也就是MVC,我感受到了函數(shù)式反應(yīng)型編程(reactive)的強(qiáng)大威力,這個(gè)過(guò)程致力于流程的簡(jiǎn)化,并試圖消除我們這個(gè)行業(yè)在生產(chǎn)率方面的膨脹情緒。我相信你會(huì)對(duì)我的發(fā)現(xiàn)感興趣的。
MVC的輝煌過(guò)去與現(xiàn)存問(wèn)題
在每個(gè)用戶界面背后,我們都在使用MVC模式,也就是模型-視圖-控制器(Model-View-Controller)。MVC發(fā)明的時(shí)候,Web尚不存在,當(dāng)時(shí)的軟件架構(gòu)充其量是胖客戶端在原始網(wǎng)絡(luò)中直接與單一數(shù)據(jù)庫(kù)會(huì)話。但是,幾十年之后,MVC依然在使用,持續(xù)地用于OmniChannel應(yīng)用的構(gòu)建。
Angular 2正式版即將發(fā)布,在這個(gè)時(shí)間節(jié)點(diǎn)重估MVC模式及各種MVC框架為應(yīng)用架構(gòu)帶來(lái)的貢獻(xiàn)意義重大。
我第一次接觸到MVC是在1990年,當(dāng)時(shí)NeXT剛剛發(fā)布Interface Builder(讓人驚訝的是,如今這款軟件依然發(fā)揮著重大的作用)。當(dāng)時(shí),我們感覺(jué)Interface Builder和MVC是一個(gè)很大的進(jìn)步。在90年代末期,MVC模式用到了HTTP上的任務(wù)中(還記得Struts嗎?),如今,就各個(gè)方面來(lái)講,MVC是所有應(yīng)用架構(gòu)的基本原則。
MVC的影響十分深遠(yuǎn),以致于React.js在介紹他們的框架時(shí)都委婉地與其劃清界限:“React實(shí)現(xiàn)的只是MVC中視圖(View)的部分”。
當(dāng)我去年開(kāi)始使用React的時(shí)候,我感覺(jué)它在某些地方有著明顯的不同:你在某個(gè)地方修改一部分?jǐn)?shù)據(jù),不需要顯式地與View和Model進(jìn)行交互,整個(gè)UI就能瞬間發(fā)生變化(不僅僅是域和表格中的值)。這也就是說(shuō),我很快就對(duì)React的編程模型感到了失望,在這方面,我顯然并不孤獨(dú)。我分享一下Andre Medeiros的觀點(diǎn):
React在很多方面都讓我感到失望,它主要是通過(guò)設(shè)計(jì)不佳的API來(lái)引導(dǎo)程序員[…]將多項(xiàng)關(guān)注點(diǎn)混合到一個(gè)組件之中。
作為服務(wù)端的API設(shè)計(jì)者,我的結(jié)論是沒(méi)有特別好的方式將API調(diào)用組織到React前端中,這恰恰是因?yàn)镽eact只關(guān)注View,在它的編程模型中根本不存在控制器。
到目前為止,F(xiàn)acebook一直致力于在框架層面彌合這一空白。React團(tuán)隊(duì)起初引入了Flux模式,不過(guò)它依然令人失望,最近Dan Abramov又提倡另外一種模式,名為Redux,在一定程度上來(lái)講,它的方向是正確的,但是在將API關(guān)聯(lián)到前端方面,依然比不上我下面所介紹的方案。
google發(fā)布過(guò)GWT、Android SDK還有Angular,你可能認(rèn)為他們的工程師熟知何為最好的前端架構(gòu),但是當(dāng)你閱讀Angular 2設(shè)計(jì)考量的文章時(shí),便會(huì)不以為然,即便在Google大家也達(dá)成這樣的共識(shí),他們是這樣評(píng)價(jià)之前的工作成果的:
Angular 1并不是基于組件的理念構(gòu)建的。相反,我們需要將控制器與頁(yè)面上各種[元素]進(jìn)行關(guān)聯(lián)(attach),其中包含了我們的自定義邏輯。根據(jù)我們自定義的指令如何對(duì)其進(jìn)行封裝(是否包含isolate scope?),scope會(huì)進(jìn)行關(guān)聯(lián)或繼續(xù)往下傳遞。
基于組件的Angular 2看起來(lái)能簡(jiǎn)單一點(diǎn)嗎?其實(shí)并沒(méi)有好多少。Angular 2的核心包本身就包含了180個(gè)語(yǔ)義(semantics),整個(gè)框架的語(yǔ)義已經(jīng)接近500個(gè),這是基于html5和css3的。誰(shuí)有那么多時(shí)間學(xué)習(xí)和掌握這樣的框架來(lái)構(gòu)建Web應(yīng)用呢?當(dāng)Angular 3出現(xiàn)的時(shí)候,情況又該是什么樣子呢?
在使用過(guò)React并了解了Angular 2將會(huì)是什么樣子之后,我感到有些沮喪:這些框架都系統(tǒng)性地強(qiáng)制我使用BFF“頁(yè)面可替換模式(Screen Scraping)”模式,按照這種模式,每個(gè)服務(wù)端的API要匹配頁(yè)面上的數(shù)據(jù)集,不管是輸入的還是輸出的。
棄用MVC之后怎么走?
此時(shí),我決定“讓這一切見(jiàn)鬼去吧”。我構(gòu)建了一個(gè)Web應(yīng)用,沒(méi)有使用React、沒(méi)有使用Angular也沒(méi)有使用任何其他的MVC框架,通過(guò)這種方式,我看一下是否能夠找到一種在View和底層API之間進(jìn)行更好協(xié)作的方式。
就React來(lái)講,我最喜歡的一點(diǎn)在于Model和View之間的關(guān)聯(lián)關(guān)系。React不是基于模板的,View本身沒(méi)有辦法請(qǐng)求數(shù)據(jù)(我們只能將數(shù)據(jù)傳遞給View),看起來(lái),針對(duì)這一點(diǎn)進(jìn)行探索是一個(gè)很好的方向。
如果看得足夠長(zhǎng)遠(yuǎn)的話,你會(huì)發(fā)現(xiàn)React唯一的目的就是將View分解為一系列(純粹的)函數(shù)和JSX語(yǔ)法:
<V params={M}/>
它實(shí)際上與下面的格式并沒(méi)有什么差別:
V = f( M )
例如,我當(dāng)前正在從事項(xiàng)目的Web站點(diǎn), Gliiph,就是使用這種函數(shù)構(gòu)建的:
圖1:用于生成站點(diǎn)Slider組件HTML的函數(shù)
這個(gè)函數(shù)需要使用Model來(lái)填充數(shù)據(jù):
圖2:支撐slider的Model
如果用簡(jiǎn)單的JAVAScript函數(shù)就能完成任務(wù),我們?yōu)槭裁催€要用React呢?
虛擬DOM(virtual-dom)?如果你覺(jué)得需要這樣一種方案的話(我并不確定有很多的人需要這樣),其實(shí)有這樣的可選方案,我也期望開(kāi)發(fā)出更多的方案。
GraphQL?并不完全如此。不要因?yàn)镕acebook大量使用它就對(duì)其產(chǎn)生誤解,認(rèn)為它一定是對(duì)你有好處的。GraphQL僅僅是以聲明的方式來(lái)創(chuàng)建視圖模型。強(qiáng)制要求Model匹配View會(huì)給你帶來(lái)麻煩,而不是解決方案。React團(tuán)隊(duì)可能會(huì)覺(jué)得使用“客戶端指定查詢(Client-specified queries)”是沒(méi)有問(wèn)題的(就像反應(yīng)型團(tuán)隊(duì)中那樣):
GraphQL完全是由View以及編寫(xiě)它們的前端工程師的需求所驅(qū)動(dòng)的。[…]另一方面,GraphQL查詢會(huì)精確返回客戶端請(qǐng)求的內(nèi)容,除此之外,也就沒(méi)什么了。
GraphQL團(tuán)隊(duì)沒(méi)有關(guān)注到JSX語(yǔ)法背后的核心思想:用函數(shù)將Model與View分離。與模板和“前端工程師所編寫(xiě)的查詢”不同,函數(shù)不需要Model來(lái)適配View。
當(dāng)View是由函數(shù)創(chuàng)建的時(shí)候(而不是由模板或查詢所創(chuàng)建),我們就可以按需轉(zhuǎn)換Model,使其按照最合適的形式來(lái)展現(xiàn)View,不必在Model的形式上添加人為的限制。
例如,如果View要展現(xiàn)一個(gè)值v,有一個(gè)圖形化的指示器會(huì)標(biāo)明這個(gè)值是優(yōu)秀、良好還是很差,我們沒(méi)有理由將指示器的值放到Model中:函數(shù)應(yīng)該根據(jù)Model所提供的v值,來(lái)進(jìn)行簡(jiǎn)單的計(jì)算,從而確定指示器的值。
現(xiàn)在,把這些計(jì)算直接嵌入到View中并不是什么好主意,使View-Model成為一個(gè)純函數(shù)也并非難事,因此當(dāng)我們需要明確的View-Model時(shí),就沒(méi)有特殊的理由再使用GraphQL了:
V = f( vm(M) )
作為深諳MDE之道的人,我相信你更善于編寫(xiě)代碼,而不是元數(shù)據(jù),不管它是模板還是像GraphQL這樣的復(fù)雜查詢語(yǔ)言。
這個(gè)函數(shù)式的方式能夠帶來(lái)多項(xiàng)好處。首先,與React類似,它允許我們將View分解為組件。它們創(chuàng)建的較為自然的界面允許我們?yōu)閃eb應(yīng)用或Web站點(diǎn)設(shè)置“主題”,或者使用不同的技術(shù)來(lái)渲染View(如原生的方式)。函數(shù)實(shí)現(xiàn)還有可能增強(qiáng)我們實(shí)現(xiàn)反應(yīng)型設(shè)計(jì)的方式。
在接下來(lái)的幾個(gè)月中,可能會(huì)出現(xiàn)開(kāi)發(fā)者交付用JavaScript函數(shù)包裝的基于組件的HTML5主題的情況。這也是最近這段時(shí)間,在我的Web站點(diǎn)項(xiàng)目中,我所采用的方式,我會(huì)得到一個(gè)模板,然后迅速地將其封裝為JavaScript函數(shù)。我不再使用wordPress/ target=_blank class=infotextkey>WordPress。基本上花同等的工夫(甚至更少),我就能實(shí)現(xiàn)HTML5和CSS的最佳效果。
這種方式也需要在設(shè)計(jì)師和開(kāi)發(fā)人員之間建立一種新型的關(guān)系。任何人都可以編寫(xiě)這些JavaScript函數(shù),尤其是模板的設(shè)計(jì)人員。人們不需要學(xué)習(xí)綁定方法、JSX和Angular模板的語(yǔ)法,只掌握簡(jiǎn)單的JavaScript核心函數(shù)就足以讓這一切運(yùn)轉(zhuǎn)起來(lái)。
有意思的是,從反應(yīng)型流程的角度來(lái)說(shuō),這些函數(shù)可以部署在最合適的地方:在服務(wù)端或在客戶端均可。
但最為重要的是,這種方式允許在View與Model之間建立最小的契約關(guān)系,讓Model來(lái)決定如何以最好的方式將其數(shù)據(jù)傳遞給View。讓Model去處理諸如緩存、懶加載、編配以及一致性的問(wèn)題。與模板和GraphQL不同,這種方式不需要從View的角度來(lái)直接發(fā)送請(qǐng)求。
既然我們有了一種方式將Model與View進(jìn)行解耦,那么下一個(gè)問(wèn)題就是:在這里該如何創(chuàng)建完整的應(yīng)用模型呢?“控制器”該是什么樣子的?為了回答這個(gè)問(wèn)題,讓我們重新回到MVC上來(lái)。
蘋(píng)果公司了解MVC的基本情況,因?yàn)樗麄冊(cè)谏鲜兰o(jì)80年代初,從Xerox PARC“偷來(lái)了”這一模式,從那時(shí)起,他們就堅(jiān)定地實(shí)現(xiàn)這一模式:
圖3:MVC模式
Andre Medeiros曾經(jīng)清晰地指出,這里核心的缺點(diǎn)在于, MVC模式是“交互式的(interactive)”(這與反應(yīng)型截然不同)。在傳統(tǒng)的MVC之中,Action(Controller)將會(huì)調(diào)用Model上的更新方法,在成功(或出錯(cuò))之時(shí)會(huì)確定如何更新View。他指出,其實(shí)并非必須如此,這里還有另外一種有效的、反應(yīng)型的處理方式,我們只需這樣考慮,Action只應(yīng)該將值傳遞給Model,不管輸出是什么,也不必確定Model該如何進(jìn)行更新。
那核心問(wèn)題就變成了:該如何將Action集成到反應(yīng)型流程中呢?如果你想理解Action的基礎(chǔ)知識(shí)的話,那么你應(yīng)該看一下TLA+。TLA代表的是“Action中的邏輯時(shí)序(Temporal Logic of Actions)”,這是由Dr. Lamport所提出的學(xué)說(shuō),他也因此獲得了圖靈獎(jiǎng)。在TLA+中,Action是純函數(shù):
data’ = A (data)
我真的非常喜歡TLA+這個(gè)很棒的理念,因?yàn)樗鼜?qiáng)制函數(shù)只轉(zhuǎn)換給定的數(shù)據(jù)集。
按照這種形式,反應(yīng)型MVC看起來(lái)可能就會(huì)如下所示:
V = f( M.present( A(data) ) )
這個(gè)表達(dá)式規(guī)定當(dāng)Action觸發(fā)的時(shí)候,它會(huì)根據(jù)一組輸入(例如用戶輸入)計(jì)算一個(gè)數(shù)據(jù)集,這個(gè)數(shù)據(jù)是提交到Model中的,然后會(huì)確定是否需要以及如何對(duì)其自身進(jìn)行更新。當(dāng)更新完成后,View會(huì)根據(jù)新的Model狀態(tài)進(jìn)行更新。反應(yīng)型的環(huán)就閉合了。Model持久化和獲取其數(shù)據(jù)的方式是與反應(yīng)型流程無(wú)關(guān)的,所以,它理所應(yīng)當(dāng)?shù)?ldquo;不應(yīng)該由前端工程師來(lái)編寫(xiě)”。不必因此而感到歉意。
再次強(qiáng)調(diào),Action是純函數(shù),沒(méi)有狀態(tài)和其他的副作用(例如,對(duì)于Model,不會(huì)包含計(jì)數(shù)的日志)。
反應(yīng)型MVC模式很有意思,因?yàn)槌薓odel以外,所有的事情都是純函數(shù)。公平來(lái)講,Redux實(shí)現(xiàn)了這種特殊的模式,但是帶有React不必要的形式,并且在reducer中,Model和Action之間存在一點(diǎn)不必要的耦合。Action和接口之間是純粹的消息傳遞。
這也就是說(shuō),反應(yīng)型MVC并不完整,按照Dan喜歡的說(shuō)法,它并沒(méi)有擴(kuò)展到現(xiàn)實(shí)的應(yīng)用之中。讓我們通過(guò)一個(gè)簡(jiǎn)單的樣例來(lái)闡述這是為什么。
假設(shè)我們需要實(shí)現(xiàn)一個(gè)應(yīng)用來(lái)控制火箭的發(fā)射:一旦我們開(kāi)始倒計(jì)時(shí),系統(tǒng)將會(huì)遞減計(jì)數(shù)器(counter),當(dāng)它到達(dá)零的時(shí)候,會(huì)將Model中所有未定的狀態(tài)設(shè)置為規(guī)定值,火箭的發(fā)射將會(huì)進(jìn)行初始化。
這個(gè)應(yīng)用有一個(gè)簡(jiǎn)單的狀態(tài)機(jī):
圖4:火箭發(fā)射的狀態(tài)機(jī)
其中decrement和launch都是“自動(dòng)”的Action,這意味著我們每次進(jìn)入(或重新進(jìn)入)counting狀態(tài)時(shí),將會(huì)保證進(jìn)行轉(zhuǎn)換的評(píng)估,如果計(jì)數(shù)器的值大于零的話,decrement Action將會(huì)繼續(xù)調(diào)用,如果值為零的話,將會(huì)調(diào)用launchAction。在任何的時(shí)間點(diǎn)都可以觸發(fā)abort Action,這樣的話,控制系統(tǒng)將會(huì)轉(zhuǎn)換到aborted狀態(tài)。
在MVC中,這種類型的邏輯將會(huì)在控制器中實(shí)現(xiàn),并且可能會(huì)由View中的一個(gè)計(jì)時(shí)器來(lái)觸發(fā)。
這一段至關(guān)重要,所以請(qǐng)仔細(xì)閱讀。我們已經(jīng)看到,在TLA+中,Action沒(méi)有副作用,只是計(jì)算結(jié)果的狀態(tài),Model處理Action的輸出并對(duì)其自身進(jìn)行更新。這是與傳統(tǒng)狀態(tài)機(jī)語(yǔ)義的基本區(qū)別,在傳統(tǒng)的狀態(tài)機(jī)中,Action會(huì)指定結(jié)果狀態(tài),也就是說(shuō),結(jié)果狀態(tài)是獨(dú)立于Model的。
在TLA+中,所啟用的Action能夠在狀態(tài)表述(也就是View)中進(jìn)行觸發(fā),這些Action不會(huì)直接與觸發(fā)狀態(tài)轉(zhuǎn)換的行為進(jìn)行關(guān)聯(lián)。換句話說(shuō),狀態(tài)機(jī)不應(yīng)該由連接兩個(gè)狀態(tài)的元組(S1, A, S2)來(lái)進(jìn)行指定,傳統(tǒng)的狀態(tài)機(jī)是這樣做的,它們?cè)M的形式應(yīng)該是(Sk, Ak1, Ak2,…),這指定了所有啟用的Action,并給定了一個(gè)狀態(tài)Sk,Action應(yīng)用于系統(tǒng)之后,將會(huì)計(jì)算出結(jié)果狀態(tài),Model將會(huì)處理更新。
當(dāng)我們引入“state”對(duì)象時(shí),TLA+提供了一種更優(yōu)秀的方式來(lái)對(duì)系統(tǒng)進(jìn)行概念化,它將Action和view(僅僅是一種狀態(tài)的表述)進(jìn)行了分離。
我們樣例中的Model如下所示:
model = {
counter: ,
started: ,
aborted: ,
launched:
}
系統(tǒng)中四個(gè)(控制)狀態(tài)分別對(duì)應(yīng)于Model中如下的值:
ready = {counter: 10, started: false, aborted: false, launched: false }
counting = {counter: [0..10], started: true, aborted: false, launched: false }
launched = {counter: 0, started: true, aborted: false, launched: true}
aborted = {counter: [0..10], started: true, aborted: true, launched: false}
這個(gè)Model是由系統(tǒng)的所有屬性及其可能的值所指定的,狀態(tài)則指定了所啟用的Action,它會(huì)給定一組值。這種類型的業(yè)務(wù)邏輯必須要在某個(gè)地方進(jìn)行實(shí)現(xiàn)。我們不能指望用戶能夠知道哪個(gè)Action是否可行。在這方面,沒(méi)有其他的方式。不過(guò),這種類型的業(yè)務(wù)邏輯很難編寫(xiě)、調(diào)試和維護(hù),在沒(méi)有語(yǔ)義對(duì)其進(jìn)行描述時(shí),更是如此,比如在MVC中就是這樣。
讓我們?yōu)榛鸺l(fā)射的樣例編寫(xiě)一些代碼。從TLA+角度來(lái)講,next-action斷言在邏輯上會(huì)跟在狀態(tài)渲染之后。當(dāng)前狀態(tài)呈現(xiàn)之后,下一步就是執(zhí)行next-action斷言,如果存在的話,將會(huì)計(jì)算并執(zhí)行下一個(gè)Action,這個(gè)Action會(huì)將其數(shù)據(jù)交給Model,Model將會(huì)初始化新?tīng)顟B(tài)的表述,以此類推。
圖5:火箭發(fā)射器的實(shí)現(xiàn)
需要注意的是,在客戶端/服務(wù)器架構(gòu)下,當(dāng)自動(dòng)Action觸發(fā)之后,我們可能需要使用像WebSocket這樣的協(xié)議(或者在WebSocket不可用的時(shí)候,使用輪詢機(jī)制)來(lái)正確地渲染狀態(tài)表述。
我曾經(jīng)使用Java和JavaScript編寫(xiě)過(guò)一個(gè)很輕量級(jí)的開(kāi)源庫(kù),它使用TLA+特有的語(yǔ)義來(lái)構(gòu)造狀態(tài)對(duì)象,并提供了樣例,這些樣例使用WebSocket、輪詢和隊(duì)列實(shí)現(xiàn)瀏覽器/服務(wù)器交互。在火箭發(fā)射器的樣例中可以看到,我們并非必須要使用那個(gè)庫(kù)。一旦理解了如何編寫(xiě),狀態(tài)實(shí)現(xiàn)的編碼相對(duì)來(lái)講是很容易的。
新模式——SAM模式
對(duì)于要引入的新模式來(lái)說(shuō),我相信我們已經(jīng)具備了所有的元素,這個(gè)新模式作為MVC的替代者,名為SAM模式(狀態(tài)-行為-模型,State-Action-Model),它具有反應(yīng)型和函數(shù)式的特性,靈感來(lái)源于React.js和TLA+。
SAM模式可以通過(guò)如下的表達(dá)式來(lái)進(jìn)行描述:
V = S( vm( M.present( A(data) ) ), nap(M))
它表明在應(yīng)用一個(gè)Action A之后,View V可以計(jì)算得出,Action會(huì)作為Model的純函數(shù)。
在SAM中,A(Action)、vm(視圖-模型,view-model)、nap(next-action斷言)以及S(狀態(tài)表述)必須都是純函數(shù)。在SAM中,我們通常所說(shuō)的“狀態(tài)”(系統(tǒng)中屬性的值)要完全局限于Model之中,改變這些值的邏輯在Model本身之外是不可見(jiàn)的。
隨便提一下,next-action斷言,即nap()是一個(gè)回調(diào),它會(huì)在狀態(tài)表述創(chuàng)建完成,并渲染給用戶時(shí)調(diào)用。
圖6:狀態(tài)-行為-模型(SAM)模式
模式本身是獨(dú)立于任何協(xié)議的(可以不費(fèi)什么力氣就能在HTTP上實(shí)現(xiàn))和客戶端/服務(wù)器拓?fù)浣Y(jié)構(gòu)的。
SAM并不意味著我們必須要使用狀態(tài)機(jī)的語(yǔ)義來(lái)獲取View的內(nèi)容。如果Action是由View觸發(fā)的,那next-action斷言就是一個(gè)空函數(shù)。不過(guò),這可能是一個(gè)很好的實(shí)踐,它清晰暴露了底層狀態(tài)機(jī)的控制狀態(tài),因?yàn)楦鶕?jù)(控制)狀態(tài)的不同,View看起來(lái)可能也是不同的。
另一方面,如果你的狀態(tài)機(jī)涉及到自動(dòng)化的Action,那么Action和Model都不可能做到純粹的不包含next-action斷言:有些Action將會(huì)變得有狀態(tài),或者M(jìn)odel必須要觸發(fā)Action,而這本來(lái)并不是它的角色。順便提一下,也許并不那么直觀,狀態(tài)對(duì)象并沒(méi)有持有任何的“狀態(tài)”,它同樣也是純函數(shù),它會(huì)渲染View并計(jì)算next-action斷言,這兩者都來(lái)源于Model的屬性值。
這種新模式的好處在于,它清晰地將CRUD操作從Action中分離了出來(lái)。Model負(fù)責(zé)它的持久化,將會(huì)通過(guò)CRUD操作來(lái)實(shí)現(xiàn),通過(guò)View是無(wú)法進(jìn)行訪問(wèn)的。尤其是,View永遠(yuǎn)不會(huì)處于“獲取”數(shù)據(jù)的位置,View所能做的唯一的事情就是請(qǐng)求系統(tǒng)中當(dāng)前的狀態(tài)表述并通過(guò)觸發(fā)Action初始化一個(gè)反應(yīng)型流程。
Action僅僅代表了一種具有權(quán)限的通道,以此來(lái)建議Model該怎樣進(jìn)行變更。它們本身(在Model方面)并沒(méi)有什么副作用。如果必要的話,Action會(huì)調(diào)用第三方的API(同樣,對(duì)Model沒(méi)有副作用),比如說(shuō),修改地址的Action可能會(huì)希望調(diào)用地址校驗(yàn)服務(wù),并將服務(wù)返回的地址提交到Model中。
如下就是“修改地址”Action該如何進(jìn)行實(shí)現(xiàn),它會(huì)調(diào)用地址校驗(yàn)的API:
圖7:“修改地址”的實(shí)現(xiàn)
模式中的元素,包括Action和Model,可以進(jìn)行自由地組合:
函數(shù)組合
data’ = A(B(data))
端組合(Peer)(相同的數(shù)據(jù)集可以提交給兩個(gè)Model)
M1.present(data’)
M2.present(data’)
父子組合(父Model控制的數(shù)據(jù)集提交給子Model)
M1.present(data’,M2)
function present(data, child) {
// 執(zhí)行更新
…
// 同步Model
child.present(c(data))
}
發(fā)布/訂閱組合
M1.on(“topic”, present )
M2.on(“topic”, present )
或
M1.on(“data”, present )
M2.on(“data”, present )
有些架構(gòu)師可能會(huì)考慮到System of Record和Systems of Engagement,這種模式有助于明確這兩層的接口(圖8),Model會(huì)負(fù)責(zé)與systems of record的交互。
圖8:SAM組合模型
整個(gè)模式本身也是可以進(jìn)行組合的,我們可以實(shí)現(xiàn)運(yùn)行在瀏覽器中的SAM實(shí)例,使其支持類似于向?qū)В╳izard)的行為(如ToDo應(yīng)用),它會(huì)與服務(wù)器端的SAM進(jìn)行交互:
圖9:SAM實(shí)例組合
請(qǐng)注意,里層的SAM實(shí)例是作為狀態(tài)表述的一部分進(jìn)行傳送的,這個(gè)狀態(tài)表述是由外層的實(shí)例所生成的。
會(huì)話檢查應(yīng)該在Action觸發(fā)之前進(jìn)行(圖10)。SAM能夠啟用一項(xiàng)很有意思的組合,在將數(shù)據(jù)提交給Model之前,View可以調(diào)用一個(gè)第三方的Action,并且要為其提供一個(gè)token和指向系統(tǒng)Action的回調(diào),這個(gè)第三方Action會(huì)進(jìn)行授權(quán)并校驗(yàn)該調(diào)用的合法性。
圖10:借助SAM實(shí)現(xiàn)會(huì)話管理
從CQRS的角度來(lái)講,這個(gè)模式?jīng)]有對(duì)查詢(Query)和命令(Command)做特殊的區(qū)分,但是底層的實(shí)現(xiàn)需要進(jìn)行這種區(qū)分。搜索或查詢“Action”只是簡(jiǎn)單地傳遞一組參數(shù)到Model中。我們可以采用某種約定(如下劃線前綴)來(lái)區(qū)分查詢和命令,或者我們可以在Model上使用兩個(gè)不同的present方法:
{ _name : ‘/^[a]$/i’ } // 名字以A或a開(kāi)頭
{ _customerId: ‘123’ } // id=123的customer
Model將會(huì)執(zhí)行必要的操作以匹配查詢,更新其內(nèi)容并觸發(fā)View的渲染。類似的約定可以用于創(chuàng)建、更新或刪除Model中的元素。在將Action的輸出傳遞給Model方面,我們可以實(shí)現(xiàn)多種方式(數(shù)據(jù)集、事件、Action……)。每種方式都會(huì)有其優(yōu)勢(shì)和不足,最終這取決于個(gè)人偏好。我更喜歡數(shù)據(jù)集的方式。
在異常方面,與React類似,我們預(yù)期Model會(huì)以屬性值的形式保存異常信息(這些屬性值可能是由Action提交的,也可能是CRUD操作返回的)。在渲染狀態(tài)表述的時(shí)候,會(huì)用到屬性值,以展現(xiàn)異常信息。
在緩存方面,SAM在狀態(tài)表述層提供了緩存的選項(xiàng)。直觀上來(lái)看,緩存這些狀態(tài)表述函數(shù)的結(jié)果能夠?qū)崿F(xiàn)更高的命中率,因?yàn)槲覀儸F(xiàn)在是在組件/狀態(tài)層觸發(fā)緩存,而不是在Action/響應(yīng)層。
該模式的反應(yīng)型和函數(shù)式結(jié)構(gòu)使得功能重放(replay)和單元測(cè)試變得非常容易。
SAM模式完全改變了前端架構(gòu)的范式,因?yàn)楦鶕?jù)TLA+的基礎(chǔ)理念,業(yè)務(wù)邏輯可以清晰地描述為:
- Action是純函數(shù)
- CRUD操作放在Model中
- 狀態(tài)控制自動(dòng)化的Action
作為API的設(shè)計(jì)者,從我的角度來(lái)講,這種模式將API設(shè)計(jì)的責(zé)任推到了服務(wù)器端,在View和Model之間保持了最小的契約。
Action作為純函數(shù),能夠跨Model重用,只要某個(gè)Model能夠接受Action所對(duì)應(yīng)的輸出即可。我們可以期望Action庫(kù)、主題(狀態(tài)表述)甚至Model能夠繁榮發(fā)展起來(lái),因?yàn)樗鼈儸F(xiàn)在能夠獨(dú)立地進(jìn)行組合。
借助SAM模式,微服務(wù)能夠非常自然地支撐Model。像Hivepod.io這樣的框架能夠插入進(jìn)來(lái),就像它本來(lái)就在這層似得。
最為重要的是,這種模式像React一樣,不需要任何的數(shù)據(jù)綁定或模板。
隨著時(shí)間的推移,我希望能夠推動(dòng)瀏覽器永久添加虛擬DOM的特性,新的狀態(tài)表述能夠通過(guò)專有API直接進(jìn)行處理。
我發(fā)現(xiàn)這個(gè)旅程將會(huì)帶來(lái)一定的革新性:在過(guò)去的幾十年中,面向?qū)ο笏坪鯚o(wú)處不在,但它已經(jīng)一去不返了。我現(xiàn)在只能按照反應(yīng)型和函數(shù)式來(lái)進(jìn)行思考。我借助SAM所構(gòu)建的東西及其構(gòu)建速度都是前所未有的。另外,我能夠關(guān)注于API和服務(wù)的設(shè)計(jì),它們不再遵循由前端決定的模式。






