cpressey/Facts-about-State-Machines:我认为状态机被低估了


我认为状态机经常被误解和应用不足:
这就是我写这篇文章的原因。这个事实列表的目的不是教你什么是状态机或如何使用它们;还有很多其他资源。相反,这里的目标是激发它们的使用并突出它们经常被忽视但仍然相关的事情。
如果您正在阅读其中一个事实并认为它微不足道,或无关紧要,或未经证实,或,或,……那么我鼓励您继续阅读下一个,希望您会发现它更有启发性。

状态机无处不在
他们到处都是。想想所有的网络应用程序、所有的电子设备、所有的家用电器、人们每天与之交互的所有工业机器。对于它们中的每一个,我们都可以制定一个状态机,以高度精确地描述其反应行为。

状态机不是状态机图
状态机是一个抽象对象——一个概念。状态机图是对该抽象对象的描述。就像同一个数字可以用数字(如“5”)或单词(如“5”)同样很好地表示,同一个状态机可以有多种表示,不同但等价。

状态机图就像系统反应行为的地图
它就像一张地图,因为对于给定的状态,您可以看到允许进出该状态的转换。您可以看到系统从一种状态“移动”到另一种状态的路径,也可以看到没有路径的地方。
就像地图一样,它不必(也可能不应该)包含每一个细节。它只需要足够的细节就可以让您有效地定位自己。

状态机就像棋盘游戏,图表就像棋盘
你可以想象你有一个计数器,它位于板上的一个正方形(状态)上。你抽一张牌,上面写着一个词(输入)。现在,如果有一个箭头从你的方格中伸出,标有那个词,你沿着它滑动你的计数器,将它移动到一个新的方格。但是,如果没有这样的箭头从您的方格中射出,您的柜台将保持原位。

非技术利益相关者可以阅读状态机图
地图和棋盘游戏是日常用品,所以我希望前两个比喻能让你相信,几乎任何人,无论是否有技术头脑,都能掌握状态机的基本功能。
当然,非技术利益相关者可能仍然会发现大图令人生畏。但是,通过将图表拆分为更小的图表,或者通过省略细节将图表简化为更简单的图表,您可以让读者轻松了解系统反应行为的结构。

状态机图允许推理系统的反应行为
如果给您一个起始状态和一系列输入,您可以通过状态机图绘制一个过程,并查看您最终处于什么状态,以及在此过程中发生了哪些操作。通过将此与您的直觉进行比较,或者与实际系统实际行为方式的报告(或它实际行为方式的规范)进行比较,您可以确定需要进行更改的地方。

状态机图允许对提议的修改进行推理
假设我们想在我们的系统中引入一个新的操作。如果我们这样做了,当与该新操作相关的事件发生时,现有状态会发生什么?应该发生什么?是否需要添加新的状态来捕捉这些事件的情况?这些状态将如何与现有状态相关联?

状态机可用于规范
以规范方式呈现的状态机图确定了您的系统应具有的反应行为。代码本身只能描述您的系统所具有的行为。确定表现出的行为是否正确的唯一方法是将其与应具有的行为进行比较。这就是制定规范的想法。

状态机可用于对用户界面建模
听说过一些不知情的说法,例如“我认为状态机仅适用于物理设备”或“用户界面不适合使用状态机建模”,这些显然是错误的,我觉得我应该指出这一点。
事实上,有一整本书是关于使用 Statecharts 设计 UI, Constructing the User Interface with Statecharts(Horrocks,1999) (archive.org,可在线借阅。)
书中指出,使用状态图符号(见下文)而不是平面状态图将有助于(实际上是必要的)解决大多数用户界面中发现的复杂性。

大多数状态机都很简单
例如,编译器具有微不足道的反应行为。绘制其状态机图表可能不是您时间的最佳利用方式。但是,即使是简单的状态机有时也值得以图表的形式进行计算,只是为了确认它们很简单,以及以何种方式简单。

复杂的状态机意味着您的系统具有复杂的行为
任何与该系统交互的东西——例如使用它的人——都将面临这种复杂性,以及这种复杂性的成本。这可能会提示您检查行为。也许复杂性是固有的......或者它可以被简化。

制定状态机可帮助您了解系统的反应行为
你不会在第一次尝试时就做对了,可能第二次或第三次尝试也不会。但是每次你修改它时,你都会学到一些关于你的系统行为方式的重要信息。

学习状态机可以帮助你简化系统的设计
为了降低系统的复杂性,你应该尽可能地消除复杂的东西,比如并发和复杂的状态交互。浏览状态机图将帮助您了解复杂的状态交互发生在哪里,并可能帮助您确定它们实际需要的位置以及可以消除它们的位置。

了解状态机将帮助您制定有用的测试
箭头表示输入发生时可能发生的转换。它可以从哪些状态发生?我们可以查看状态机图并回答这个问题。现在,那些其他状态呢?当输入发生在这些状态时会发生什么?会发生什么?可以制定测试用例来涵盖这些可能性。
中间的

状态机和有限自动机是两个不同的东西
虽然结构相似,但两者在实践中却截然不同。
有限自动机(有时也称为“有限状态机”)是自动机理论(维基百科)中正式定义的抽象。它们能够识别一些形式语言而不能识别其他语言,并且它们在软件中具有实际应用,例如在文本文档中的模式匹配中。

另一方面,状态机是一种系统设计概念,起源于电气工程(维基百科)。它通过系统之间可能的转换将系统的状态联系起来。这些转换是由从外部源接收的输入触发的。在(名义上)不同的时间接收输入。进行转换,或到达或离开状态,可能会受到条件的保护,并且可能会触发某种动作作为副作用。对于此类转换可能触发的操作类型没有预设限制。
短语“有限状态”可能会产生误导,可以避免

虽然状态机通常确实具有有限且固定数量的状态,但对可能与该机器相关联的状态量没有设定限制。该状态通常包括计数器、计时器、缓冲区、数据存储等。
请注意,“states”这个词在英语中是一个可数名词 (如“pennies”),而“state”这个词是一个大众名词(如“water”)。
为使这种区别更清晰而提出的一些术语包括将有限的状态集称为“控制状态”,将其他所有状态称为“扩展状态”。将这两者结合起来有时称为“系统状态”。
“状态机”这个词现在很常见;您不必用“有限”来限定它。当您说“状态机”时,系统设计人员通常会理解您的意思。

机器的“状态”是指系统的模式
有一个很好的论点是,“模式”这个词比“状态”这个词更适合用来指代有限控制状态集的成员。
如果你问自己,“这个系统可以处于什么模式?”,你可能会发现答案与状态机的状态集是一致的。
这也与这个集合是有限的事实相一致:很难想象一个系统具有无限数量的“模式”。
但这种规模的术语转变不太可能很快发生。每个人都知道它们是“状态机”,称它们为“模式机”是不会流行的。

“状态States”通常是定性的,而“状态state”通常是定量的
还注意到,有限状态集倾向于描述系统的 定性状态,而“扩展”状态在本质上往往是 定量的(计数器等)。

在任何给定时间恰好处于一种状态有助于推理
在推理状态机的行为时,这种互斥特性非常有用,因为如果机器处于(例如)“等待”状态,您也会立即知道它 并未处于“就绪”状态,并且不处于“拒绝”状态,等等,对于所有其他状态。

...但状态集不必是不相交的
尽管它对于推理反应行为非常非常有用(参见前面的事实),但它不是福音。Statecharts 论文 (Harel, 1986)(见下文)给出了一个图表示例,显示了图 41 中的两个部分重叠的状态。

状态机图不是流程图
流程图是“自行推进的”;一个决策点之后的转换完全由决策点内部的决策决定。但是,在状态机中,导致转换的触发器通常 在系统外部。(它重新活跃,记住。)
...但您可以在状态图中合并一些流程图元素
有时会发生输入导致转换,但转换的最终目的地不仅仅由输入决定;为了选择机器最终将转换到的状态,必须做出一个或多个决定。这可以通过相同两个状态之间的多个箭头捕获,每个箭头具有不同的条件;或者使用“空”状态,它们不代表状态,而是代表决策点。但对这两种方法都有有效的反对意见——条件可能很复杂,“空”状态是人为的。因此,确实有一个案例,可以在状态机图中使用决策点,例如流程图中的菱形框,使用它们会使行为更清晰。

状态机没有指定输入通信的方式
也就是说,不要求状态机的输入是直接发送给状态机的消息。
状态机可以“侦听”并对其他系统也正在响应的广播事件做出反应。
此外,如果您有两个状态机互相发送消息,那么这些消息在传输过程中是否驻留在队列中,或者它们传输实现的任何其他细节的问题实际上不在状态机设计的范围内本身。

输入总是在某种抽象级别上定义的
例如,按下鼠标按钮和选择菜单项都是事件,但后者的抽象层次比前者高;实际上,按下鼠标按钮可能是选择菜单项的一种方式(同时也可能有其他方式,例如使用键盘导航)。状态机可以存在于任何抽象级别,但通常只存在于那个级别;其他级别的事件通常不需要在其设计中考虑。

状态转换是原子的
对此有一个简单的证明:如果状态转换 不是原子的,那么您可以在状态图中引入另一个状态来描述中间状态——转换发生时系统的状态。而且,很自然地,你会进入和退出那个中间状态。但是,什么会阻止你为这些转换引入更多的中间状态,以及向那些更进一步的中间状态的转换......等等无休止的?这将是一个无限的倒退。因此——在状态机处理的抽象级别——状态之间的转换必须是原子的。

状态机还可以描述对象的生命周期
传统上,状态机图的主题是某种交互系统,但许多其他类型的对象也会随着时间的推移经历状态转换。这些往往是在更长的时间尺度上。例如,贷款申请在任何给定时间可能处于以下状态之一:正在填写、提交并等待评估、评估中、接受或拒绝。
生命周期趋于线性,交互系统趋于循环
这应该从“生命”这个词中显而易见,它意味着出生和死亡。
通常,状态机图具有区分“开始状态”和“结束状态”的符号。这些更强烈地应用于对象的生命周期而不是交互式系统,其中概念可能有些主观。(纸箱的启动状态是打开状态还是关闭状态?)

状态图
状态机具有组合爆炸的潜力
假设您有一组四个开/关开关。每个开关可以处于 2 种可能的状态。但是作为一个整体考虑的数组可以处于 2 次 2 次 2 次 2 (= 16)个可能的状态,并且这些状态中的每一个都转换到其他 4 个状态。包含所有这些状态和转换的状态图会很笨拙——麻烦多于有用。
可以通过嵌套和并行化来缓解组合爆炸
在上面的数组示例中,这 16 个状态中的很大一部分不太可能具有任何意义,即根据给定的输入影响下一个状态应该是什么。在大多数情况下,各州很可能受到同等对待。所以我们应该有一个符号来捕捉这一点,而不是天真地平等对待所有状态。
例如,在状态图表示法中,可以将阵列描述为每个开关都是其自己的 2 状态状态机,嵌套在表示整个阵列的单个超状态的正交区域(即并行分区)内。

状态图由 David Harel 在 1980 年代介绍
可以在此处找到介绍状态图的原始论文: 状态图:复杂系统的视觉形式主义(Harel,1987 年)(PDF)。
状态图一直是状态机设计的一个非常有影响力的符号,但仍然经常被误解和低估(就像状态机本身一样)。它的主要贡献是分层嵌套状态和 超状态中的正交区域。

状态图被 OMT 采用
OMT(对象建模技术)是 1980 年代后期制定的一种软件工程方法。OMT 提倡三轴设计,包括结构、功能和动态方面。对于动态方面的符号,OMT 采用了 Statecharts。他们最初打算对嵌套状态使用自己的基于树的表示法,但认为 Statechart 的基于轮廓的表示法是更好的选择。详细信息可以在《 面向对象的建模和设计》(Rumbaugh 等人,1991 年) (archive.org,可在线借用)一书第 5 节的参考书目注释中找到。

状态图进入 UML
UML(统一建模语言)是一套软件工程实践和符号,源于 OMT 和 Booch 形式主义。UML 提供的概念之一是UML 状态机(维基百科),它受到状态图的强烈影响,支持相同的主要贡献:分层嵌套状态和正交(即并行)区域。
执行

实现状态机的方法有很多
用 SCTV 虚构的市长 Tommy Shanks 的话来说,“有很多方法可以实现状态机,你可以对它们摇一摇。” The Anthology of the Finite State Machine Design Patterns (Adamczyk, 2003(?)) (PDF) 论文列出了数十种这样的方式(主要是状态设计模式的变体)。即使在面向对象的设计之外,也有许多选择:使用查找表、打开给定输入的状态、打开给定状态的输入、在每个输入中都有专用的输入代码状态等等。
状态机的实现可以非常类似于它的设计
尽管有前面的事实,但通常可以选择一个简单的实现方法并使用它运行。通过谨慎的软件设计选择,设计元素(状态、转换)和实现元素(状态、转换)之间的概念距离会很小。
这使得状态机更具吸引力,因为实现状态机几乎是机械的。并且根据规范检查实现也变得更加容易。(不过,使用实现本身作为规范的警告仍然适用:代码本身无法告诉您代码 应该做什么。)

有用于实现状态机的软件库
您可能会或可能不会从使用其中受益。从头开始实现状态机并不困难,选择适合您的问题空间的软件设计(参见前面的 2 个事实)是值得的——通用库可能无法提供这些设计。因此,使用库可能不如您想象的那么有益(肯定不如读取 JPEG 的库有益——这很难实现,并且不允许有太多变化。)

状态通常作为枚举实现
许多编程语言都提供了这样的功能;例如,C 有一个enum类型,而 Haskell 有一个不相交的和类型(作为代数数据类型的一部分)。它非常适合控制状态集的有限性质;它使源代码更具可读性,并且(在经过类型检查的设置中)可以防止状态索引变量采用非法值(即,不代表任何状态的值)。

布尔标志蠕变引入了不可能的状态
状态机有时被认为是架构衰退的补救措施,这在一定程度上意味着代码库倾向于在每次识别新条件时增长一个新的布尔标志。假设您正在编写用户界面。你从一个基本的实现开始,然后你意识到你需要等到一些后端服务准备好。所以你添加一个标志, is_ready. 然后您意识到某些操作可能不会成功,您需要向用户发出信号。所以你添加另一个标志,is_error. 现在,你有两种可能这些标志是真的。但这是一个有意义的情况吗?可能不是。此时最好重新考虑您的用户界面实际上可以处于的状态集,并制定与这些状态匹配的枚举,并使用该枚举而不是这些单独的布尔标志。

您可能会发现某些状态是“派生状态”
假设您正在编写一个计费系统。其中一项要求是,在创建新发票时,它没有行项目。另一个要求是不能提交空发票。
所以在状态机中,“Empty”和“Non-Empty”被建模为发票可以处于的状态,并且“submit”输入只在“Non-Empty”状态下有效,它转换为“Submitted” ”。这非常清楚地说明了要求。
但是在实现这个状态机时,制定一个同时包含“空”和“非空”的枚举可能没有意义,因为这些条件实际上取决于发票的行数——一个定量的状态. 它们都是更一般的状态“正在编辑”的实例。
因此,在这种情况下,实现可能与绘制图表的方式不同。从概念上讲,为清楚起见,“空”和“非空”是不同的状态,但在实现中,它们被实现为“派生状态”而不是“枚举状态”的一部分。或者,您可以将图表调整为具有单一状态,并让“提交”转换由条件“行项目数 > 0”保护。
另见上文“定性与定量状态”。


关于状态机的逻辑语句可以通过模型检查来决定
我们可以建立一组命题,其中每个命题在某些状态下为真,在其他状态下为假。然后我们可以给出时序逻辑的公式,例如“在某个未来存在 p 和 q 永远为真的时间”,并在我们的状态机上运行模型检查过程以查看我们的状态机的哪些状态公式成立。
有关使用时序逻辑进行模型检查的更多信息,请参见例如 计算机科学中的逻辑(Huth 和 Ryan,2000 年) (archive.org,可在线借用)的第 3 章。
这归结为时间逻辑是模态逻辑的专业化。状态机是标记转换系统的近亲;和模态逻辑是自然的(或者我们可以说是超自然的!)适合标记的转换系统。有关此视图的更多信息,请参见 模态逻辑的第一步(Popkorn,1994 年) (archive.org,可在线借阅)。
嵌套状态机实现了一种模块化
假设,本着模块化的精神,我们希望我们的状态机的某些部分是“可插拔的”:我们希望能够将它换成该部分的替代实现,在某些方面表现不同但仍然是整体的与状态机的其余部分兼容。
为此,必须围绕这部分建立模块化边界。只要边界内的任何东西都像边界外的一切所期望的那样反应,这些内部就可以换成其他满足相同期望的内部。
如果这些内部有自己的反应行为,他们可能会这样做,那么对此建模的最佳方法是将模块化边界放在单个超状态周围,该超状态具有自己的子状态(基本上是它自己的子状态机)。
为此,子状态必须是“私有的”;模块化边界之外的所有东西都不能依赖于它们的知识,以免当我们尝试用具有不同子状态的超状态交换超状态时出现问题。

可以通过使用约定来避免“历史伪状态”
状态图定义了一个“历史状态”(圆圈中的大写字母 H)。它有点令人困惑,但据我所知,在许多情况下实际上并不是必需的,特别是如果采用以下默认行为:
当重新进入包含子状态的状态s时,这些子状态中的当前子状态与之前离开s时的当前子状态相同。
如果s以前从未离开过,那么必须查阅 s 内部的起始状态 以发现当前的子状态。
所以这是一种“默认历史”的方法。 如果希望从某个外部状态转换到s的特定子状态,可以通过使转换箭头转到而不是s,而是直接到所需的子状态来实现。
像这样直接转换到子状态可能非常有用,但请注意它们违反了先前事实的“隐私”要求,因此它们与模块化(这可能是也可能不是问题,视情况而定)相反。
考虑模块化的另一种方法可能是用诸如“将s重置为其起始子状态”之类的副作用来装饰过渡到s 的箭头,而不是让它们过渡到s内的子状态。

状态机既不是命令式的也不是函数性的
当机器切换状态时,它是破坏性地更新系统的当前状态,还是创建系统的新状态并切换到该新状态?您可以通过任何一种方式实现它。状态机本身并不关心。

状态机表现出切换的弱点
假设我们有突发事件——有时你不只是得到一个事件,你会得到一堆事件。物理接触开关就是这样——这种现象称为“弹跳”。
进一步假设我们有 2 个状态,On 和 Off,以及一个从 On 到 Off 以及从 Off 到 On 的“切换”输入。如果它收到一连串“切换”输入,则无法预测它最终会进入哪种状态。
但是假设我们有 2 个输入,“打开”和“关闭”,每个输入都从一个状态转换到另一个状态。现在,如果我们得到一阵“开启”(分别是“关闭”),我们知道我们将始终处于什么状态:开启(或关闭)。
还有其他类似于爆发的情况,例如根本不知道您当前处于哪种状态,因此这为良好的设计提供了依据。