jean-jacques dubray 是一名資深工程師,他最近引入了一個新的模式:狀态-行為-模型(state-action-model,sam)。sam 是一個函數式反應型的程式設計模式,它緻力于簡化資料 model 和 view 之間的互動。它究竟有何優點值得作者棄用 mvc 呢?
話題起因
在我最近的工作中,最讓人抓狂的就是為前端開發人員設計 api。我們之間的對話大緻就是這樣的:
開發人員:這個頁面上有資料元素x,y,z…,你能不能為我建立一個 api,響應格式為{x: , y:, z: }
我:好吧
我甚至沒有進行進一步的争論。項目結束時會積累大量的 api,這些 api 與經常發生變化的頁面是關聯在一起的,按照“設計”,隻要頁面改變,相應的 api 也要随之變化,而在此之前,我們甚至對此毫不知情,最終,由于形成因素衆多且各平台之間存在些許差異,必須建立非常多的 api 來滿足這些需求。
sam newman 甚至将這種制度化的過程稱之為bff 模式,這種模式建議為每種裝置、平台當然還包含 app 版本開發特定的 api。 daniel jacobson 在接受 infoq 的采訪時曾指出,netflix 頗為勉強地将“體驗式 api”與“臨時 api(ephemeral api)”劃上了等号。 唉……
幾個月前,我開始思考是什麼造成了如今的這種現象,該做些什麼來應對它,這個過程使我開始質疑應用架構中最強大的理念,也就是 mvc,我感受到了函數式反應型程式設計(reactive)的強大威力,這個過程緻力于流程的簡化,并試圖消除我們這個行業在生産率方面的膨脹情緒。我相信 你會對我的發現感興趣的。
mvc 的輝煌過去與現存問題
在每個使用者界面背後,我們都在使用 mvc 模式,也就是模型-視圖-控制器(model-view-controller)。mvc 發明的時候,web 尚不存在,當時的軟體架構充其量是胖用戶端在原始網絡中直接與單一資料庫會話。但是,幾十年之後,mvc 依然在使用,持續地用于 omnichannel 應用的建構。
angular 2 正式版即将釋出,在這個時間節點重估 mvc 模式及各種 mvc 架構為應用架構帶來的貢獻意義重大。
我第一次接觸到 mvc 是在 1990 年,當時 next 剛剛釋出 interface builder(讓人驚訝的是,如今這款軟體依然發揮着重大的作用)。當時,我們感覺 interface builder 和 mvc 是一個很大的進步。在 90 年代末期,mvc 模式用到了 http 上的任務中(還記得 struts 嗎?),如今,就各個方面來講,mvc 是所有應用架構的基本原則。
mvc 的影響十分深遠,以緻于 react.js 在介紹他們的架構時都委婉地與其劃清界限:“react 實作的隻是 mvc 中視圖(view)的部分”。
當我去年開始使用 react 的時候,我感覺它在某些地方有着明顯的不同:你在某個地方修改一部分資料,不需要顯式地與 view 和 model 進行互動,整個 ui 就能瞬間發生變化(不僅僅是域和表格中的值)。這也就是說,我很快就對 react 的程式設計模型感到了失望,在這方面,我顯然并不孤獨。我分享一下 andre medeiros 的觀點:
react 在很多方面都讓我感到失望,它主要是通過設計不佳的 api 來引導程式員[…]将多項關注點混合到一個元件之中。
作為服務端的 api 設計者,我的結論是沒有特别好的方式将 api 調用組織到 react 前端中,這恰恰是因為 react 隻關注 view,在它的程式設計模型中根本不存在控制器。
到目前為止,facebook 一直緻力于在架構層面彌合這一空白。react 團隊起初引入了 flux 模式,不過它依然令人失望,最近 dan abramov 又提倡另外一種模式,名為 redux,在一定程度上來講,它的方向是正确的,但是在将 api 關聯到前端方面,依然比不上我下面所介紹的方案。
google 釋出過 gwt、android sdk 還有 angular,你可能認為他們的工程師熟知何為最好的前端架構,但是當你閱讀 angular 2 設計考量的文章時,便會不以為然,即便在 google 大家也達成這樣的共識,他們是這樣評價之前的工作成果的:
angular 1 并不是基于元件的理念建構的。相反,我們需要将控制器與頁面上各種[元素]進行關聯(attach),其中包含了我們的自定義邏輯。根據我們自定義的指令 如何對其進行封裝(是否包含 isolate scope?),scope 會進行關聯或繼續往下傳遞。
基于元件的 angular 2 看起來能簡單一點嗎?其實并沒有好多少。angular 2 的核心包本身就包含了 180 個語義(semantics),整個架構的語義已經接近 500 個,這是基于 html5 和 css3 的。誰有那麼多時間學習和掌握這樣的架構來建構 web 應用呢?當 angular 3 出現的時候,情況又該是什麼樣子呢?
在使用過 react 并了解了 angular 2 将會是什麼樣子之後,我感到有些沮喪:這些架構都系統性地強制我使用 bff“頁面可替換模式(screen scraping)”模式,按照這種模式,每個服務端的 api 要比對頁面上的資料集,不管是輸入的還是輸出的。
棄用 mvc 之後怎麼走?
此時,我決定“讓這一切見鬼去吧”。我建構了一個 web 應用,沒有使用 react、沒有使用 angular 也沒有使用任何其他的 mvc 架構,通過這種方式,我看一下是否能夠找到一種在 view 和底層 api 之間進行更好協作的方式。
就 react 來講,我最喜歡的一點在于 model 和 view 之間的關聯關系。react 不是基于模闆的,view 本身沒有辦法請求資料(我們隻能将資料傳遞給 view),看起來,針對這一點進行探索是一個很好的方向。
如果看得足夠長遠的話,你會發現 react 唯一的目的就是将 view 分解為一系列(純粹的)函數和 jsx 文法:
它實際上與下面的格式并沒有什麼差别:
例如,我目前正在從事項目的 web 站點, gliiph,就是使用這種函數建構的:
圖1:用于生成站點 slider 元件 html 的函數
這個函數需要使用 model 來填充資料:
圖2:支撐 slider 的 model
如果用簡單的 javascript 函數就能完成任務,我們為什麼還要用 react 呢?
虛拟 dom(virtual-dom)?如果你覺得需要這樣一種方案的話(我并不确定有很多的人需要這樣),其實有這樣的可選方案,我也期望開發出更多的方案。
graphql?并不完全如此。不要因為 facebook 大量使用它就對其産生誤解,認為它一定是對你有好處的。graphql 僅僅是以聲明的方式來建立視圖模型。強制要求 model 比對 view 會給你帶來麻煩,而不是解決方案。react 團隊可能會覺得使用“用戶端指定查詢(client-specified queries)”是沒有問題的(就像反應型團隊中那樣):
graphql 完全是由 view 以及編寫它們的前端工程師的需求所驅動的。[…]另一方面,graphql 查詢會精确傳回用戶端請求的内容,除此之外,也就沒什麼了。
graphql 團隊沒有關注到 jsx 文法背後的核心思想:用函數将 model 與 view 分離。與模闆和“前端工程師所編寫的查詢”不同,函數不需要 model 來适配 view。
當 view 是由函數建立的時候(而不是由模闆或查詢所建立),我們就可以按需轉換 model,使其按照最合适的形式來展現 view,不必在 model 的形式上添加人為的限制。
例如,如果 view 要展現一個值v,有一個圖形化的訓示器會标明這個值是優秀、良好還是很差,我們沒有理由将訓示器的值放到 model 中:函數應該根據 model 所提供的v值,來進行簡單的計算,進而确定訓示器的值。
現在,把這些計算直接嵌入到 view 中并不是什麼好主意,使 view-model 成為一個純函數也并非難事,是以當我們需要明确的 view-model 時,就沒有特殊的理由再使用 graphql 了:
作為深谙 mde 之道的人,我相信你更善于編寫代碼,而不是中繼資料,不管它是模闆還是像 graphql 這樣的複雜查詢語言。
這個函數式的方式能夠帶來多項好處。首先,與 react 類似,它允許我們将 view 分解為元件。它們建立的較為自然的界面允許我們為 web 應用或 web 站點設定“主題”,或者使用不同的技術來渲染 view(如原生的方式)。函數實作還有可能增強我們實作反應型設計的方式。
在接下來的幾個月中,可能會出現開發者傳遞用 javascript 函數包裝的基于元件的 html5 主題的情況。這也是最近這段時間,在我的 web 站點項目中,我所采用的方式,我會得到一個模闆,然後迅速地将其封裝為 javascript 函數。我不再使用 wordpress。基本上花同等的工夫(甚至更少),我就能實作 html5 和 css 的最佳效果。
這種方式也需要在設計師和開發人員之間建立一種新型的關系。任何人都可以編寫這些 javascript 函數,尤其是模闆的設計人員。人們不需要學習綁定方法、jsx 和 angular 模闆的文法,隻掌握簡單的 javascript 核心函數就足以讓這一切運轉起來。
有意思的是,從反應型流程的角度來說,這些函數可以部署在最合适的地方:在服務端或在用戶端均可。
但最為重要的是,這種方式允許在 view 與 model 之間建立最小的契約關系,讓 model 來決定如何以最好的方式将其資料傳遞給 view。讓 model 去處理諸如緩存、懶加載、編配以及一緻性的問題。與模闆和 graphql 不同,這種方式不需要從 view 的角度來直接發送請求。
既然我們有了一種方式将 model 與 view 進行解耦,那麼下一個問題就是:在這裡該如何建立完整的應用模型呢?“控制器”該是什麼樣子的?為了回答這個問題,讓我們重新回到 mvc 上來。
蘋果公司了解 mvc 的基本情況,因為他們在上世紀 80 年代初,從 xerox parc“偷來了”這一模式,從那時起,他們就堅定地實作這一模式:
圖3:mvc 模式
andre medeiros 曾經清晰地指出,這裡核心的缺點在于, mvc 模式是“互動式的(interactive)”(這與反應型截然不同)。在傳統的 mvc 之中,action(controller)将會調用 model 上的更新方法,在成功(或出錯)之時會确定如何更新 view。他指出,其實并非必須如此,這裡還有另外一種有效的、反應型的處理方式,我們隻需這樣考慮,action 隻應該将值傳遞給 model,不管輸出是什麼,也不必确定 model 該如何進行更新。
那核心問題就變成了:該如何将 action 內建到反應型流程中呢?如果你想了解 action 的基礎知識的話,那麼你應該看一下 tla+。tla 代表的是“action 中的邏輯時序(temporal logic of actions)”,這是由 dr. lamport 所提出的學說,他也是以獲得了圖靈獎。在 tla+ 中,action 是純函數:
data’ = a (data)
我真的非常喜歡 tla+ 這個很棒的理念,因為它強制函數隻轉換給定的資料集。
按照這種形式,反應型 mvc 看起來可能就會如下所示:
v = f ( m.present ( a (data) ) )
這個表達式規定當 action 觸發的時候,它會根據一組輸入(例如使用者輸入)計算一個資料集,這個資料是送出到 model 中的,然後會确定是否需要以及如何對其自身進行更新。當更新完成後,view 會根據新的 model 狀态進行更新。反應型的環就閉合了。model 持久化和擷取其資料的方式是與反應型流程無關的,是以,它理所應當地“不應該由前端工程師來編寫”。不必是以而感到歉意。
再次強調,action 是純函數,沒有狀态和其他的副作用(例如,對于 model,不會包含計數的日志)。
反應型 mvc 模式很有意思,因為除了 model 以外,所有的事情都是純函數。公平來講,redux 實作了這種特殊的模式,但是帶有 react 不必要的形式,并且在 reducer 中,model 和 action 之間存在一點不必要的耦合。action 和接口之間是純粹的消息傳遞。
這也就是說,反應型 mvc 并不完整,按照 dan 喜歡的說法,它并沒有擴充到現實的應用之中。讓我們通過一個簡單的樣例來闡述這是為什麼。
假設我們需要實作一個應用來控制火箭的發射:一旦我們開始倒計時,系統将會遞減計數器(counter),當它到達零的時候,會将 model 中所有未定的狀态設定為規定值,火箭的發射将會進行初始化。
這個應用有一個簡單的狀态機:
圖4:火箭發射的狀态機
其中 decrement 和 launch 都是“自動”的 action,這意味着我們每次進入(或重新進入)counting 狀态時,将會保證進行轉換的評估,如果計數器的值大于零的話,decrement action 将會繼續調用,如果值為零的話,将會調用 launchaction。在任何的時間點都可以觸發 abort action,這樣的話,控制系統将會轉換到 aborted 狀态。
在 mvc 中,這種類型的邏輯将會在控制器中實作,并且可能會由 view 中的一個計時器來觸發。
這一段至關重要,是以請仔細閱讀。我們已經看到,在 tla+ 中,action 沒有副作用,隻是計算結果的狀态,model 處理 action 的輸出并對其自身進行更新。這是與傳統狀态機語義的基本差別,在傳統的狀态機中,action 會指定結果狀态,也就是說,結果狀态是獨立于 model 的。
在 tla+ 中,所啟用的 action 能夠在狀态表述(也就是 view)中進行觸發,這些 action 不會直接與觸發狀态轉換的行為進行關聯。換 句話說,狀态機不應該由連接配接兩個狀态的元組(s1, a, s2)來進行指定,傳統的狀态機是這樣做的,它們元組的形式應該是(sk, ak1, ak2,…),這指定了所有啟用的 action,并給定了一個狀态 sk,action 應用于系統之後,将會計算出結果狀态,model 将會處理更新。
當我們引入“state”對象時,tla+ 提供了一種更優秀的方式來對系統進行概念化,它将 action 和 view(僅僅是一種狀态的表述)進行了分離。
我們樣例中的 model 如下所示:
系統中四個(控制)狀态分别對應于 model 中如下的值:
這個 model 是由系統的所有屬性及其可能的值所指定的,狀态則指定了所啟用的 action,它會給定一組值。這種類型的業務邏輯必須要在某個地方進行實作。我們不能指望使用者能夠知道哪個 action 是否可行。在這方面,沒有其他的方式。不過,這種類型的業務邏輯很難編寫、調試和維護,在沒有語義對其進行描述時,更是如此,比如在 mvc 中就是這樣。
讓我們為火箭發射的樣例編寫一些代碼。從 tla+ 角度來講,next-action 斷言在邏輯上會跟在狀态渲染之後。目前狀态呈現之後,下一步就是執行 next-action 斷言,如果存在的話,将會計算并執行下一個 action,這個 action 會将其資料交給 model,model 将會初始化新狀态的表述,以此類推。
圖5:火箭發射器的實作
需要注意的是,在用戶端/伺服器架構下,當自動 action 觸發之後,我們可能需要使用像 websocket 這樣的協定(或者在 websocket 不可用的時候,使用輪詢機制)來正确地渲染狀态表述。
我曾經使用 java 和 javascript 編寫過一個很輕量級的開源庫,它使用 tla+ 特有的語義來構造狀态對象,并提供了樣例,這些樣例使用 websocket、輪詢和隊列實作浏覽器/伺服器互動。在火箭發射器的樣例中可以看到,我們并非必須要使用那個庫。一旦了解了如何編寫,狀态實作的編碼 相對來講是很容易的。
新模式——sam 模式
對于要引入的新模式來說,我相信我們已經具備了所有的元素,這個新模式作為 mvc 的替代者,名為 sam 模式(狀态-行為-模型,state-action-model),它具有反應型和函數式的特性,靈感來源于 react.js 和 tla+。
sam 模式可以通過如下的表達式來進行描述:
它表明在應用一個 action a 之後,view v 可以計算得出,action 會作為 model 的純函數。
在 sam 中,a(action)、vm(視圖-模型,view-model)、nap(next-action 斷言)以及s(狀态表述)必須都是純函數。在 sam 中,我們通常所說的“狀态”(系統中屬性的值)要完全局限于 model 之中,改變這些值的邏輯在 model 本身之外是不可見的。
随便提一下,next-action 斷言,即 nap ()是一個回調,它會在狀态表述建立完成,并渲染給使用者時調用。
圖7:“修改位址”的實作
模式中的元素,包括 action 和 model,可以進行自由地組合:
函數組合
端組合(peer)(相同的資料集可以送出給兩個 model)
父子組合(父 model 控制的資料集送出給子 model)
釋出/訂閱組合
或
有些架構師可能會考慮到 system of record 和 systems of engagement,這種模式有助于明确這兩層的接口(圖8),model 會負責與 systems of record 的互動。
圖8:sam 組合模型
整個模式本身也是可以進行組合的,我們可以實作運作在浏覽器中的 sam 執行個體,使其支援類似于向導(wizard)的行為(如 todo 應用),它會與伺服器端的 sam 進行互動:
圖9:sam 執行個體組合
請注意,裡層的 sam 執行個體是作為狀态表述的一部分進行傳送的,這個狀态表述是由外層的執行個體所生成的。
會話檢查應該在 action 觸發之前進行(圖 10)。sam 能夠啟用一項很有意思的組合,在将資料送出給 model 之前,view 可以調用一個第三方的 action,并且要為其提供一個 token 和指向系統 action 的回調,這個第三方 action 會進行授權并校驗該調用的合法性。
圖 10:借助 sam 實作會話管理
從 cqrs 的角度來講,這個模式沒有對查詢(query)和指令(command)做特殊的區分,但是底層的實作需要進行這種區分。搜尋或查詢“action”隻是 簡單地傳遞一組參數到 model 中。我們可以采用某種約定(如下劃線字首)來區分查詢和指令,或者我們可以在 model 上使用兩個不同的 present 方法:
model 将會執行必要的操作以比對查詢,更新其内容并觸發 view 的渲染。類似的約定可以用于建立、更新或删除 model 中的元素。在将 action 的輸出傳遞給 model 方面,我們可以實作多種方式(資料集、事件、action……)。每種方式都會有其優勢和不足,最終這取決于個人偏好。我更喜歡資料集的方式。
在異常方面,與 react 類似,我們預期 model 會以屬性值的形式儲存異常資訊(這些屬性值可能是由 action 送出的,也可能是 crud 操作傳回的)。在渲染狀态表述的時候,會用到屬性值,以展現異常資訊。
在緩存方面,sam 在狀态表述層提供了緩存的選項。直覺上來看,緩存這些狀态表述函數的結果能夠實作更高的命中率,因為我們現在是在元件/狀态層觸發緩存,而不是在 action/響應層。
該模式的反應型和函數式結構使得功能重放(replay)和單元測試變得非常容易。
sam 模式完全改變了前端架構的範式,因為根據 tla+ 的基礎理念,業務邏輯可以清晰地描述為:
action 是純函數
crud 操作放在 model 中
狀态控制自動化的 action
作為 api 的設計者,從我的角度來講,這種模式将 api 設計的責任推到了伺服器端,在 view 和 model 之間保持了最小的契約。
action 作為純函數,能夠跨 model 重用,隻要某個 model 能夠接受 action 所對應的輸出即可。我們可以期望 action 庫、主題(狀态表述)甚至 model 能夠繁榮發展起來,因為它們現在能夠獨立地進行組合。
借助 sam 模式,微服務能夠非常自然地支撐 model。像 hivepod.io 這樣的架構能夠插入進來,就像它本來就在這層似得。
最為重要的是,這種模式像 react 一樣,不需要任何的資料綁定或模闆。
随着時間的推移,我希望能夠推動浏覽器永久添加虛拟 dom 的特性,新的狀态表述能夠通過專有 api 直接進行處理。
我發現這個旅程将會帶來一定的革新性:在過去的幾十年中,面向對象似乎無處不在,但它已經一去不返了。我現在隻能按照反應型和函數式來進行思考。我 借助 sam 所建構的東西及其建構速度都是前所未有的。另外,我能夠關注于 api 和服務的設計,它們不再遵循由前端決定的模式。
====================================分割線================================