从实例开始谈状态图的使用
2005119@gmail.com
v1.0 2006-03-15
摘要:在对“靓号租用”项目的重构中,我通过状态图很好的理解了业务逻辑。由此进一步归纳了状态图在开发过程中的使用提示和技巧。
关键词:UML,状态图,重构
读者水平:初级
引言
“靓号租用”是原无线技术部门开发GLSMRPIDService中的一个部分,由于这部分存在已的功能缺陷,需要对之进行适当的修改,通过这种方式来使功能得到完善并使版本得到演进。我先给出当前需求,对对象“靓号”的状态图。
按图说图图(1)描述了对象“靓号”在其生命期中的几个状态,“靓号”能被锁定,被锁定的“靓号”不能被其它用户再锁定。被锁定的靓号如果在15分钟内没被用户租用,则还回到初始状态,可以再被(其他)用户锁定。被锁定的“靓号”可以被租用,租用到期后能被系统预留(即为先前的用户保护起来),预留一个月后如用户未续租,则此“靓号”可以被其他人锁定或租用。在租用状态下的“靓号”,如果被同一用户累积租用超过半年,则可以买断。被买断后,此“靓号”变成普通号。
概念
很遗憾,原设计由于所面对的问题领域规模小,所以并没有采用OOD/OOP的方式,所以看不到类 CCoolIdentity这样的实体类,但由于问题领域所处理的对象即为“靓号”,因此,这里先引入这个类。这里,插入状态图使用的第一个准则:
准则1: 状态图只对单一对象的复杂行为进行模建。这里的对象指类、角色、子系统、或组件。 |
因此,状态图并不为多个对象之间的行为建模。多个对象之间的行为建模参考“活动图”,“时序图”,对象之间的关系参考“类图”,“对象图”以及参考设计模式(设计模式通常用UML或Booch图表示类之间的关系, Booch是UML的前身)。现在我们给出状态图的定义。
状态图,全称为状态机视图(state machine view),通过对每个类的生个对象形字的生命期建模,描述了对象在时间上的动态行为。状态图用于对模型元素的动态行为进行建模,更具体地说,就是对系统行为中受事件驱动的方面进行建模。
状态图由状态组成,各状态由转移链接在一起。状态是对象执行某项活动或等待某个事件时的条件。转移是两个状态之间的关系,它由某个事件触发,然后执行特定的操作或评估并导致特定的结束状态。图 (2) 描绘了状态图的各种元素。
图2 |
态是对象执行某项活动或等待某个事件时的条件。对象可能会在有限的时间长度内保持某一状态。状态具有以下几项特征:
名称 |
将一个状态与其他状态区分开来的文本字符串;状态也可能是匿名的,这表示它没有名称。 |
进入/退出操作 |
在进入和退出状态时所执行的操作。 |
内部转移 |
在不使状态发生变更的情况下进行的转移。 |
子状态 |
状态的嵌套结构,包括不相连的(依次处于活动状态的)或并行的(同时处于活动状态的)子状态。 |
延迟的事件 |
未在该状态中处理但被延迟处理(即列队等待由另一个状态中的对象来处理)的一系列事件。 |
如图 (2) 所示,可以为对象的状态图定义两种特殊的状态。初始状态指示状态图或子状态的默认起始位置。
何时需要状态图
在实际的项目开发中,并不是对每一个类都画状态图。何时需要状态图,我们可以采用下面的原则来确定:
敏捷建模( AM) ( Ambler 2002)的原则--最大化项目干系人的投资--建议你只有当模型能够提供正面价值的时候才创建模型。 如果一个实体,比如一个类或组件,表示的行为的顺序和当前的状态无关,那么画一个UML状态图可能是没有什么用处的。例如一个CLogFile类就很简单,表示了那些你将会在系统中记录一操作的数据,因此一个UML状态图就没有任何相关之处。而“靓号”这类对象就经比较的复杂。
l 当给定一项选择时,要使用状态图的可视语义,而不要写出详细的转移代码。例如,不要用几个信号触发一个转移,然后使用详细代码来管理以不同的方式依赖于信号的控制流。应使用由单独的信号来触发的单独转移。在隐藏了附加行为的转移代码中,要避免使用条件逻辑。
l 根据在状态期间等待的事件或正在发生的事件来命名状态。记住,状态不是“时间点”;它是状态图等待某个事件发生的时间段。例如,“waitingForEnd”这一名称比“end”更好;“timingSomeActivity”比“timeout”更好。不要让状态的名称看起来象是操作名。
l 在一个状态图内唯一地命名所有状态和转移;这将便于进行源级别的调试。
l 谨慎使用状态变量;不要在创建新状态时使用它们。如果状态不多,很少带有或不带有依赖于状态的行为,并且很少有或根本没有可能与包含状态图的封装体并行或独立的行为,就可以使用状态变量。如果有复杂的、依赖于状态的潜在并行行为,或者如果必须处理的事件可能来自于包含状态图的封装体之外,则应考虑使用构件封装体。
l 如果单个图中的状态超过 5 * 2 个,就应考虑使用子状态。在这里可以应用我们的常识:在一个非常规则的模式中可以有十个状态,但如果两个状态之间具有四十个转移,显然就需要重新考虑了。务必要使状态图易于理解。
l 使用触发事件的事件和/或在转移期间发生的事件为转移命名。选择更加易于理解的名称。
l 当您看见一个选择点时,应考虑是否可以将作出该选择的职责委托给另一个构件,以便将其作为一组将不同的信号提供给封装体遵照执行(例如,代替对消息->数据 > x 的选择),并考虑是否可以让发送方或另一中间主角来作出决定,然后通过在信号名称中明确显示该决定的方式发送信号(例如,使用名为 isFull 和 isEmpty 的信号,而不是以值命名信号并检查消息数据)。
l 为在选择点中回答的问题指定描述性的名称,例如“isThereStillLife”或“isItTimeToComplain”。
l 在任何给定的封装体中,尽量使选择点名称保持唯一(其原因与转移名称需保持唯一相同)。
l 转移的代码段是否太长?是否应使用函数来代替它们,是否将常用代码段记录为函数?转移应该类似于高层的伪代码,并且应当遵循与 C++ 函数相同或更严格的长度规则。例如,代码超过 25 行的转移可被认为是过长。
l 应根据函数执行的操作来命名函数。
l 要特别注意进入和退出操作:在进行更改后忘记更改相应进入和退出操作的情况尤其容易发生。
l 退出操作可用于提供安全性功能,例如,从“heaterOn”状态中的退出操作将关闭加热器,在这里,操作被用来强制执行一个断言语句。
l 通常,除非状态图是抽象的并且将由包含元素的子类来进行改进,否则子状态应包含两个或更多个状态。
l 应该用选择点来代替操作或转移中的条件逻辑。选择点容易被看到,而代码中的条件逻辑则是不可见的,很容易被忽略。
l 避免使用警戒条件。
n 如果事件触发了几个转移,将无法控制首先对哪个警戒条件求值。这会产生无法预料的结果。
n 可能有多个警戒条件为“True”,但随后只能有一个转移。所选择的路径是无法预料的。
n 警戒条件是不可见的;要“看见”它们的出现更是困难。
n 避免使用类似流程图的状态图。
u 这可能表示您试图对并不实际存在的抽象概念进行建模,例如:
u 使用一个封装体来对最适合于数据类的行为进行建模,或
n 通过使用紧密耦合的数据类和封装体类来对数据类建模(例如,数据类用于向四周传递类型信息,但封装体类包含了应与数据类相关联的大部分数据)。
u 状态图的这种错误用法可以通过以下故障现象来识别:
u 被发送给“自己”的消息,主要是为了重复使用代码
u 几乎没有状态,但有很多选择点
u 在某些情况下没有循环的状态图。在流程控制应用程序中,或者在试图控制一个事件序列时,这样的状态图是有效的;如果它们在分析过程中出现,则表示状态图已退化为流程图。
n 当发现问题时,应采取以下措施:
u 考虑将封装体分解为职责更明确的小单元,
u 将更多的行为转移到与有问题的封装体相关联的数据类中。
u 将更多的行为转移到封装体类函数中。
u 制作更有意义的信号,以避免对数据的依赖。
l 避免"黑洞"状态。
n 黑洞状态是那种只有变换进来但没有任何变换发出的状态,这种情况要么由于该状态是一个最终状态,要么就是你已经错过了一个或多个变换变换。
l 避免"奇迹"状态。
n 奇迹状态是那种只有变换发出但没有任何变换进来的状态,这种情况要么由于该状态是一个起点,要么就是你已经错过了一个或多个变换变换。
参考文献
《UML用户手册》
“Rational Unified Process”
其它网络资源