原文:How to visually design state in JavaScript
作者:Shawn McKay
譯者:逆圖
一份教你使用狀態機和狀態圖來開發應用的路線圖
為什麼狀態管理在JavaScript中顯得特別困難?是現代應用繼承的複雜性,還是工具的問題?其他工程領域是如何開發可靠和可預測的系統?是否有可能設計一個系統並將它轉換成程式碼,反之亦然?
讓我們探索狀態管理中的正規化轉換【譯者注:關於正規化轉換,請戳這裡】,用狀態機和狀態圖來直觀的設計系統。
概念 > 庫
我接觸狀態管理已經有一段時間了。而且已經嘗試過了各種各樣的狀態管理庫:Flux,Reflux,Redux,Dva,Vuex,Mobx以及我自己的庫。
爭論哪一個庫是更高效的解決方案是沒有意義的。各個狀態管理庫雖然有著不同的口味但卻有著相同的配方。它們都是整個拼圖中的一部分————它們讓連線和同步資料變得更加容易。
接下來我們所關注的解決方案涉及更大的藍圖:我們需要在規劃和設計系統方面做得更好。
打碎一切
想象一個你認為設計優雅的產品。一種能夠抵禦住使用者大量隨機互動後果的東西————你懂的,當使用者按下按鈕的次數超過預期時,這種不可預測性會以某種意想不到的順序與輸入來進行互動,或者是能夠以某種其它方式來讓你懷疑人生。生活總是很難在正軌上。
讓我來預測下你正在想象的這個產品是什麼吧。
嗯……你可能不會考慮專為web而打造的東西,那裡的哲學似乎是"快速行動,破除陳規"【譯者注:原文是:'move fast and break things'.這句話被作為標語貼在Facebook的辦公室內】。
從更新頻率上來看,你可能也沒有考慮過手機。
你可能甚至都沒想過那些最近才開發的東西。我們在開發可靠產品方面的能力似乎在倒退。
我想我知道你在想什麼了…
我猜對了嗎?...也許沒有?【譯者注:肯定是沒有...原po可能是個索粉...】
你可能不知道,這是20世紀80年代生產的一款的索尼隨身聽【walkman】。
小時候,我從一位已經升級到使用便攜CD播放器的朋友那裡收到了一個類似wlakman的卡帶播放器。我明白,一些年輕的讀者可能對這兩種裝置都不太熟悉。你們可以把Walkman想象成iPhone,但按鍵更大,也更容易壞。我的主要任務就是:搞壞它。
我將會嘗試所有不同按鈕的使用組合,看看會發生什麼:
- 在磁帶快速轉動時將它彈出
- 在快進的同時進行倒帶
在盡我所能的各種嘗試下,索尼Walkman的表現依舊比今天絕大多數網站都要好。
硬體介面
像Walkman這樣的電子裝置經受住了使用者測試的挑戰,沒有任何隱藏或禁用使用者介面元素的能力。任何按鈕都可以隨時被按下,任何事情都可能發生。然而它似乎依舊牢不可破。
這讓我想知道:
也許電子裝置能為我們如何構建網路介面提供了更好的範例。
我們可以從古老的電子產品設計過程中學到什麼?我們如何更好地設計應用?馬蒂,我們需要回到未來!【譯者注:出自美國經典科幻電影,《回到未來》.Marty是影片中的男主角.】
電子產品與網路
電子產品真的能教我們如何更好地在瀏覽器中建立應用嗎?
考慮到元件在過去五年中是Web開發中最大的變化之一。也許我們可以借用一些電子工程中的其他概念嗎?
作為Web開發人員,我們的條件已經很好了。真的真的很好。發現了一個bug?沒關係,我們可以在一小時內將更新部署到伺服器。
但是其他的工程領域並不是那麼寬容的。硬體產生問題往往直接導致裝置被廢棄。嵌入式開發人員必須非常小心,以確保韌體更新時不會耗盡電池或者讓所有現有裝置崩潰。
Web開發者擁有有一種不計後果的奢侈。
更不用說,軟體開發者很少會遇到和電子裝置開發者一樣的資源限制。你最近一次關心的可能是效能和記憶體使用,而不是如何讓這該死的東西工作。每秒60幀是一道低門檻。但隨著我們開始構建越來越複雜的應用且希望它能在低端手機和物聯網裝置上執行,這道門檻的標準正在上升。我們正逐漸面臨底層工程師數十年來所經歷的工程問題。
約束培育創造力。限制帶來了更好的設計。
要了解如何限制會帶來更好的設計,我們需要回到狀態管理的基礎。
一些狀態管理基礎
當下社群中的討論方向往往是傾向NPM包而不是基本的電腦科學原理。
工程師們從來不會問:“哪個庫更好?”他們會問:“我們該如何設計一個更好的系統?”
我們可以從一些設計良好的基本原則開始:
- 區分不確定資料和有限狀態
- 限制從一個狀態到另一個狀態轉換的可能性
- 設計直觀
我將通過8個要點來使用自己的方式完成這些任務。
1.狀態不等於資料
在程式系統中,狀態和資料之間的差異是模糊的。他們都儲存在記憶體中,因此被相同對待。
在React中,狀態和資料共享相同的名字和機制:
- 獲取:
this.state
- 儲存:
this.state = {}
- 更新:
this.setState(nextState)
在電子產品中,對狀態和資料的區別就沒有那麼多的混淆。
狀態表示系統可以處於有限數量的模式下————通常由電路本身定義。對Walkman來說,就像是“Playing”,“Stopped”,“Ejected”。就像是一種“模式”或“配置”,狀態是可數的。
資料儲存在具有幾乎有無限可能的儲存器中。對Walkman來說,就像是正在演奏的歌曲“Song 2”。資料,就像歌曲,擁有無限的可能性。
無論下面的DataLoader
元件做什麼,它的狀態都只可能生成一組有限的檢視:“loading”,“loaded”,“error”。
分離狀態和資料可以減少混淆,並允許我們構建基於有限狀態機的應用。
2.狀態有限
電子產品開發者很早就知道,一個可預測的介面一定是具有有限和可控狀態數量的狀態。如果狀態的數量不加控制,系統不僅會難以除錯,更無法進行徹底的測試。
在有限狀態機中,狀態是被明確定義的。轉換是一組你可以進行狀態轉移事件的集合。
舉個例子,使用事件“STOP”會觸發狀態轉換將狀態轉移至“Stopped”。
在React中,我們可以定義一個擁有兩種狀態的基本款Walkman:“Stopped”和“Playing”。
可以在這個CodeSandBox中檢視具體演示。
在一個有限狀態機中,系統始終會處於一個可能的配置下。這個介面絕無可能出現除了“Playing”和“Stopped”之外的東西。只要對這兩種狀態進行測試就能夠讓我們對系統充滿信心!
3.管理狀態機的複雜度
讓我們看看當我們開始向這個狀態機樣例中新增兩個新狀態時會發生什麼:“Rewinding”和“FastForwarding”。
當狀態相同時,他們看起來是很容易新增的。每一個狀態都像是一個可以單獨單獨開發和測試的模組。不過要小心,狀態轉換並不總是可行的。
我們應該注意不同狀態間不受控制的轉換
也許被你發現了。我們在上面的程式碼裡引入了一個bug。花點時間去看看你能否發現其中的問題。
4.守衛轉換
由於我們允許使用者在rewinding
和fastForwarding
兩種狀態間快速切換,而且沒有在切換過程中去將磁帶暫停。我們的磁帶似乎纏成了一坨。
為了解決這個問題,我們可以為我們的狀態轉換新增一層守衛。守衛是狀態轉換髮生所必須要滿足的條件。例如:我們可以確保事件FASTFORWARD
,REWIND
,和PLAY
只能在狀態為“Stopped”時觸發。
除非我們重新思考我們狀態管理的規劃和設計方式,否則意外的狀態轉換是必然發生的。
當我們新增諸如ejected
這樣的額外狀態時,我們必須去思考哪種狀態轉換在哪種條件下是被允許的。拿Walkman來說,你可以在它處於停止模式時按下停止鍵來彈出磁帶。為了新增這個功能,我們必須新增更多的守衛,並確定哪些轉換是可行的。
伴隨著各種新狀態的新增,各種未處理狀態組合的可能性成倍的增加。這並不是一個可擴充的解決方案。每個額外狀態都會檢查所有的轉換守衛。
這開始變得像是狀態在管理你了。
管理守衛的問題來源於狀態的表現方式:“Stopped”,“Playing”,“Rewinding”。
狀態的理想資料結構並不是字串或物件。
但那又會是什麼呢?
5.狀態是圖
狀態理想的資料結構通常是圖。狀態圖提供了一種直觀的方法來設計,視覺化的控制狀態在每個節點間的轉換。
這並不是什麼大新聞————電子工程師們這幾十年來一直在用狀態圖來描述複雜系統。
我們來看一個網上的例子。AWS Step Function為繪製應用程式工作流提供了視覺化介面。每個節點控制一個lambda————一個在雲上呼叫的遠端函式————每個函式的輸出會觸發下一個函式的輸入。
在上面的例子中,我們可以很清楚的看到使用者的操作是如何在每個步驟中移動的。包括那些可能的錯誤以及錯誤處理。新增額外的步驟並不會導致複雜性的指數級增加。
一些工程師可能已經注意到了,Step Function與PLC(可程式設計邏輯控制器)模組接線圖有不少共同之處。一些設計師可能會說這與他們的工作流程圖也有很多共同點。我們設計狀態的方式難道不應該與設計應用的方式有更多的共同之處嗎?
6.狀態圖腳手架
狀態圖應當成為開發應用的腳手架。
舉個例子。通過狀態圖,我們可以更加容易直觀的理解Walkman的操作。
如果沒有深入研究一些較為複雜的“守衛”程式碼,我們肯定會說除非在“Stopped”狀態下,否則都不應該從“Rewinding”跳到任何其它狀態。與此相反的是,你應該列出你的應用可以做的所有轉換,而不是概述的說它不能做什麼事。開發方式從防禦性的自下向上的編碼轉為自頂向下的設計。這種轉換是事半功倍的。
狀態圖更直觀,更容易除錯,而且更容易容納新需求。通過狀態機,每種狀態的變化都能夠與它相鄰的狀態相隔離。更不用說那些複雜的狀態“守衛”邏輯可以通過一種更加直觀易理解的方式去涵蓋。
不幸的是,狀態圖也可能是一顆定時炸彈
重度連通圖是沒法縮放的。想想如果我們在上圖中再新增另外4個狀態會發生什麼。可讀性降低而且重複性增加,各個糾纏的箭頭指向各個方向去爭奪空間。這種狀態圖的泛化被稱為狀態爆炸。
幸運的是,有一種使用形式化描述系統的方式可以用來減輕狀態圖的複雜度:讓我們開始探索statecharts。
7.掌握statecharts
我第一次瞭解到statecharts是在Luca Matteis在溫哥華React直面會上所做的《如何使用statecharts為Redux應用的行為建模》演講。第二天在工作中,我提出了“新”的狀態管理模式,其實我只是發現了許多我身邊的工程師早已熟悉的概念。我在一家基於IOT的公司工作,與許多硬體和嵌入式開發的工程師一起工作。我們正在招聘:)。
statechart的概念可以追溯到1987年,當時數學家David Harel發表了一篇關於視覺化描述複雜系統的論文。就以下面的石英錶為例:
一旦你理解了它自身的語言,statechart既直觀又容易掌握。
上面的例子介紹了一些新的狀態型別:
- 初始狀態 ———— 由帶箭頭的點標記的起始狀態。
- 巢狀狀態 ———— 可以訪問其父級轉換的狀態。
- 並行狀態 ———— 由虛線表示的兩個平行狀態。
- 歷史狀態 ———— 記住並可以返回其先前值的狀態。
除此之外,statecharts還可以包含觸發轉換和行為的時間和方式:
- 轉換 ———— 一個基於命名事件觸發狀態更改的函式。“Stopped”→轉換(“Play”)→“Playing”
- 守衛 ———— 轉換髮生必須滿足的條件。例如,如果沒有磁帶,或者磁帶已經播到了最後,則無法觸發“play”。“Stopped”→轉換(“Play”)**[hasTape]**→“Playing”。在既定的順序下,是可以產生多次轉換的。
- 行為 ———— 基於狀態變化發生的觸發器。例如,當狀態進入“playing”時觸發磁帶播放。動作可能發生在
onEntry
或onExit
上。
將Walkman示例用statechart重寫可以刪除狀態圖中冗餘的部分。注意,現在不再需要重複“STOP”事件了。statechart是可擴充套件的————新增其他並行狀態(如“Recording”和“Volume”)並不難。
Statechart並不僅僅是一個視覺化描述應用程式的概念。
statechart可以為我們生成應用程式底層的狀態機
你可以將看到的圖轉化為程式碼,反之亦然。以圖表的形式來檢視你的應用邏輯,或者把它畫出來。
8.statecharts工具
Statecharts為真正設計系統提供了一個充滿希望的未來————而不僅僅是紙上談兵。雖然其他程式語言已經出現了相關工具,但JavaScript現在才剛剛開始顯現出statechart工具的繁榮。
C和Java的開發人員可以通過使用工具來編寫statechart進行編碼。作為一個例子,Yakindu Statechart Tools中彙集了各種視覺化設計和程式碼。我最近學會了Yakindu還做了一個Typescript程式碼生成器。
同樣的工具最終也可以用於JavaScript。
Sketch Systems提供了一種在markdown中設計系統的方法。這可以用於在JavaScript中設計原型。雖然Sketch Systems尚不支援行為或守衛,但我發現它對於原型設計和測試狀態圖都非常有用。
Sketch Systems允許你將圖表匯出到XState,這是一個基於statechart的JavaScript庫,具有自己的視覺化和可點選狀態原型設計工具。
想象一下你編輯器中更高階的工具。想象一下你的工作流程,你可以在視覺化設計和手動編寫應用程式邏輯之間無縫切換。在社群中來推動我們想要更好地支援使用statechart的工具,庫和編輯器外掛,我們所必須付出的巨大努力是值得的。
結論
複雜度已經悄然降臨到了JavaScript社群中了。我認為我們還沒有做好準備。就個人而言,我承認我花了很長時間才開始擅長設計應用。我會先畫出一個元件樹和一些狀態的草稿。看著原型圖一步步迭代進生產。但是,如果沒有一個視覺化規格語言來設計狀態圖,我怎能真正擅長設計應用程式呢?
在很長一段時間裡,我坦白我接近狀態管理更多的是僅僅是好奇它的高深莫測。我不知道從電腦科學的其他領域也可以學到很多東西,有更長的建立和管理複雜系統的歷史。我逐漸明白,回顧過去是有價值的,並且要多關注我們周圍的工程領域。
我們可以向那些開創並開發了數十年系統解決方案的工程師學習,這些解決方案可用於建立複雜但可預測的系統。我們可以在工具和庫的基礎上構建一個生態系統,以支援應用程式邏輯的視覺化設計。我們需要這麼做,因為JavaScript需要有這些東西。
在JavaScript中設計應用程式擁有比以往都更加光明的未來。這篇文章站在非常高的角度上,可能留下的問題多於答案。在第2部分中,我想更深入地討論使用statecharts構建元件的模式。