如何直观的在JavaScript中管理状态

原文:How to visually design state in JavaScript

作者:Shawn McKay

译者:逆图

一份教你使用状态机和状态图来开发应用的路线图

为什么状态管理在JavaScript中显得特别困难?是现代应用继承的复杂性,还是工具的问题?其他工程领域是如何开发可靠和可预测的系统?是否有可能设计一个系统并将它转换成代码,反之亦然?

让我们探索状态管理中的范式转换【译者注:关于范式转换,请戳这里】,用状态机状态图来直观的设计系统。

概念 > 库

我接触状态管理已经有一段时间了。而且已经尝试过了各种各样的状态管理库:Flux,Reflux,Redux,Dva,Vuex,Mobx以及我自己的库

争论哪一个库是更高效的解决方案是没有意义的。各个状态管理库虽然有着不同的口味但却有着相同的配方。它们都是整个拼图中的一部分————它们让连接和同步数据变得更加容易。

接下来我们所关注的解决方案涉及更大的蓝图:我们需要在规划和设计系统方面做得更好。

打碎一切

想象一个你认为设计优雅的产品。一种能够抵御住用户大量随机交互后果的东西————你懂的,当用户按下按钮的次数超过预期时,这种不可预测性会以某种意想不到的顺序与输入来进行交互,或者是能够以某种其它方式来让你怀疑人生。生活总是很难在正轨上。

让我来预测下你正在想象的这个产品是什么吧。

嗯……你可能不会考虑专为web而打造的东西,那里的哲学似乎是"快速行动,破除陈规"【译者注:原文是:'move fast and break things'.这句话被作为标语贴在Facebook的办公室内】。

从更新频率上来看,你可能也没有考虑过手机。

你可能甚至都没想过那些最近才开发的东西。我们在开发可靠产品方面的能力似乎在倒退。

我想我知道你在想什么了…

wlakman

我猜对了吗?...也许没有?【译者注:肯定是没有...原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.守卫转换

由于我们允许用户在rewindingfastForwarding两种状态间快速切换,而且没有在切换过程中去将磁带暂停。我们的磁带似乎缠成了一坨。

为了解决这个问题,我们可以为我们的状态转换添加一层守卫。守卫是状态转换发生所必须要满足的条件。例如:我们可以确保事件FASTFORWARD,REWIND,和PLAY只能在状态为“Stopped”时触发。

除非我们重新思考我们状态管理的规划和设计方式,否则意外的状态转换是必然发生的。

当我们添加诸如ejected这样的额外状态时,我们必须去思考哪种状态转换在哪种条件下是被允许的。拿Walkman来说,你可以在它处于停止模式时按下停止键来弹出磁带。为了添加这个功能,我们必须添加更多的守卫,并确定哪些转换是可行的。

伴随着各种新状态的添加,各种未处理状态组合的可能性成倍的增加。这并不是一个可拓展的解决方案。每个额外状态都会检查所有的转换守卫。

这开始变得像是状态在管理你了。

管理守卫的问题来源于状态的表现方式:“Stopped”,“Playing”,“Rewinding”。

状态的理想数据结构并不是字符串或对象。

但那又会是什么呢?

5.状态是图

状态理想的数据结构通常是图。状态图提供了一种直观的方法来设计,可视化的控制状态在每个节点间的转换。

这并不是什么大新闻————电子工程师们这几十年来一直在用状态图来描述复杂系统。

我们来看一个网上的例子。AWS Step Function为绘制应用程序工作流提供了可视化界面。每个节点控制一个lambda————一个在云上调用的远程函数————每个函数的输出会触发下一个函数的输入。

AWS Step Functions

在上面的例子中,我们可以很清楚的看到用户的操作是如何在每个步骤中移动的。包括那些可能的错误以及错误处理。添加额外的步骤并不会导致复杂性的指数级增加。

一些工程师可能已经注意到了,Step Function与PLC(可编程逻辑控制器)模块接线图有不少共同之处。一些设计师可能会说这与他们的工作流程图也有很多共同点。我们设计状态的方式难道不应该与设计应用的方式有更多的共同之处吗?

6.状态图脚手架

状态图应当成为开发应用的脚手架。

举个例子。通过状态图,我们可以更加容易直观的理解Walkman的操作。

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”时触发磁带播放。动作可能发生在onEntryonExit上。

将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构建组件的模式。

猜你喜欢

转载自juejin.im/post/5b6b1ec8e51d4534b93f7adb
今日推荐