状态机表示法

3
我希望把GUI实现为状态机。我认为这样做有一些好处和一些缺点,但这不是这个问题的主题。
在阅读了一些关于此的资料后,我发现有几种用C++建模状态机的方法,但我卡在了两种上,但我不知道哪种方法更适合GUI建模。
1. 将状态机表示为具有以下方法的状态列表: - OnEvent(...) - OnEnterState(...) - OnExitState(...)
从 StateMachine::OnEvent(...) 转发事件到 CurrentState::OnEvent(...),并在此处做出是否进行转换的决定。在转换时,我会调用 CurrentState::OnExitState(...)、NewState::OnEnterState() 和 CurrentState = NewState;
使用这种方法,状态将与操作紧密耦合,但当我可以从一个状态转移到多个状态并且必须针对不同的转换采取不同的操作时,State 可能会变得复杂。
2. 将状态机表示为具有以下属性的转换列表: - InitialState - FinalState - OnEvent(...) - DoTransition(...)
从 StateMachine::OnEvent(...) 将事件转发到所有转换,其中 InitialState 与状态机中的 CurrentState 具有相同的值。如果满足转换条件,则停止循环,调用 DoTransition 方法,并将 CurrentState 设置为 Transition::FinalState。
使用这种方法,Transition 将非常简单,但转换计数可能会非常高。同时,跟踪一个状态在接收事件时将执行哪些操作将变得更加困难。
你认为哪种方法更适合GUI建模?你是否知道其他表示法可能更适合我的问题?

1
你是否考虑过使用状态机库,例如Boost.StatechartBoost.Meta State Machine - Björn Pollex
我猜你是指 current_state->OnEvent(),而不是 CurrentState::OnEvent() - Ben Voigt
@BjörnPollex:这些库提供了什么价值,使得在调试时必须逐步查看Boost代码? - Ben Voigt
@BjörnPollex 我查看了Boost.Statechar,发现状态在编译时与状态机绑定。但我不想这样。如果我有误,请纠正我。 - Mircea Ispas
@MSalters:是什么问题让您相信Felics正在使用Visual Studio?因为我找不到任何迹象。 - Ben Voigt
显示剩余4条评论
5个回答

3

这里有第三种选择:

  • 将状态机表示为转移矩阵。
    • 矩阵列索引表示状态。
    • 矩阵行索引表示符号(见下文)。
    • 矩阵单元格表示状态机应该转移到的状态。这既可以是新状态也可以是相同状态。
    • 每个状态都有一个OnEvent方法,它返回一个符号

StateMachine::OnEvent(...)将事件转发到State::OnEvent,后者返回执行结果符号StateMachine然后根据当前状态和返回的符号决定是否:

  • 必须进行不同状态的转换,或
  • 保留当前状态
  • 可选地,如果进行转换,则为相应状态调用OnExitStateOnEnterState

3个状态和3个符号的示例矩阵:

0 1 2
1 2 0
2 0 1

在这个例子中,如果机器处于任何一个状态 (0、1、2) 中,且 State::OnEvent 返回符号 0(矩阵中的第一行),则它保持在同一状态。
第二行表示,如果当前状态为 0,返回的符号是 1,则转换到状态 1。对于状态 1 -> 状态 2,对于状态 2 -> 状态 0。
类似地,第三行表示,对于符号 2,状态 0 -> 状态 2,状态 1 -> 状态 0,状态 2 -> 状态 1。
这意味着:
1. 符号的数量可能比状态的数量少得多。 2. 各个状态之间并不相互感知。 3. 所有转换都受一个点控制,因此,一旦您想要将DB_ERROR与NETWORK_ERROR处理方式不同,只需更改转换表而不触及状态实现即可。

1

我不知道这是否是您期望的答案,但我通常会以直接的方式处理这样的状态机。

使用枚举类型的状态变量(可能的状态)。在GUI的每个事件处理程序中,测试状态值,例如使用switch语句。根据需要进行任何处理,并设置下一个状态的值。

轻量级和灵活。保持代码规范使其易读且“正式”。


这是第一个版本的一种可能实现方式 - 对于一个状态,您会评估输入并采取行动。我希望避免在从一个状态转移到许多状态时出现巨大的“评估输入/采取行动”函数。 - Mircea Ispas
@Felics:更常见的情况是“对于一个输入,我可以根据当前状态转移到多个状态”。 - Ben Voigt
完整的决策表可以有NI x NS个条目(输入数量乘以状态数量)。如果你想避免庞大的函数,那么表格必须是稀疏的,即许多条目都是琐碎的。你可以根据哪种压缩效果最佳来在输入或状态上压缩表格。 - user1196549

1

我个人更喜欢你提到的第一种方法。我认为第二种方法相当不直观和过于复杂。每个状态都有一个类是简单易懂的,如果你在OnEnterState中设置正确的事件处理程序并在OnExitState中删除它们,你的代码将会很干净,而且每个状态都是自包含的,使得代码易于阅读。

你还可以避免使用大型switch语句来选择正确的事件处理程序或过程调用,因为每个状态所做的一切都在状态本身内部完美可见,从而使状态机代码变得简短而简单。

最后但并非最不重要的是,这种编码方式是从状态机图形精确转换到任何你使用的语言。


这里唯一的问题是难以保留额外的持久数据。你不能将其保存在状态的成员变量中,即使是继承的变量,因为每个状态必须是不同的对象才能进行虚拟调度。因此,最终需要向每个方法传递一个辅助结构。然而,对于每个状态数据(如计数器)的管理非常容易。 - Ben Voigt
@BenVoigt:持久化数据属于状态机自身,每个状态在构造时将接收到其所有者状态机的引用。 - BlackBear
确切地说,您需要通过另一个对象才能访问您的数据。这不是致命问题,但肯定会使代码更加冗长。 - Ben Voigt
@BenVoigt 不完全正确。您可以添加一个模板状态,并使用状态机的方法来填充它。在所有状态中,您将拥有相同的对象 - 即状态机本身。 - Mircea Ispas
@Felics:这听起来与BlackBear提出的有些不同。在他的设计中,每个状态都是一个单独的类。由于无法在构建后更改对象的类类型,这意味着每个状态也需要是一个单独的对象。 - Ben Voigt
在我的情况下,每个状态也是一个不同的对象。但这并不妨碍我从状态对象调用成员函数回调,并且这些函数对于所有状态都在同一个对象中。模板机制将为每个状态生成单独的类。 - Mircea Ispas

0

我更喜欢一种非常简单的方式来处理这种代码。

  • 状态的枚举。
  • 每个事件处理程序在决定要执行的操作之前都会检查当前状态。操作只是一个switch语句或if链中的复合块,并设置下一个状态。
  • 当操作变得超过几行长或需要重复使用时,重构为调用单独的辅助方法。

这样就没有额外的状态机管理元数据结构和管理该元数据的代码。只有您的业务数据和转换逻辑。并且操作可以直接检查和修改所有成员变量,包括当前状态。

缺点是您无法添加局部于一个状态的其他数据成员。这不是真正的问题,除非您有大量的状态。

如果您始终在进入每个状态时配置所有UI属性,则我发现这也会导致更健壮的设计,而不是假设先前的设置并创建状态退出行为以在状态转换之前恢复不变量。无论您使用什么方案实现转换,都适用。


0

如果您想要实现更复杂的行为,可以考虑使用Petri网来建模所需的行为。这将是更好的选择,因为它允许您确定所有可能的情况并防止死锁。

这个库可能对于实现控制GUI的状态机非常有用:PTN Engine


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接