PlantUML——状态图

PlantUML状态图

状态图(Statechart Diagram)是UML中对系统动态方面建模的图之一,它通过建立类对象的生命周期模型来描述对象随时间变化的动态行为。由于系统中对象的状态最易发现和理解,所以建模时我们往往首先考虑基于状态之间的控制流。

1、状态图的基本概念

状态图用于描述模型元素实例(如对象或交互)的行为。它适用于描述状态和动作的顺序,不仅可以展现一个对象拥有的状态,还可以说明事件如何随着时间的推移来影响这些状态。

1.1、状态图的定义

1.1.1、状态机

广义上,状态机是一种记录下给定时刻状态的设备,它可以根据各种不同的输入对每个给定的变化而改变其状态或引发一个动作。比如,计算机就是一个状态机,各种客户端软件、Web上的各种交互页面都是状态机。

在UML中,状态机由对象的各个状态和连接这些状态的转换组成,是展示状态与状态转换的图。在面向对象的软件系统中,一个对象无论多么简单或者多么复杂,都必然会经历一个从开始创建到最终消亡的完整过程,这个过程通常被称为对象的生命周期。一般说来,对象在其生命期内是不可能完全孤立的,它必然会接受消息来改变自身或者发送消息来影响其他对象。而状态机就是用于说明对象在其生命周期中响应事件所经历的状态序列以及其对这些事件的响应。在状态机的语境中,一个事件就是一次激发的产生,每个激发都可以触发一个状态转换。

状态机由状态转换事件活动动作5部分组成:

  • 状态指的是对象在其生命周期中的一种状况,处于某个特定状态中的对象必然会满足某些条件、执行某些动作或者是等待某些事件。
  • 转换指的是两个不同状态之间的一种关系,表明对象将在第一个状态中执行一定的动作,并且在满足某个特定条件下由某个事件触发进入第二个状态。
  • 事件指的是发生在时间和空间上的对状态机来讲有意义的那些事情。事件通常会引起状态的变迁,促使状态机从一种状态切换到另一种状态。
  • 活动指的是状态机中进行的非原子操作。
  • 动作指的是状态机中可以执行的那些原子操作,所谓原子操作指的是它们在运行的过程中不能被其他消息所中断,必须一直执行下去,最终导致状态的变更或者返回一个值。

通常一个状态机依附于一个类,并且描述该类的实例(即对象)对接收到的事件的响应。除此之外,状态机还可以依附于用例、操作等,用于描述它们的动态执行过程。在依附于某个类的状态机中,总是将对象孤立地从系统中抽象出来进行观察,而来自外部的影响都抽象为事件。

在UML中,状态机常用于对系统行为中受事件驱动的方面进行建模。不过状态机总是一个对象、协作或用例的局部视图。由于它考虑问题时将实体与外部世界相互分离,所以适合对局部、细节进行建模。

1.1.2、状态图

一个状态图(Statechart Diagram)本质上就是一个状态机,或者是状态机的特殊情况,它基本上是一个状态机中的元素的一个投影。状态图描述了一个实体基于事件反应的动态行为,显示了该实体如何根据当前所处的状态对不同的事件做出反应。

在UML中,状态图由表示状态的节点和表示状态之间转换的带箭头的直线组成。状态的转换由事件触发,状态和状态之间由转换箭头连接。每一个状态图都有一个初始状态(实心圆)​,用来表示状态机的开始。还有一个终止状态(半实心圆)​,用来表示状态机的终止。状态图主要由元素状态、转换、初始状态、终止状态和判定等组成,一个简单的状态图如图所示。

1. 状态

状态用于对实体在其生命周期中的各种状况进行建模,一个实体总是在有限的一段时间内保持一个状态。状态由一个带圆角的矩形表示,状态的描述应该包括:名称入口出口动作内部转换嵌套状态

  • 状态名:状态名指的是状态的名字,通常用字符串表示,其中每个单词的首字母大写。状态名可以包含任意数量的字母、数字,除":"以外的一些符号,可以较长,连续几行。但是一定要注意一个状态的名称在状态图所在的上下文中应该是唯一的,能够把该状态和其他状态区分开。入口和出口动作:一个状态可以具有或者没有入口和出口动作。入口和出口动作分别指的是进入和退出一个状态时所执行的"边界"动作。
  • 内部转换:内部转换指的是不导致状态改变的转换。内部转换中可以包含进入或者退出该状态应该执行的活动或动作。
  • 嵌套状态:状态分为简单状态(simple state)和组成状态(composite state)。简单状态是在语义上不可分解的、对象保持一定属性值的状况,简单状态不包含其他状态;而组成状态是内部嵌套有子状态的状态,在组成状态的嵌套状态图部分包含的就是此状态的子状态。如图所示为一个简单的状态。

    2. 转换

在UML的状态建模机制中,转换用带箭头的直线表示,一端连接源状态,箭头指向目标状态。转换还可以标注与此转换相关的选项,如事件、监护条件和动作等,如图所示。要注意,如果转换上没有标注触发转换的事件,则表示此转换自动进行。

在状态转换中需要注意的5个概念:

  • 源状态(source state):指的是激活转换之前对象处于的状态。如果一个状态处于源状态,当它接收到转换的触发事件或满足监护条件时,就激活一个离开的转换。
  • 目标状态(target state):指的是转换完成后对象所处的状态。
  • 事件触发器(event trigger):指的是引起源状态转换的事件。事件不是持续发生的,它只发生在时间的一点上,对象接收到事件,导致源状态发生变化,激活转换并使监护条件得到满足。
  • 监护条件(guard condition):是一个布尔表达式。当接收到触发事件要触发转换时,对该表达式求值。如果表达式为真,则激活转换;如果表达式为假,则不激活转换,所接收到的触发事件丢失。
  • 动作(action):是一个可执行的原子计算。

3. 初始状态

每个状态图都应该有一个初始状态,它代表状态图的起始位置。初始状态是一个伪状态(一个和普通状态有连接的假状态)​,对象不可能保持在初始状态,必须要有一个输出的无触发转换(没有事件触发器的转换)​。

通常初始状态上的转换是无监护条件的,并且初始状态只能作为转换的源,而不能作为转换的目标。在UML,一个状态图只能有一个初始状态,用一个实心的圆表示,如图所示。

4. 终止状态

终止状态是一个状态图的终点,一个状态图可以拥有一个或者多个终止状态。对象可以保持在终止状态,但是终止状态不可能有任何形式的触发转换,它的目的就是为了激发封装状态上的完成转换。因此,终止状态只能作为转换的目标而不能作为转换的源,在UML中终止状态用一个含有实心圆的空心圆表示,如图所示。

要注意的是,对于一些特殊的状态图,可以没有终止状态。如图所示为一部电话的状态图,在这个状态图中没有终止状态。因为不管在什么样的情况下,电话的状态都是在"空闲"和"忙"之间转换。

5. 判定

活动图和状态图中都有需要根据给定条件进行判断,然后根据不同的判断结果进行不同的转换的情况。实际就是工作流在此处按监护条件的取值发生分支,在UML中判定用空心菱形表示,如图所示。

1.2、状态图的作用

状态图用于对系统的动态方面建模,适合描述跨越多个用例的对象在其生命周期中的各种状态及其状态之间的转换。这些对象可以是类、接口、构件或者节点。状态图常用于对反应型对象建模,反应型对象在接收到一个事件之前通常处于空闲状态,当这个对象对当前事件作出反应后又处于空闲状态等待下一个事件。

如果一个系统的事件个数比较少,并且事件的合法顺序比较简单,那么状态图的作用就没有那么明显。但是对于一个有很多事件并且事件顺序复杂的系统来说,如果没有一个好的状态图,就很难保证程序没有错误。

状态图的作用主要体现在以下几个方面:

  • 状态图清晰地描述了状态之间的转换顺序,通过状态的转换顺序也就可以清晰地看出事件的执行顺序。如果没有状态图我们就不可避免地要使用大量的文字来描述外部事件的合法顺序。
  • 清晰的事件顺序有利于程序员在开发程序时避免出现事件错序的情况。例如,对于一个网上销售系统,在用户处于登录状态前是不允许购买商品的,这就需要程序员在开发程序的过程中加以限制。
  • 状态图清晰地描述了状态转换时所必须的触发事件、监护条件和动作等影响转换的因素,有利于程序员避免程序中非法事件的进入。例如,当飞机起飞前半小时不允许售票,在状态图中就可以清晰地看到,可以提醒程序员不要遗漏这些限制条件。
  • 状态图通过判定可以更好地描述工作流因为不同的条件发生的分支。例如,当一个班级的人数少于10人的时候需要和其他班级合为一起上课,大于10人则单独上课,在状态图中就可以很明确地表达出来。

总之,一个简洁完整的状态图可以帮助一个设计者不会遗漏任何事情,最大程度的避免程序中的错误。

2、状态图的组成

2.1、状态

状态是状态图的重要组成部分,它描述了一个类对象生命周期中的一个时间段。详细的说就是:在某些方面相似的一组对象值;对象执行持续活动时的一段事件;一个对象等待事件发生时的一段事件。

因为状态图中的状态一般是给定类的对象的一组属性值,并且这组属性值对所发生的事件具有相同性质的反应。所以,处于相同状态的对象对同一事件的反应方式往往是一样的,当给定状态下的多个对象接受到相同事件时会执行相同的动作。但是,如果对象处于不同状态,会通过不同的动作对同一事件做出不同的反应。

要注意的是,不是任何一个状态都是值得关注的。在系统建模时,我们只关注那些明显影响对象行为的属性,以及由它们表达的对象状态。对于那些对对象行为没有什么影响额度的状态,我们可以不用理睬。

状态可以分为简单状态和组成状态。简单状态指的是不包含其他状态的状态,简单状态没有子结构,但是它可以具有内部转换、进入退出动作等。组成状态包含嵌套的子状态。

1. 状态名

在实际使用中,状态名通常是直观、易懂、能充分表达语义的名词短语,其中每个单词的首字母要大写。状态还可以匿名,但是为了方便起见,最好为状态取个有意义的名字,状态名字通常放在状态图标的顶部。

2. 内部活动

状态可以包含描述为表达式的内部活动。当状态进入时,活动在进入动作完成后就开始。如果活动结束,状态就完成,然后一个从这个状态出发的转换被触发。否则,状态等待触发转换以引起状态本身的改变。如果在活动正在执行时转换触发,那么活动被迫结束并且退出动作被执行。

3. 内部转换

状态可能包含一系列的内部转换,内部转换因为只有源状态而没有目标状态,所以内部转换的结果并不改变状态本身。如果对象的事件在对象正处在拥有转换的状态时发生,那内部转换上的动作也被执行。激发一个内部转换和激发一个外部转换的条件是相同的。但是,在顺序区域里的每个事件只激发一个转换,而内部转换的优先级大于外部转换。

内部转换和自转换不同,在后者中,外部转换发生时,会引发所有嵌在具有自转换状态中的状态执行退出动作。在转向当前状态的自转换过程中,退出动作被执行,退出后再重新进入。换句话说,自转换可以强制从嵌套状态退出,但是内部转换不能。

当状态向自身转换时,就可以用到内部转换。例如,对于一个网站的后台管理程序,管理员登录后处于登录状态。它的入口动作是验证账号密码,出口动作是清除登录记录。当管理员在登录状态下编辑资源的时,就可以使用内部转换,不触发入口动作和出口动作的执行。

4. 入口和出口动作

状态可能具有入口和出口动作。这些动作的目的是封装这个状态,这样就可以不必知道状态的内部状态而在外部使用它。入口动作和出口动作原则上依附于进入和出去的转换,但是将它们声明为特殊的动作可以使状态的定义不依赖状态的转换,因此起到封装的作用。

当进入状态时,进入动作被执行,它在任何附加在进入转换上的动作之后而在任何状态的内部活动之前执行。入口动作通常用来进行状态所需要的内部初始化。因为不能回避一个入口动作,任何状态内的动作在执行前都可以假定状态的初始化工作已经完成,不需要考虑如何进入这个状态。

状态退出时执行的退出动作,会在任何内部活动完成之后并且在任何状态转换动作之前执行。无论何时从一个状态离开都要执行一个出口动作来进行后处理工作。当出现代表错误情况的高层转换使嵌套状态异常终止时,出口动作就变得很重要。出口动作可以处理这种情况以使对象的状态保持前后一致。

5. 历史状态

组成状态可能包含历史状态(History State)​,历史状态本身是个伪状态,用来说明组成状态曾经有的子状态。

一般情况下,当状态机通过转换进入组成状态嵌套的子状态时,被嵌套的子状态要从子初始状态进行。但是,如果一个被继承的转换引起从复合状态就自动退出,状态会记住当强制性退出发生的时候处于活动的状态。这种情况下,就可以直接进入上次离开组成状态时的最后一个子状态,而不必从它的子初始状态开始执行。

历史状态可以有来自外部状态或者初始状态的转换,也可以是一个没有监护条件的外向转换;转换的目标是默认的历史状态。如果状态区域从来没有进入或者已经退出,到历史状态的转换会到达缺省的历史状态。

历史状态代表上次离开组成状态时的最后一个活动子状态,它用一个包含字母"H"的小圆圈表示,如图所示。

历史状态虽然有它的优点,但是它过于复杂,而且不是一种好的实现机制,尤其是深历史状态(DeepHistory)更容易出问题。在建模的过程中,应该尽量避免历史机制,使用更易于实现的机制。

2.2、转换

转换用于表示一个状态机的两个状态之间的一种关系,即一个在某初始状态的对象通过执行指定的动作并符合一定的条件下进入第二种状态。

在这个状态的变化中,转换被称作激发。在激发之前的状态叫作源状态,在激发之后的状态叫作目标状态。简单转换只有一个源状态和一个目标状态。复杂转换有不止一个源状态和(或)有不止一个目标状态。

除了源状态和目标状态,一个转换还包括事件触发器、监护条件和动作,在转换中,这5部分信息并不一定都同时存在,有一部分信息可能会缺少。

1. 外部转换

外部转换是一种改变状态的转换,也是最普通最常见的一种转换。在UML中,它用从源状态到目标状态的带箭头的线段表示,其他属性以文字串附加在箭头旁,如图所示。

注意,只有内部状态上没有转换时,外部状态上的转换才有资格激发。否则,外部转换会被内部转换所掩盖。

2. 内部转换

内部转换只有源状态,没有目标状态,不会激发入口和出口动作,因此内部转换激发的结果不改变本来的状态。如果一个内部转换带有动作,它也要被执行。内部转换常用于对不改变状态的插入动作建立模型。要注意的是内部转换的激发可能会掩盖使用相同事件的外部转换。

内部转换的表示法与入口动作和出口动作的表示法很相似。他们的区别主要在于入口和出口动作使用了保留字"entry"和"exit"​,其他部分两者的表示法相同。

3. 完成转换

完成转换没有明确标明触发器事件的转换是由状态中活动的完成引起的。完成转换也可以带一个监护条件,这个监护条件在状态中的活动完成时被赋值,而不是活动完成后被赋值。

4. 复合转换

复合转换(complex transition)由简单转换组成,这些简单转换通过分支和合并组合起来。因此,复合转换可以具有多个源状态和多个目标状态。

在现实生活中,我们经常会碰到殊途同归的情况。例如,我们购物后付款可以选择现金支付或者刷卡支付。虽然我们的支付手段不一样,但是我们的出发点都是选购货物,最终的状态也都是付款完成购物,这种情况就要用到复合转换。复合转换可以包含判定或者合并,也可以同时包含两者,如图所示,为一个包含判定和合并的复合转换。

要注意,空心菱形符号不但可以表示判定,也可以表示语义与判定相反的合并。合并的情况需要有两个或更多的输入箭头和一个单独的输出箭头,不需要监护条件。而判定则与合并相反,有一个输入箭头和两个或多个输出箭头,每个路径有不同的监护条件。

5. 监护条件

转换可能具有一个监护条件,监护条件是一个布尔表达式,它是触发转换必须满足的条件。当一个触发器事件被触发时,监护条件被赋值。如果表达式的值为真,转换可以激发;如果表达式的值为假,转换则不能激发;如果没有转换适合激发,事件就会被忽略,这种情况并非错误。如果转换没有监护条件,监护条件就会被认为是真,而且一旦触发器事件发生,转换就激活。

从一个状态引出的多个转换可以有同样的触发器事件。若此事件发生,所有监护条件都被测试,测试的结果如果有超过一个的值为真,也只有一个转换会激发。如果没有给定优先权,则选择哪个转换来激发是不确定的。

注意,监护条件的值只在事件被处理时计算一次。如果其值开始为假,以后又为真,则因为赋值太迟转换不会被激发。除非有另一个事件发生,且令这次的监护条件为真。监护条件的设置一定要考虑到各种情况,要确保一个触发器事件的发生能够引起某些转换。如果某些情况没有考虑到,很可能一个触发器事件不引起任何转换,那么在状态图中将忽略这个事件。

当一个监护条件的情况比较复杂时,为了方便,可以将一个监护条件拆解成一系列简单的监护条件。这一系列监护条件一般是某个触发器事件或监护条件的分支,每一分支都是一个单独的转换。每一个转换的监护条件都是独立的、互斥的、有效的,每一个转换都可以被触发器事件所触发。这条路径上所有的表达式都会在转换访问激发前得到值,然后根据不同的情况进入不同的状态。这一系列的转换不能部分地激发。如图所示为一个监护条件树。

注意,要实现这样的效果,并不是一定要用监护条件树和转换排序,也可以用一套独立的转换实现。之所以选择监护条件树,主要是因为方便。如果用一套独立的转换,要注意每个转换要有它自己的互斥的监护条件。

6. 触发器事件

触发器事件就是能够引起状态转换的事件。如果此事件有参数,这些参数可以被转换所用,也可以被监护条件和动作的表达式所用。触发器事件可以是信号、调用和时间段等。

对应与触发器事件,没有明确的触发器事件的转换称作结束转换(或无触发器转换)​,是在结束时被状态中的任一内部活动隐式触发的。

注意,当一个对象接收到一个事件的时候,如果它没有时间来处理事件,就将事件保存起来。如果有两个事件同时发生,对象每次只处理一个事件,两个事件并不会同时被处理。并且在处理事件的时候,转换必须激活。另外,要完成转换,必须满足监护条件,如果完成转换的时候监护条件不成立,则隐含的完成事件被消耗掉。并且以后即使监护条件再成立,转换也不会被激发。

7. 动作

动作(action)通常是一个简短的计算处理过程或一组可执行语句。动作也可以是一个动作序列,即一系列简单的动作。动作可以给另一个对象发送消息、调用一个操作、设置返回值、创建和销毁对象。

动作是原子性的,所以动作是不可中断的,动作和动作序列的执行不会被同时发生的其他动作影响或终止。动作的执行时间非常短,所以动作的执行过程不能再插入其他事件。如果在动作的执行期间接收到事件,那么这些事件都会被保存,直到动作结束,这时事件一般已经得到返回值。

整个系统可以在同一时间执行多个动作,但是动作的执行应该是独立的。一旦动作开始执行,它必须执行到底并且不能与同时处于活动状态的其他动作发生交互作用。动作不能用于表达处理过程很长的事物。与系统处理外部事件所需要的时间相比,动作的执行过程应该很简洁,以使系统的反应时间不会减少,做到实时响应。

动作可以附属于转换,当转换被激发时动作被执行。它们还可以作为状态的入口动作和出口动作出现,由进入或离开状态的转换触发。活动不同于动作,它可以有内部结构,并且活动可以被外部事件的转换中断。所以活动只能附属于状态中,而不能附属于转换。

下表动作的种类列出了各种动作及描述。

2.3、判定

判定用来表示一个事件依据不同的监护条件有不同的影响。在实际建模的过程中,如果遇到需要使用判定的情况,通常用监护条件来覆盖每种可能,使得一个事件的发生能保证触发一个转换。判定将转换路径分为多个部分,每一个部分都是一个分支,都有单独的监护条件。这样,几个共享同一触发器事件却有着不同监护条件的转换能够在模型中被分在同一组中,以避免监护条件的相同部分被重复。

判定在活动图和状态图中都有很重要的作用。转换路径因为判定而分为多个分支,可以将一个分支的输出部分与另外一个分支的输入部分连接而组成一棵树,树的每个路径代表一个不同的转换。树为建模提供了很大的方便。在活动图中,判定可以覆盖所有的可能,保证一些转换被激发。否则,活动图就会因为输出转换不再重新激发而被冻结。

通常情况下判定有一个转入和两个转出,根据监护条件的真假可以触发不同的分支转换,如图所示。

使用判定仅仅是一种表示上的方便,不会影响转换的语义,如图所示为没有使用判定的情况。

2.4、同步状态

同步条是为了说明并发工作流的分支与汇合。状态图和活动图中都可能用到同步。在UML中,同步用一条线段来表示,如图所示。

并发分支表示把一个单独的工作流分成两个或者多个工作流,几个分支的工作流并行地进行。并发汇合表示两个或者多个并发的工作流在得到同步,这意味着先完成的工作流需要在此等待,直到所有的工作流到达后,才能继续执行以下的工作流。同步在转换激发后立即初始化,每个分支点之后都要有相应的汇合点。如图所示为同步示例图。

要注意同步与判定的区别。同步和判定都会造成工作流的分支,初学者很容易将两者混淆。他们的区别是,判定是根据监护条件使工作流分支,监护条件的取值最终只会触发一个分支的执行。比如,如果有分支A和分支B,假设监护条件为真时执行分支A,那么分支B就不可能被执行。反之则执行分支B,分支A就不可能被执行。而同步的不同分支是并发执行,并不会因为一个分支的执行造成其他分支的中断。

2.5、事件

在状态机中,一个事件的出现可以触发状态的改变。它发生在时间和空间上的一点,没有持续时间。如接受到从一个对象到另一个对象的调用或信号、某些值的改变或一个时间段的终结。

事件可以分成明确或隐含的几种,主要包括:信号事件、调用事件、改变事件和时间事件等。

1. 信号事件(signal event)

信号(signal)是作为两个对象之间的通信媒介的命名的实体,它以对象之间显式通讯为目的。发送对象明确地创建并初始化一个信号实例并把它发送到一个对象或者对象的集合。信号有明确的参数列表。发送者在发信号时明确了信号的变元,发给对象的信号可能触发它们的零个或者一个转换。信号是可泛化的,子信号除了继承了父亲的属性外,也可以增加它自己的属性。子信号可以激发声明为使用它的祖先信号的转换。

信号事件(signal event)指的是一个对象对发送给它的信号的接收事件,它可能会在接收对象的状态机内触发转换。

信号分为异步单路通信和双路通信。其中最基本的信号是异步单路通信。在异步单路通信中,发送者是独立的,不用等待接收者如何处理信号。在双路通信模型中,需要用到多路信号,即至少要在每个方向上有一个信号。发送者和接收者可以是同一个对象。

2. 调用事件(call event)

调用(call)是在一个过程的执行点上激发一个操作,它将一个控制线程暂时从调用过程转换到被调用过程。调用发生时,调用过程的执行被阻断,并且在操作执行中调用者放弃控制,直到操作返回时重新获得控制。

调用事件(call event)指的是一个对象对调用(call)的接收,这个对象用状态的转换而不是用固定的处理过程实现操作。事件的参数是操作的引用、操作的参数和返回引用。调用事件分为同步调用和异步调用,如果调用者需要等待操作的完成,则是同步调用,反之则是异步调用。

当一个操作的调用发生时,如果调用事件符合一个活动转换上的触发器事件,那么它就触发该转换。转换激发的实际效果包括任何动作序列和return(value)动作,其目的是将值返回给调用者。当转换执行结束时,调用者重新获得控制并且可以继续执行。如果调用失败而没有进行任何状态转换,则控制立即返回到调用者。

3. 改变事件(change event)

改变事件指的是依赖与特定属性值的布尔表达式所表示的条件满足时,事件发生改变。修改事件包含由一个布尔表达式指定的条件,事件没有参数。这种事件隐含一个对条件的连续的测试。当布尔表达式的值由假变到真时,事件就发生。要想事件再次发生,必须先将值变成假,否则,事件不会再发生。

我们要小心使用改变事件,因为他表示了一种具有事件持续性的并且可能是涉及全局的计算过程。它使修改系统潜在值和最终效果的活动之间的因果关系变得模糊。可能要花费很大的代价测试改变事件,因为原则上改变时间是持续不断的。因此,改变事件往往用于当一个具有更明确表达式的通信形式显得不自然的时候。

要注意改变事件与监护条件的区别。监护条件仅只在引起转换的触发器事件触发时或者事件接受者对事件进行处理时被赋值一次。如果为假,那么转换不激发并且事件被遗失,条件也不会再被赋值。而改变事件隐含连续计算,因此可以对改变事件连续赋值,直到条件为真激发转换。

4. 时间事件(time event)

时间(time)表示一个绝对或者相对时刻的值。时间表达式(time expression)指的是计算结果为一个相对或者绝对时间值的表达式。

时间事件(time event)表示时间表达式被满足的事件,它代表时间的流逝。时间事件是一个依赖于时间包因而依赖于时钟存在的事件。而现实世界的时钟或虚拟内部时钟可以定义为绝对时间或者流逝时间。因此时间事件既可以被指定为绝对形式(天数)​,也可以被指定为相对形式(从某一指定事件发生开始所经历的时间)​。时间事件不像信号为一个命名事件那样声明,时间事件仅用做转换的触发。

3、组成状态

组成状态(composite state)是内部嵌套有子状态的状态。一个组成状态包括一系列子状态。组成状态可以使用"与"关系分解为并行子状态,或者通过"或"关系分解为互相排斥的互斥子状态。因此,组成状态可以是并发或者顺序的。如果一个顺序组成状态是活动的,则只有一个子状态是活动的。如果一个并发组成状态是活动的,则与它正交的所有子状态都是活动的。

一个系统在同一时刻可以包含多个状态。如果一个嵌套状态是活动的,则所有包含它的组成状态都是活动的。进入或者离开组成状态的转换会引起入口动作或者出口动作的执行。如果转换带有动作,那么这个动作在入口动作执行后,出口动作执行前执行。

为了促进封装,组成状态可以具有初始状态和终止状态。它们是伪状态,目的是为了优化状态机的结构。到组成状态的转换代表初始状态的转换,到组成状态的终止状态的转换代表了在这个封闭状态里活动的完成。而封闭状态里活动的完成会激发活动事件的完成,最终引发封闭状态上的完成转换。

3.1、顺序组成状态

如果一个组成状态的多个子状态之间是互斥的,不能同时存在的,这种组成状态称为顺序组成状态。

一个顺序组成状态最多可以有一个初始状态和一个终态,同时也最多可以由一个浅(shallow)历史状态和一个深(deep)历史状态。

当状态机通过转换进入组成状态时,一个转换可以以组成状态为目标,也可以以它的一个子状态为目标,如果它的目标是一个组成状态,那么进入组成状态后先执行其入口动作,然后再将控制传递给初态。如果它的目标是一个子状态,那么在执行组成状态的入口动作和子状态的入口动作后将控制传递给嵌套状态。

如图所示,为表示录音机工作状态的组成状态。

3.2、并发组成状态

在一个组成状态中,可能有两个或者多个并发的子状态机,我们称这样的组成状态为并发组成状态。每个并发子状态还可以进一步分解为顺序组成状态。

一个并发组成状态可能没有初始状态,终态,或者历史状态。但是嵌套在它们里的任何顺序组成状态可包含这些伪状态。

如果一个状态机被分解成多个并发的子状态,那么代表着它的控制流也被分解成与并发子状态数目一样的并发流。当进入一个并发组成状态时,控制线程数目增加;当离开一个并发组成状态时,控制线程减少。只有所有的并发子状态都到达它们的终态,或者有一个离开组成状态的显式转换时,控制才能重新汇合成一个流。

如图所示,组成状态考核为某一单位招人时的内部状态转换。该状态机表示,对一个人员的考核包括考试和体检,考试又分为笔试和面试。只有当考试和体检这两部分都分别完成并合格时,录取的考核结果才算通过。只要其中任何一项通不过,都将被淘汰。

4、PlantUML状态图

4.1、普通状态

  • 使用 [*] 绘制状态图的起点或终点。
  • 使用--> 添加箭头。
js 复制代码
@startuml
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 -> State2
State2 --> [*]
@enduml

4.2、简化状态

可以使用隐藏空描述,即 hide empty description 关键字,渲染一个简单的状态。

js 复制代码
@startuml
hide empty description
[*] --> 状态1
状态1 --> [*]
状态1 : 这是一段字符串
状态1 : 这是另一段字符串
状态1 -> 状态2
状态2 --> [*]
@enduml

4.3、复杂状态

一个状态也可能是嵌套的,必须使用关键字 state花括号来定义复杂状态。

4.3.1、内部子状态
js 复制代码
@startuml
scale 350 width
[*] --> NotShooting
state NotShooting {
    [*] --> Idle
    Idle --> Configuring : EvConfig
    Configuring --> Idle : EvConfig
}
state Configuring {
    [*] --> NewValueSelection
    NewValueSelection --> NewValuePreview : EvNewValue
    NewValuePreview --> NewValueSelection : EvNewValueRejected
    NewValuePreview --> NewValueSelection : EvNewValueSaved
    state NewValuePreview {
        State1 -> State2
    }
}
@enduml
4.3.2、子状态间的连接
js 复制代码
@startuml
state A {
    state X {
    }
    state Y {
    }
}
state B {
    state Z {
    }
}
X --> Z
Z --> Y
@enduml

4.4、长状态名

可以使用关键字 state 来给状态描述较长的状态名,并定义其指代名。

js 复制代码
@startuml
scale 600 width
[*] -> State1
State1 --> State2 : Succeeded
State1 --> [*] : Aborted
State2 --> State3 : Succeeded
State2 --> [*] : Aborted
state State3 {
    state "Accumulate Enough Data\nLong State Name" as long1
    long1 : Just a test
    [*] --> long1
    long1 --> long1 : New Data
    long1 --> ProcessData : Enough Data
}
State3 --> State3 : Failed
State3 --> [*] : Succeeded / Save Result
State3 --> [*] : Aborted
@enduml

4.5、历史状态

在嵌套状态中,你可以用 [H] 来表示历史状态,[H*] 表示深层历史状态.

js 复制代码
@startuml
[*] -> State1
State1 --> State2 : Succeeded
State1 --> [*] : Aborted
State2 --> State3 : Succeeded
State2 --> [*] : Aborted
state State3 {
    state "Accumulate Enough Data" as long1
    long1 : Just a test
    [*] --> long1
    long1 --> long1 : New Data
    long1 --> ProcessData : Enough Data
    State2 --> [H]: Resume
}
State3 --> State2 : Pause
State2 --> State3[H*]: DeepResume
State3 --> State3 : Failed
State3 --> [*] : Succeeded / Save Result
State3 --> [*] : Aborted
@enduml

4.6、分支状态fork, join

可以使用版型 <<fork>><<join>> 来表示状态的分叉及合并。

js 复制代码
@startuml
state fork_state <<fork>>
[*] --> fork_state
fork_state --> State2
fork_state --> State3
state join_state <<join>>
State2 --> join_state
State3 --> join_state
join_state --> State4
State4 --> [*]
@enduml

4.7、并发状态--, \|\|

-- or || 作为分隔符来合成并发状态。

4.7.1、水平分隔 --
js 复制代码
@startuml
[*] --> Active
state Active {
    [*] -> NumLockOff
    NumLockOff --> NumLockOn : EvNumLockPressed
    NumLockOn --> NumLockOff : EvNumLockPressed
    --
    [*] -> CapsLockOff
    CapsLockOff --> CapsLockOn : EvCapsLockPressed
    CapsLockOn --> CapsLockOff : EvCapsLockPressed
    --
    [*] -> ScrollLockOff
    ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
    ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}
@enduml
4.7.2、竖直分隔 ||
js 复制代码
@startuml
[*] --> Active
state Active {
    [*] -> NumLockOff
    NumLockOff --> NumLockOn : EvNumLockPressed
    NumLockOn --> NumLockOff : EvNumLockPressed
    ||
    [*] -> CapsLockOff
    CapsLockOff --> CapsLockOn : EvCapsLockPressed
    CapsLockOn --> CapsLockOff : EvCapsLockPressed
    ||
    [*] -> ScrollLockOff
    ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
    ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}
@enduml

4.8、选择结点 choice

版型 <<choice>> 可以用来表示一个选择结点,表示状态条件。

js 复制代码
@startuml
state "Req(Id)" as ReqId <<sdlreceive>>
state "Minor(Id)" as MinorId
state "Major(Id)" as MajorId
state c <<choice>>
Idle --> ReqId
ReqId --> c
c --> MinorId : [Id <= 10]
c --> MajorId : [Id > 10]
@enduml

4.9、一个使用版型的完整样例 start, choice, fork, join, end

js 复制代码
@startuml
state start1 <<start>>
state choice1 <<choice>>
state fork1 <<fork>>
state join2 <<join>>
state end3 <<end>>
[*] --> choice1 : from start\nto choice
start1 --> choice1 : from start stereo\nto choice
choice1 --> fork1 : from choice\nto fork
choice1 --> join2 : from choice\nto join
choice1 --> end3 : from choice\nto end stereo
fork1 ---> State1 : from fork\nto state
fork1 --> State2 : from fork\nto state
State2 --> join2 : from state\nto join
State1 --> [*] : from state\nto end
join2 --> [*] : from join\nto end
@enduml

4.10、入口和出口 entryPoint, exitPoint

可以用以下版型给合成状态添加入口结点 <<entryPoint>> 和出口结点 <<exitPoint>>

js 复制代码
@startuml
state Somp {
    state entry1 <<entryPoint>>
    state entry2 <<entryPoint>>
    state sin
    entry1 --> sin
    entry2 -> sin
    sin -> sin2
    sin2 --> exitA <<exitPoint>>
}
[*] --> entry1
exitA --> Foo
Foo1 -> entry2
@enduml

4.11、引脚 inputPin, outputPin

可以用以下版型添加引脚结点 <<inputPin>><<outputPin>>

js 复制代码
@startuml
state Somp {
    state entry1 <<inputPin>>
    state entry2 <<inputPin>>
    state sin
    entry1 --> sin
    entry2 -> sin
    sin -> sin2
    sin2 --> exitA <<outputPin>>
}
[*] --> entry1
exitA --> Foo
Foo1 -> entry2
@enduml

4.12、扩展 expansionInput, expansionOutput

可以用以下版型添加扩展结点 <<expansionInput>><<expansionOutput>>

js 复制代码
@startuml
state Somp {
    state entry1 <<expansionInput>>
    state entry2 <<expansionInput>>
    state sin
    entry1 --> sin
    entry2 -> sin
    sin -> sin2
    sin2 --> exitA <<expansionOutput>>
}
[*] --> entry1
exitA --> Foo
Foo1 -> entry2
@enduml

4.13、箭头方向

使用-> 定义水平箭头,也可以使用下列格式强制设置箭头方向:

  • -down-> (default arrow)
  • -right-> or ->
  • -left->
  • -up->
js 复制代码
@startuml
[*] -up-> First
First -right-> Second
Second --> Third
Third -left-> Last
@enduml

4.14、更改箭头线条的颜色和风格

js 复制代码
@startuml
State S1
State S2
S1 -[#DD00AA]-> S2
S1 -left[#yellow]-> S3
S1 -up[#red,dashed]-> S4
S1 -right[dotted,#blue]-> S5
X1 -[dashed]-> X2
Z1 -[dotted]-> Z2
Y1 -[#blue,bold]-> Y2
@enduml

4.15、注释

可以用 note left of, note right of, note top of, note bottom of 关键字来定义注释。

还可以定义多行注释。

js 复制代码
@startuml
[*] --> Active
Active --> Inactive
note left of Active : this is a short\nnote
note right of Inactive
A note can also
be defined on
several lines
end note
@enduml

以及浮动注释。

js 复制代码
@startuml
state foo
note "This is a floating note" as N1
@enduml

4.16、在箭头上添加注释

可以在连接箭头上使用 note on link 关键字来添加注释.

js 复制代码
@startuml
[*] -> State1
State1 --> State2
note on link
this is a state-transition note
end note
@enduml

4.17、给复杂状态添加注释

js 复制代码
@startuml
[*] --> NotShooting
state "Not Shooting State" as NotShooting {
    state "Idle mode" as Idle
    state "Configuring mode" as Configuring
    [*] --> Idle
    Idle --> Configuring : EvConfig
    Configuring --> Idle : EvConfig
}
note right of NotShooting : This is a note on a composite state
@enduml

4.18、颜色

js 复制代码
@startuml
state CurrentSite #pink {
    state HardwareSetup #lightblue {
        state Site #brown
        Site -[hidden]-> Controller
        Controller -[hidden]-> Devices
    }
    state PresentationSetup{
        Groups -[hidden]-> PlansAndGraphics
    }
    state Trends #FFFF77
    state Schedule #magenta
    state AlarmSupression
}
@enduml

4.19、显示参数

用 skinparam 改变字体和颜色。

可以在如下场景中使用:

  • 在图示的定义中,
  • 在导入的文件中,
  • 在命令行或者 ANT 任务提供的配置文件中。

还可以为状态的构造类型指定特殊的字体和颜色。

js 复制代码
@startuml
skinparam backgroundColor LightYellow
skinparam state {
    StartColor MediumBlue
    EndColor Red
    BackgroundColor Peru
    BackgroundColor<<Warning>> Olive
    BorderColor Gray
    FontName Impact
}
[*] --> NotShooting
state "Not Shooting State" as NotShooting {
    state "Idle mode" as Idle <<Warning>>
    state "Configuring mode" as Configuring
    [*] --> Idle
    Idle --> Configuring : EvConfig
    Configuring --> Idle : EvConfig
}
NotShooting --> [*]
@enduml

状态图所有显示参数测试

js 复制代码
@startuml
skinparam State {
    AttributeFontColor blue
    AttributeFontName serif
    AttributeFontSize 9
    AttributeFontStyle italic
    BackgroundColor palegreen
    BorderColor violet
    EndColor gold
    FontColor red
    FontName Sanserif
    FontSize 15
    FontStyle bold
    StartColor silver
}
state A : a a a\na
state B : b b b\nb
[*] -> A : start
A -> B : a2b
B -> [*] : end
@enduml

4.20、改变样式

js 复制代码
@startuml
<style>
stateDiagram {
    BackgroundColor Peru
    'LineColor Gray
    FontName Impact
    FontColor Red
    arrow {
        FontSize 13
        LineColor Blue
    }
}
</style>
[*] --> NotShooting
state "Not Shooting State" as NotShooting {
    state "Idle mode" as Idle <<Warning>>
    state "Configuring mode" as Configuring
    [*] --> Idle
    Idle --> Configuring : EvConfig
    Configuring --> Idle : EvConfig
}
NotShooting --> [*]
@enduml
js 复制代码
@startuml
<style>
diamond {
    BackgroundColor #palegreen
    LineColor #green
    LineThickness 2.5
}
</style>
state state1
state state2
state choice1 <<choice>>
state end3 <<end>>
state1 --> choice1 : 1
choice1 --> state2 : 2
choice1 --> end3 : 3
@enduml

4.21、改变状态颜色和样式

js 复制代码
@startuml
state FooGradient #red-green ##00FFFF
state FooDashed #red|green ##[dashed]blue {
}
state FooDotted ##[dotted]blue {
}
state FooBold ##[bold] {
}
state Foo1 ##[dotted]green {
state inner1 ##[dotted]yellow
}
state out ##[dotted]gold
state Foo2 ##[bold]green {
state inner2 ##[dotted]yellow
}
inner1 -> inner2
out -> inner2
@enduml
js 复制代码
@startuml
state FooGradient #red-green;line:00FFFF
state FooDashed #red|green;line.dashed;line:blue {
}
state FooDotted #line.dotted;line:blue {
}
state FooBold #line.bold {
}
state Foo1 #line.dotted;line:green {
state inner1 #line.dotted;line:yellow
}
state out #line.dotted;line:gold
state Foo2 #line.bold;line:green {
state inner2 #line.dotted;line:yellow
}
inner1 -> inner2
out -> inner2
@enduml
js 复制代码
@startuml
state s1 : s1 description
state s2 #pink;line:red;line.bold;text:red : s2 description
state s3 #palegreen;line:green;line.dashed;text:green : s3 description
state s4 #aliceblue;line:blue;line.dotted;text:blue : s4 description
@enduml

4.22、别名

有了 State,你可以使用 alias ,比如。

js 复制代码
@startuml
state alias1
state "alias2"
state "long name" as alias3
state alias4 as "long name"
alias1 : ""state alias1""
alias2 : ""state "alias2"""
alias3 : ""state "long name" as alias3""
alias4 : ""state alias4 as "long name"""
alias1 -> alias2
alias2 -> alias3
alias3 -> alias4
@enduml
js 复制代码
@startuml
state alias1 : ""state alias1""
state "alias2" : ""state "alias2"""
state "long name" as alias3 : ""state "long name" as alias3""
state alias4 as "long name" : ""state alias4 as "long name"""
alias1 -> alias2
alias2 -> alias3
alias3 -> alias4
@enduml
相关推荐
吴声子夜歌8 小时前
PlantUML——序列图
uml·plantuml·序列图
吴声子夜歌10 小时前
PlantUML——活动图
uml·plantuml·活动图
吴声子夜歌1 天前
PlantUML——类图(一)
uml
吴声子夜歌1 天前
PlantUML——类图(二)
uml·plantuml·类图
吴声子夜歌1 天前
PlantUML——对象图
uml·plantuml·对象图
吴声子夜歌2 天前
PlantUML——用例图
uml·plantuml
rolt4 天前
PlantUML描述《分析模式》第4章企业财务观察(1)
产品经理·架构师·uml·系统工程
KobeSacre5 天前
UML 学习
学习·uml
hssfscv7 天前
软件设计师2021上、下上午题错题解析+2022上、下下午题训练5道 练习真题训练16
笔记·设计模式·uml