版权归原作者所有,如有侵权,请联系我们

[科普中国]-状态图

科学百科
原创
科学百科为用户提供权威科普内容,打造知识科普阵地
收藏

状态图(Statechart Diagram)是描述一个实体基于事件反应的动态行为,显示了该实体如何根据当前所处的状态对不同的事件做出反应。通常我们创建一个UML状态图是为了以下的研究目的:研究类、角色、子系统、或组件的复杂行为。

基本介绍状态图用于显示状态机(它指定对象所在的状态序列)、使对象达到这些状态的事件和条件、以及达到这些状态时所发生的操作。1

动态建模状态机用于对模型元素的动态行为进行建模,更具体地说,就是对系统行为中受事件驱动的方面进行建模(请参见概念:事件与信号)。状态机专门用于定义依赖于状态的行为(即根据模型元素所处的状态而有所变化的行为)。其行为不会随着其元素状态发生变化的模型元素不需要用状态机来描述其行为(这些元素通常是主要负载管理数据的被动类)。

状态机由状态组成,各状态由转移链接在一起。状态是对象执行某项活动或等待某个事件时的条件。转移是两个状态之间的关系,它由某个事件触发,然后执行特定的操作或评估并导致特定的结束状态。图 1 描绘了状态机的各种元素。一个简单的编辑器可被视为有限的状态机,其状态为Empty(空)、Waiting for a command(等待命令)和 Waiting for text(等待文本)。事件 Load file(装载文件)、Insert text(插入文本)、Insert character(插入字符)和Saveandquit(保存并退出)导致了状态机中的转移。下面的图 2 描绘了编辑器的状态机。2

状态状态是对象执行某项活动或等待某个事件时的条件。对象可能会在有限的时间长度内保持某一状态。状态具有以下几项特征:

|| ||

转移转移是两个状态之间的关系,它表示当发生指定事件并且满足指定条件时,第一个状态中的对象将执行某些操作并进入第二个状态。当发生这种状态变更时,即“触发”了转移。在触发转移之前,可认为对象处于“源”状态;在触发转移之后,可认为对象处于“目标”状态。转移具有以下几项特征:

|| ||

一个转移可能有多个源状态,在这种情况下,它将呈现为一个从多个并行状态出发的结合点;一个转移也可能有多个目标状态,在这种情况下,它将呈现为一个到多个并发状态的叉形图。

事件触发器在状态机环境中,事件是指可触发状态转移的激励的发生。事件可能包括信号、调用、时间推移或状态变更。信号或调用可能具有其值可用于转移的参数,其中包括警戒条件和操作的表达式。也可能会有无触发器的转移,这样的转移没有事件触发器。这种转移也被称为完成转移,它们在源状态完成其活动后将被隐含触发。

警戒条件当转移的触发事件发生时,将对警戒条件进行求值。只要警戒条件不重叠,就可能会有来自同一源状态并具有同一事件触发器的多个转移。在事件发生时,只为转移进行一次警戒条件求值。该布尔表达式可能会引用对象的状态。

操作操作是可执行的、不可分割的计算过程,这意味着,它不会被事件中断,而会一直运行到结束为止。它与活动正好相对,因为活动可能被其他事件中断。操作可以包括操作调用(调用状态机的拥有者以及其他可见对象)、创建或破坏其他对象、或者向另一个对象发送信号。在发送信号的情况下,信号名称以关键字“send”为前缀。

进入退出操作每当进入或退出状态时,进入和退出操作将分别允许发出同一操作。这可以通过进入和退出操作来顺利地完成,而不必明确地将操作放在每个输入或输出转移上。进入和退出操作可能没有实参或警戒条件。位于模型元素的状态机顶层的进入操作可能具有特定的参数,这些参数代表了在创建该模型元素时状态机所接收到的实参。

内部转移内部转移使事件可以在不退出状态的情况下在状态内得到处理,从而可避免触发进入或退出操作。内部转移可能会有带参数和警戒条件的事件,它们所代表的基本上是中断处理程序。

延迟的事件延迟的事件是其处理过程被推迟的事件,它们的处理过程要到事件不被延迟的状态被激活时才会执行。当该状态被激活时,将触发该事件,同时可能导致转移(好象该事件刚刚发生)。要实施延迟的事件,需要有事件的内部队列。如果事件已发生但被列为延迟,它就会被添加到队列中。当对象进入了不会使事件延迟的状态时,将立即从该队列中取出这些事件。

子状态简单状态是没有子结构的状态。具有子状态(嵌套状态)的状态被称为复合状态。子状态可能被嵌套到任意级别。嵌套的状态机最多可能有一个初始状态和一个终止状态。通过显示某些状态只能在特定环境(包含状态)中存在,子状态可以简化复杂的平面状态机。

转移的源状态是包含复合状态之外的源状态,其目标状态可能是复合状态或子状态。如果其目标状态是复合状态,嵌套的状态机就必须包括一个初始状态,在进入复合状态之后并在发出它的进入操作(如果有)之后,控制权将被传递给该初始状态。如果其目标状态是嵌套状态,那嵌套状态的进入操作(如果有)后,控制权将被传递给该嵌套状态。

从复合状态出发的转移可能会以复合状态或子状态作为它的源状态。在这两种情况下,控制权先离开嵌套状态(并在可能的情况下发出它的退出操作),然后离开复合状态(并在可能的情况下发出它的退出操作)。其源状态为复合状态的转移基本上会中断嵌套状态机的活动。1

历史状态除非另有指定,当转移进入复合状态时,嵌套状态机的操作将从初始状态开始重新执行(除非转移直接以子状态为目标)。历史状态使状态机可以重新进入在它退出复合状态之前的最后一个活动子状态。图 4 显示了如何使用历史状态的示例。3

建模技术状态机最多地用于建立对象在其生命期内的行为模型。当对象具有依赖于状态的行为时,尤其需要使用状态机。可能具有状态机的对象包括:类、子系统、用例、接口(以声明实现该接口的对象必须满足的状态)和协议(以声明实现该协议的对象必须满足的状态)。并非所有对象都需要有状态机。如果对象的行为很简单,只是存储或检索数据,那么该对象的行为就与状态无关,它的状态机也没有多少用处。

要建立对象生命期的模型,需要包括三个事项:指定对象可以响应的事件、指定对这些事件作出的响应以及指定过去行为对当前行为的影响。对象生命期的建模还涉及到确定对象有意义地响应事件的顺序,即从创建对象时开始,继续到该对象被破坏时为止。

要建立对象生命期的模型:

将状态机的环境设置为类、用例或整个系统。

如果环境是类或用例,则要收集相邻的类,其中包括父类或通过关联关系或依赖关系可以接触到的类。这些相邻类是操作的候选目标,并且是可以包括在警戒条件中的候选目标。

如果环境是整个系统,则要将重点集中到系统的一个行为上,然后考虑在该方面涉及到的对象的生命期。整个系统的生命期通常会大得无法成为有意义的重点。

确定对象的初始状态和终止状态。如果初始和终止状态具有前提条件和后续条件,也应将这些条件定义出来。

确定对象要响应的事件。这些事件可以在对象的接口或协议中找到。

按照从初始状态到终止状态的顺序,列出对象可能处于的顶层状态。将这些状态与相应事件所触发的转移连接起来。然后添加这些转移。

确定所有进入操作或退出操作。

通过使用子状态来扩展或简化状态机。

检查状态机中的所有事件触发转移是否与该对象实现的接口或协议所期望的事件相符。同样,检查对象的接口或协议所期望的所有事件是否都得到了状态机的处理。最后,确定要在哪些地方明确地忽略事件(如延迟的事件)。

检查状态机中的所有操作是否都得到了包含对象的关系、方法和操作的支持。

跟踪状态机,将它与事件及其响应的预期序列进行比较。搜索无法达到的状态以及状态机无法继续向前的状态。

如果要重新布置或重新构建状态机,需检查并确保语义没有发生变更。3

提示与技巧当给定一项选择时,要使用状态机的可视语义,而不要写出详细的转移代码。例如,不要用几个信号触发一个转移,然后使用详细代码来管理以不同的方式依赖于信号的控制流。应使用由单独的信号来触发的单独转移。在隐藏了附加行为的转移代码中,要避免使用条件逻辑。

根据在状态期间等待的事件或正在发生的事件来命名状态。记住,状态不是“时间点”;它是状态机等待某个事件发生的时间段。例如,“waitingForEnd”这一名称比“end”更好;“timingSomeActivity”比“timeout”更好。不要让状态的名称看起来象是操作名。

在一个状态机内唯一地命名所有状态和转移;这将便于进行源级别的调试。

谨慎使用状态变量;不要在创建新状态时使用它们。如果状态不多,很少带有或不带有依赖于状态的行为,并且很少有或根本没有可能与包含状态机的封装体并行或独立的行为,就可以使用状态变量。如果有复杂的、依赖于状态的潜在并行行为,或者如果必须处理的事件可能来自于包含状态机的封装体之外,则应考虑使用构件封装体。

如果单个图中的状态超过 5 * 2 个,就应考虑使用子状态。在这里可以应用我们的常识:在一个非常规则的模式中可以有十个状态,但如果两个状态之间具有四十个转移,显然就需要重新考虑了。务必要使状态机易于理解。

使用触发事件的事件和/或在转移期间发生的事件为转移命名。选择更加易于理解的名称。

当您看见一个选择点时,应考虑是否可以将作出该选择的职责委托给另一个构件,以便将其作为一组将不同的信号提供给封装体遵照执行(例如,代替对消息->数据 > x 的选择),并考虑是否可以让发送方或另一中间主角来作出决定,然后通过在信号名称中明确显示该决定的方式发送信号(例如,使用名为 isFull 和 isEmpty 的信号,而不是以值命名信号并检查消息数据)。

为在选择点中回答的问题指定描述性的名称,例如“isThereStillLife”或“isItTimeToComplain”。

在任何给定的封装体中,尽量使选择点名称保持唯一(其原因与转移名称需保持唯一相同)。

转移的代码段是否太长?是否应使用函数来代替它们,是否将常用代码段记录为函数?转移应该类似于高层的伪代码,并且应当遵循与 C++ 函数相同或更严格的长度规则。例如,代码超过 25 行的转移可被认为是过长。

应根据函数执行的操作来命名函数。

要特别注意进入和退出操作:在进行更改后忘记更改相应进入和退出操作的情况尤其容易发生。

退出操作可用于提供安全性功能,例如,从“heaterOn”状态中的退出操作将关闭加热器,在这里,操作被用来强制执行一个断言语句。

通常,除非状态机是抽象的并且将由包含元素的子类来进行改进,否则子状态应包含两个或更多个状态。

应该用选择点来代替操作或转移中的条件逻辑。选择点容易被看到,而代码中的条件逻辑则是不可见的,很容易被忽略。

避免使用警戒条件。

如果事件触发了几个转移,将无法控制首先对哪个警戒条件求值。这会产生无法预料的结果。

可能有多个警戒条件为“True”,但随后只能有一个转移。所选择的路径是无法预料的。

警戒条件是不可见的;要“看见”它们的出现更是困难。

避免使用类似流程图的状态机。

这可能表示您试图对并不实际存在的抽象概念进行建模,例如:

使用一个封装体来对最适合于数据类的行为进行建模,或

通过使用紧密耦合的数据类和封装体类来对数据类建模(例如,数据类用于向四周传递类型信息,但封装体类包含了应与数据类相关联的大部分数据)。

状态机的这种错误用法可以通过以下故障现象来识别:

被发送给“自己”的消息,主要是为了重复使用代码

几乎没有状态,但有很多选择点

在某些情况下没有循环的状态机。在流程控制应用程序中,或者在试图控制一个事件序列时,这样的状态机是有效的;如果它们在分析过程中出现,则表示状态机已退化为流程图。

当发现问题时,应采取以下措施:

考虑将封装体分解为职责更明确的小单元,

将更多的行为转移到与有问题的封装体相关联的数据类中。

将更多的行为转移到封装体类函数中。

制作更有意义的信号,以避免对数据的依赖。

使用抽象状态机进行设计

抽象状态机是需要添加更多细节才能实际使用的状态机。抽象状态机可用于定义可复用的一般行为,这些行为将在随后的模型元素中得到进一步的改进。请考虑图 5 中的抽象状态机。例如,图 5 中的简单状态机代表了实时系统中许多不同元素类型的最抽象的行为(自动“控制”)。尽管不同的元素类型都具有这一最高抽象程度,但是,根据其目的,它们可能在“Running”状态中具有非常不同的详细行为。因此,该状态机最有可能在某个抽象封装体类中定义,该封装体类被用作不同的专用封装体类的根类。4

因此,我们将使用继承来定义该抽象状态机的两种不同的改进形式。图 6 显示了这两种改进形式 R1 和 R2。为清晰起见,我们使用浅灰笔来描绘从父类继承而来的元素。

这两种改进形式的明显差异在于它们分解“Running”(正在运行)状态的方式,以及它们扩展原“start”(开始)转移的方式。当然,只有当改进形式已知,因而未在抽象类中使用单个端到端转移来实施时,才能作出这些选择。

链式状态对于上述的改进类型而言,能够同时“继续”输入转移和输出转移是基本的能力。结合使用进入点、终止状态和继续转移似乎就足以提供这些语义。但是,如果有多个不同的转移需要扩展,这就行不通了。

在这种情况下,抽象行为模式需要的是用一种方法把在单个运行至结束的步骤中全部执行的两个或更多个转移段链接起来。这意味着,要将进入分层结构状态的转移拆分成一个在状态边界处有效终止的进入部分,以及一个在状态内继续的扩展部分。同样,将分层嵌套的状态所发出的输出转移分为一个在包含状态边界处终止的部分,以及一个从状态边界继续到目标状态的部分。通过引入链式状态的概念,可以在 UML 中获得这一效果。它通过 UML 状态概念的原型 («chainState») 来建模。该状态的唯一目的是将更多的自动(无触发器)转移“链接”到输入转移上。链式状态没有内部结构:没有进入操作,没有内部活动,没有退出操作。它也没有由事件触发的转移。但它可以有任意数量的输入转移。链式状态可能有不带触发事件的输出转移;当输入转移激活链式状态时,该转移将自动触发。这种状态的目的是将输入转移链接到独立的输出转移上。在输入转移和被链接的输出转移之间,一个状态连接到包含状态内部的另一个状态,而该状态又连接到包含状态外部的另一个状态。引入链式状态的目的是将包含状态的内部规约与其外部环境分隔开,这也是一种封装。

实际上,链式状态代表的是一种“串通”状态,它用于将某个转移链接到一个特定的继续转移。如果没有定义继续转移,转移就会在链式状态中终止。要使操作继续,就必须触发包含状态中的某一转移。

图 7 中的示例状态机段显示了链式状态及其符号。链式状态在状态机图中表示为在适当分层结构状态内的白色小圆(该符号类似于与它们相似的初始状态和终止状态)。圆是链式状态原型的原型图标,为了方便,通常把它们描绘在边界附近。(实际上,另一种标志法是把它们描绘在包含状态的边界上,类似于封装体上的端口符号。)

该示例中,被链接的转移包括三个被链接的转移段 (e1/a11-/a12-/a13)。当收到信号 e1 时,将调用标记为 e1/a11 的转移,执行它的操作 a11,然后进入链式状态 c1。接着,调用 c1 和 c2 之间的继续转移。最后,由于 c2 也是链式状态,所以从 c2 转移到 S21。如果沿这些路径的状态都有退出和进入操作,那么实际的操作执行顺序将是:

S11 的退出操作

操作 a11

S1 的退出操作

操作 a12

S2 的进入操作

操作 a13

S21 的进入操作

以上所有操作都将在单个运行至结束的步骤中执行。

应将这些操作与直接转移 e2/a2 的操作执行语义进行对比,后者是:

S11 的退出操作

S1 的退出操作

操作 a2

状态 S2 的进入操作

状态 S21 的进入操作 © 1987 - 2001RationalSoftware Corporation。版权所有。

通用准则当行为的改变和状态有关时才创建状态图。

敏捷建模(AM) ( Ambler 2002)的原则--最大化项目干系人的投资--建议你只有当模型能够提供正面价值的时候才创建模型。 如果一个实体,比如一个类或组件,表示的行为的顺序和当前的状态无关,那么画一个UML状态图可能是没有什么用处的。例如一个SurfaceAddrESs类就很简单,表示了那些你将会在系统中显示和操作的数据,因此一个UML状态图就没有任何相关之处。而一个Seminar对象就非常的复杂,学生注册这样一个事件将会根据它的当前状态有不同的反应,就像你在图1中看到的。5

把初始状态放置在左上角。

如你在图1所见的,初始状态被建模成一个实心圈,把初始状态放在左上角反映西方人的阅读文化的习惯。

把最终状态放置在右下角。

如你在图1所见,最终状态被建模为一个带边界的实心圆。把最终状态放右下角反映了西方的文化的从左到右,从上到下的阅读习惯。

状态指南状态是一个实体的行为模式的某个阶段。 状态的表示是通过实体的属性值。 例如,在图1中,当seminar被标记为open,并且存在空位的时候,seminar就处于Open For Enrollment的状态。

状态名称要简单但应具有描述性。

象Open For Enrollment和PropOSed这种的状态名称很容易理解,从而提高了图⒈的沟通价值。理论上状态名称应该是现在时,但是用过去式写成的诸如Proposed的名称要比用现在时写成的诸如Is Proposed的名称好的多。

避免"黑洞"状态。

黑洞状态是那种只有变换进来但没有任何变换发出的状态,这种情况要么由于该状态是一个最终状态,要么就是你已经错过了一个或多个变换变换。

避免"奇迹"状态。

奇迹状态是那种只有变换发出但没有任何变换进来的状态,这种情况要么由于该状态是一个起点,要么就是你已经错过了一个或多个变换变换。

建模指南建模子状态图1中展示的UML状态图是不完整的,因为它没有建模Seminar的poST - enrollment(注册后)状态。 图2建模了一个Seminar的完整的生命周期,把图1描述为一个新的包括子状态集合的Enrollment的复合状态,也称作超状态。 注意按理说你会像图1的模型那样处理标记,但为了简化起见在原先变换上的标记都没有包括在内。当一个现有状态表现出复杂的行为时,建模子状态就是有意义的,从而促使你来研究它的子状态。 当几个现有状态共用一个通用的入口条件或出口条件( DouglASs 1999)时,引入超状态是有意义的,在图1中你可以看到所有的状态共用一个通用的closed变换,以到达最终状态。

子状态变换和图1中每一个子状态都拥有一个cancelled变换不同,在图2中你可以看到cancelled变换仅用于描述Enrollment超状态,这使图形得到简化。 如果子状态都共享一个入口变换或出口变换,都可以使用一个同样的方法。 变换上的警戒点和动作(如果有)也应该使相等的。

为复杂的实体创建一个分层的状态图

虽然这种表现子状态的方法是很好使的,但是最终的图可能变得相当复杂--我们只要设想一下如果Being Taught状态也有子状态的话,图2会变成什么样就知道了。 一个替代的方法是创建一个分层的UML状态图。 例如,图3表示高阶视图,而图1描述了一个细节视图。这种方法的好处是如果需要的话,马上就可以建立一张详图来研究Being Taught状态。

最高阶的状态图总有初始态和最终态

一个高阶的UML状态图,例如图2描述的这样,应该表示实体的完整的生命周期,包括"出生"和最后的"死亡"。 低阶的图未必包含初始状态和最终状态,特别是那些建模一个实体的生命周期的"中间状态"的图。

变换和动作变换是从一种状态到另一种状态的序列,它可能是通过一个事件触发的。简而言之就是被建模的实体的内部或外部的行为。 对一个类来说,变换一般是将会导致状态的重要改变的操作调用的结果,因此我们需要了解一点,并不是所有的方法调用都会导致变换产生的,这一点非常重要。 一个动作就是某个东西,对类来说就是一个操作,被建模的实体所调用的操作。

用实现语言的命名规则命名软件动作

图1中的动作遵循Java操作的命名规则( Vermeulen et. 2000),因为系统使用

用叙述性文字命名角色动作

UML状态图可用于建模非软件实体的生命周期,特别是UML图上的角色。 例如学生角色就可能有诸如Accepted、Full Time、Part Time、Graduated、Masters、Doctoral、和Post - Doctoral等状态,以显示各人的不同行为。 当你在建模现实世界的角色时,与软件中Student类不同的是,状态间的变换最好是使用叙述性文字来描述,例如drop seminar和pay fees,而不是dropSeminar ()和payFees (),因为现实生活中的人是做事情,而不是执行操作。

只有对所有的入口变换都合适时才注明入口动作

在图1中你可以看到Closed To Enrollment状态的入口中操作notifyInstructor ()都是经由entry/动作标记来调用的。 这暗示着每次进入状态时都需要调用该操作,如果你不希望每次都发生,那么就把动作关联到特定的入口变换。 例如,addStudent ()动作是在student enrolled变换到Open For Enrollment变换发生,而在到opened变换则不会发生,这是因为每次你在进入该状态并不需要增加一个学生。

只有对所有的出口变换适合时才注明出口动作

出口动作,用exit/标记来表示,工作方式类似于入口动作。

只有当你想终止并再进入该状态时才建模递归变换

一个递归的变换是那些两个端点都拥有相同状态的变换。 一个重要的暗示是实体从状态出来,又回到原有的状态,因此,那些由于entry/或exit/动作标记而被调用的任何一种操作都可能被自动调用。 图1的Open For Enrollment状态就是这种递归变换的例子,因此当前班级大小就在入口处被记录下来。

用过去式命名转换事件

图1中的转换事件,例如seminar split和cancelled,是使用过去式命名的,反映了这样一个事实:变换是事件的结果--因为事件发生在变换之前,因此应该用过去式命名。

把转换标记放在接近源状态的地方

虽然图1比较复杂,变换标记尽可能放在靠近来源的地方,例如seminar split和student enrolled。 Furthermore, the labels were justified (left and right respECtively) to help visually place them close to the source state.

为了更易于判断哪个标记和变换是一起的,按照如下的规则来放置变换标记:

在变换线条上的从左到右。

在变换线条下的从右到左。

变换线条右边的往下。

变换线条左边的往上。

警戒点 一个警戒点是为了穿过一个转换而必须为真的一个条件。

警戒点不应该重叠

离开状态的相似变换上的警戒点必须彼此一致。 举例来说,x 0的警戒点是一致的,而x = 0的警戒点就不是一致的,因为他们重叠了,它并没有明确的指出当x为0时将发生什么。在图1中,你可以看到警界点的一致性,从填写注册表活动出发的该学生划线变换上的警戒点没有重叠,决策点上的警戒点也一样。

为可视化的定位警戒点而引入接合点。

在图2中你可以看到从Being Taught触发student dropped事件存在两个变换,而图3中仅有一个,变换被合并了,因此我们需要一个接合点(填满的圆)。 这种方法的好处是现在图上的两个警戒点更彼此接近了,更容易看出警戒点是否重叠。

警戒点不必配套

一个状态的变换警戒点有可能是不完整的。例如,一个bank account对象可能从Open状态变换到Needs Authorization状态,这时需要一个大额存款"large deposit"的警戒点。可是,一个带有"small deposit"的警戒点的deposit变换可能并不需要建模,它是被隐含的,我们遵循了AM的实践--简单的描述模型和仅仅包括相关的信息。

一致的命名警戒点

图1包含了诸如seat avAIlable和no seat available的警戒点,两个警戒点的描述是一致的。 然而,诸如seats left、no seat left、no seats left、no seats available、seat unavailable之类的描述就是不一致,而且难于理解的。

本词条内容贡献者为:

徐恒山 - 讲师 - 西北农林科技大学