一开始,用状态机来思考可能有点令人惊讶,所以我们快速浏览一下其中的概念。
状态机是描述系统行为的具体模型。它由有限数量的状态和转换组成。
简单状态没有子状态。它可以有数据、进入和退出行为以及延迟事件。可以为状态(或状态机)提供进入和退出行为(也称为动作),当进入或离开某个状态时,无论如何都会执行这些行为。状态还可以具有不会调用进入或退出行为的内部转换。状态可以将事件标记为延迟。这意味着如果该状态处于活动状态,则无法处理该事件,但必须保留它。下次不延迟该事件的状态处于活动状态时,该事件将被处理,就好像它刚刚被触发一样。
转换是由于事件触发而引发的活动状态之间的切换。动作和保护条件可以附加到转换上。动作在转换触发时执行,保护条件是一个首先执行的布尔运算,它可以返回 false 来阻止转换触发。
初始状态标记了状态机的第一个活动状态。它没有实际存在,从它开始的转换也没有。
复合状态是包含一个区域或分解为两个或多个区域的状态。复合状态包含其自己的状态和区域集。
子机是在另一个状态机中作为状态插入的状态机。同一个子机可以插入多次。
正交区域是复合状态或子机的部分,每个区域都有自己的一组互斥的状态和转换。
UML 还定义了许多伪状态,它们被认为是重要的建模概念,但不足以使其成为头等公民。终止伪状态会终止状态机的执行(MSM 的处理方式略有不同。状态机不会被销毁,但不会再处理任何事件)。
退出点伪状态会退出复合状态或子机,并强制终止所有包含区域的执行。
进入点伪状态允许一种受控的进入复合状态的方式。具体来说,它将复合状态外部的转换连接到复合状态内部的转换。一个重要的点是,这种机制只允许进入一个区域。在上图中,在 region1 中,初始状态将变为活动状态。
除了直接进入子机(如区域案例所示)这一明显且更常见的情况外,还有另外两种进入子机的方式。显式进入意味着内部状态是转换的目标。与直接进入不同,不进行任何试探性封装,只执行一次转换。显式退出是从内部状态到子机外部状态的转换(MSM 不支持)。我不建议使用显式进入或退出。
最后一种进入可能性是使用 fork。fork 是显式进入一个或多个区域。其他区域再次使用其初始状态激活。
UML 定义了两种历史:浅层历史和深层历史。浅层历史是表示子机最近子状态的伪状态。一个子机最多可以有一个浅层历史。以历史伪状态为目标的转换等同于以最近子状态为目标的转换。非常重要的是,只有一个转换可以源自历史。深层历史是浅层历史,它递归地重新激活最近子状态的子状态。它表示为浅层历史,带有一个星号(圆圈内的 H*)。
历史并不是一个完全令人满意的概念。首先,只有一个历史伪状态,而且只有一个转换可以源自它。因此,它们与正交区域不兼容,因为只能“记住”一个区域。深层历史更糟糕,看起来像是最后添加的。历史必须由一个转换激活,并且只有一个转换源自它,那么如何对源自深层历史伪状态并指向子机最近子状态的转换进行建模呢?此外,它还不灵活,也不接受新型历史。坦率地说,历史听起来很棒,理论上很有用,但 UML 版本并不完全符合要求。因此,MSM 提供了这个有用概念的不同版本。
完成事件(或转换),也称为匿名转换,定义为没有规定触发事件的转换。这意味着,只要保护条件允许,当成为匿名转换源的状态变为活动状态时,此类转换将立即触发。它们在建模算法方面很有用,就像活动图通常会做的那样。在实时领域,它们具有使周期性执行动作的持续时间更容易估算的优点。例如,考虑以下图表。
设计者现在可以随时知道他最多需要 4 次转换。通过估算转换所需的时间,他可以估算出他需要预留的时间框架(实时任务通常以固定的时间间隔执行)。如果他还能估算出动作的持续时间,他甚至可以使用图算法来更好地估算他的定时要求。
如果对于给定事件,启用了多个转换,则称它们处于冲突状态。有两种冲突。
对于给定的源状态,定义了多个由同一事件触发的转换。通常,每个转换中的保护条件决定了哪个转换被触发。
源状态是子机或简单状态,冲突发生在状态内部的转换和由同一事件触发且目标为另一个状态的转换之间。
第一个很简单;只需在转换表中定义两行或多行,具有相同的源和触发器,但具有不同的保护条件。但是,请注意,UML 标准要求这些条件不重叠。如果它们重叠,标准不会说明任何内容,只说这是不正确的,因此实现者可以自由地按自己认为合适的方式实现。在 MSM 的情况下,转换表中最后出现的转换会首先被选择,如果返回 false(表示禁用),库会尝试使用前一个,依此类推。
在第二种情况下,UML 定义最内部的转换会首先被选择,这很有意义,否则将不可能存在退出点伪状态(内部转换将我们带到退出点,从那里包含的状态机可以接管)。
MSM 会自己处理这两种情况,因此设计者只需要专注于其状态机和 UML 的细微之处(不重叠的条件),而无需自己实现此行为。