在我最近的工作中带膜,最讓人抓狂的就是為前端開(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)感興趣的壤蚜。
在每個(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):
作為服務(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ù)集虹脯,不管是輸入的還是輸出的驴娃。
此時(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ǔ)法:
它實(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赂蕴×基本上花同等的工夫(甚至更少),我就能實(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)行更新捧书。
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群。交流學(xué)習(xí)群號(hào):575745314 里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring骤星,MyBatis经瓷,Netty源碼分析,高并發(fā)洞难、高性能舆吮、分布式、微服務(wù)架構(gòu)的原理队贱,JVM性能優(yōu)化色冀、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源柱嫌,目前受益良多
那核心問(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ù)亍安粦?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)的表述募寨,以此類推族展。
(點(diǎn)擊放大圖像)
圖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)講是很容易的怀骤。
對(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中。
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群蹈丸。交流學(xué)習(xí)群號(hào):575745314 里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring成黄,MyBatis,Netty源碼分析逻杖,高并發(fā)奋岁、高性能、分布式荸百、微服務(wù)架構(gòu)的原理闻伶,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系够话。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源蓝翰,目前受益良多
如下就是“修改地址”Action該如何進(jìn)行實(shí)現(xiàn),它會(huì)調(diào)用地址校驗(yàn)的API:
(點(diǎn)擊放大圖像)
圖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
{ _name : ‘/^[a]$/i’ } // 名字以A或a開(kāi)頭 { _customerId: ‘123’ } // id=123的customerModel將會(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)行處理蟹腾。
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群。交流學(xué)習(xí)群號(hào):575745314 里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring区宇,MyBatis娃殖,Netty源碼分析,高并發(fā)萧锉、高性能珊随、分布式、微服務(wù)架構(gòu)的原理柿隙,JVM性能優(yōu)化叶洞、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源禀崖,目前受益良多
我發(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ì)封寞,它們不再遵循由前端決定的模式。