Las máquinas de estado se utilizan para modelar el comportamiento dinámico de un elemento de modelo, y más
específicamente, los aspectos dirigidos por sucesos del comportamiento del sistema (consulte el apartado Concepto: Sucesos y señales). Las máquinas de estado se utilizan específicamente para
definir un comportamiento dependiente del estado, o el comportamiento que varía dependiendo del estado en que se
encuentran los elementos de modelo. Los elementos de modelo cuyo comportamiento no varía con el estado del elemento, no
requieren máquinas de estado para describir su comportamiento (estos elementos son habitualmente clases pasivas cuya
responsabilidad principal es gestionar datos). En concreto, las máquinas de estado deben utilizarse para modelar el
comportamiento de clases activas que utilizan sucesos de llamada y sucesos de señal para implementar sus operaciones
(como transiciones en la máquina de estado de la clase).
Una máquina de estado consta de estados, enlazados por transiciones. Un estado es una condición de un objeto en que se
lleva a cabo alguna tarea o donde se espera un suceso. Una transición es una relación entre dos estados que se
desencadenan con algún suceso, que efectúa ciertas acciones o evaluaciones, y que resulta en un estado final
específico. Los elementos de una máquina de estado se ilustran en la Figura 1.
Figura 1. Notación de máquina de estado.
Un editor sencillo se puede visualizar como máquina de estado finita con los estados Vacío, Esperando un
mandato y Esperando texto. Los sucesos Cargar archivo, Insertar texto, Insertar
carácter y Guardar y salir generan transiciones en la máquina de estado. La máquina de estado del editor se
ilustra en la Figura 1 a continuación.
Figura 2. La máquina de estado para un editor sencillo.
Un estado es una condición de un objeto en que se lleva a cabo alguna tarea o donde se espera un suceso. Un objeto
puede mantenerse en un estado durante una cantidad de tiempo finita. Un estado tiene diferentes propiedades:
Nombre
|
Una cadena de texto que distingue el estado de otros estados; un estado también puede ser anónimo, lo
que significa que no tiene nombre .
|
Acciones de entrar/salir
|
Acciones que se ejecutan al entrar o al salir del estado.
|
Transiciones internas
|
Las transiciones que se manejan sin generar cambios en el estado.
|
Subestados
|
La estructura anidada de un estado, que implica subestados desconectados (secuencialmente activos) o
concurrentes (simultáneamente activos).
|
Sucesos diferidos
|
Una lista de sucesos que no se manejan en ese estado sino que se posponen y se ponen en cola para que
los maneje el objeto en otro estado.
|
Tal como se ilustra en la Figura 1, hay dos estados especiales que se pueden definir para la máquina de estado de un
objeto. El estado inicial indica el lugar de inicio por omisión para la máquina de estado o subestado. Un estado
inicial se ilustra como un círculo lleno. El estado final indica la terminación de la ejecución de la máquina de estado
o que el estado incluido se ha completado. Un estado final se representa como un círculo negro lleno rodeado por un
círculo sin llenar. En realidad, los estados inicial y final son seudoestados. Ninguno de ellos puede tener los
componentes habituales de un estado normal, excepto un nombre. Una transición de un estado inicial a un estado final
puede tener el complemento completo de características, incluida una condición de vigilancia y una acción, pero puede
no tener un suceso desencadenante.
Una transición es una relación entre dos estados que indican que un objeto que se encuentra en el primer estado realiza
acciones determinadas y entrar en un segundo estado cuando ocurra un suceso especificado y se satisfagan las
condiciones especificadas. En dicho cambio de estado, la transición se "lanza". Hasta que se lanza la transición, el
objeto se encuentra en el estado "origen" y una vez que se ha lanzado, se encuentra en el estado de "destino". Una
transición tiene varias propiedades:
Estado de origen
|
Estado al que afecta la transición; si un objeto está en el estado de origen, se puede lanzar una
transición saliente cuando el objeto recibe el suceso desencadenante de la transición y si se satisface
la condición de vigilancia, si hay alguna.
|
Desencadenante de suceso
|
Suceso que convierte la transición en elegible para el lanzamiento (siempre que se satisfaga su
condición de vigilancia) cuando la recibe el objeto del estado de origen.
|
Condición de vigilancia
|
Expresión booleana que se evalúa cuando la recepción del desencadenante de suceso desencadena la
transición; si la expresión evalúa Verdadero, la transición es elegible para el lanzamiento, y si
evalúa Falso, no se lanza. Si el mismo suceso no puede desencadenar ninguna otra transición, el suceso
se pierde.
|
Acción
|
Cálculo global ejecutable que puede actuar directamente en el objeto que posee la máquina de estado, e
indirectamente en otros objetos que son visibles para el objeto.
|
Estado de destino
|
El estado que está activo con la terminación de la transición.
|
Una transición puede tener varios orígenes, en cuyo caso representa una unión de varios estados concurrentes, así como
varios destinos, en cuyo caso representa una bifurcación a varios estados concurrentes.
En el contexto de la máquina de estado, un suceso es una aparición de un estímulo que puede desencadenar una transición
de estado. Los sucesos pueden incluir sucesos de señal, sucesos de llamada, el paso del tiempo o un cambio de estado.
Una señal o llamada puede tener parámetros cuyos valores estén disponibles para la transición, incluidas expresiones
para las condiciones de vigilancia y la acción. También puede haber una transición sin desencadenante, representada por
una transición sin desencadenante de sucesos. Estas transiciones, también denominadas transiciones de terminación, se
desencadenan implícitamente cuando el estado de origen ha completado su tarea.
Una condición de vigilancia se evalúa después de que se produzca el suceso desencadenante para la transición. Puede
haber varias transiciones a partir del mismo estado origen y con el mismo desencadenante de suceso, siempre que las
condiciones de vigilancia no se solapen. Una condición de vigilancia se evalúa sólo una vez para la transición en el
momento en el que produce el suceso. La expresión booleana puede hacer referencia al estado del objeto.
Una acción es un cálculo global ejecutable, lo que significa que no la puede interrumpir un suceso y, por tanto, se
ejecuta hasta su terminación. A diferencia de una tarea, que pueden interrumpir otros sucesos. Las acciones pueden
incluir llamadas de operaciones (al propietario de la máquina de estado, así como a otros objetos visibles), la
creación o destrucción de otro objeto, o bien, el envío de una señal a otro objeto. En el caso del envío de una señal,
el nombre de señal tiene el prefijo de la palabra clave 'send'.
Las acciones de entrar y salir permiten enviar la misma acción cada vez que se entra o sale del estado,
respectivamente. Las acciones de entrar y salir permiten llevarlo a cabo sin tener que poner explícitamente acciones en
cada transición entrante o saliente. Las acciones de entrar y salir pueden no tener argumentos o condiciones de
vigilancia. Las acciones de entrar que se encuentran en el nivel superior de una máquina de estado para el elemento de
modelo pueden tener parámetros que representen los argumentos que recibe la máquina al crear el elemento.
Las transiciones internas permiten manejar los sucesos en el estado sin salir del estado, evitando así el
desencadenamiento de acciones de entrar o salir. Las transiciones internas pueden tener sucesos con parámetros y
condiciones de vigilancia y representan, básicamente, manejadores de interrupción.
Los sucesos diferidos son aquellos cuyo manejo se pospone hasta que se activa un estado en el que el suceso no se
aplaza. Cuando se activa dicho estado, se desencadena la aparición del suceso y puede causar transiciones como si
acabara de ocurrir. La implementación de los sucesos diferidos requiere la presencia de una cola de sucesos interna. Si
se produce un suceso pero se lista como diferido, se pone en cola. Los sucesos se quitan de la cola tan pronto como el
objeto entra en un estado que no difiera dichos sucesos.
Un estado sencillo es un estado que no tiene subestructura. Un estado con subestados (estados anidados) se denomina
estado compuesto. Los subestados se pueden anidar en cualquier nivel. Una máquina de estado anidada puede tener, como
máximo, un estado inicial y un estado final. Los subestados se utilizan para simplificar máquinas de estado planas
complejas al mostrar que algunos estados sólo son posibles dentro de un contexto concreto (el estado inclusivo).
Figura 3. Subestados.
Desde un origen externo a un estado compuesto inclusivo, una transición puede dirigir el estado compuesto o dirigir un
subestado. Si el destino es el estado compuesto, la máquina de estado anidada debe incluir un estado inicial, a la que
pasa el control después de pasar al estado compuesto y de enviar la acción de entrada (si procede). Si el destino es el
estado anidado, el control pasa al estado anidado después de enviar la acción de entrada del estado compuesto (si
procede) y, a continuación, la acción de entrada del estado anidado (si procede).
Una transición que conduce fuera de un estado compuesto puede tener como origen el estado compuesto o un subestado. En
cualquier caso, en primer lugar el control abandona el estado anidado (y se envía su acción de salir, si existe alguna)
y, a continuación sale del estado compuesto (y se envía su acción de salir, si existe alguna). Una transición cuyo
origen es el estado compuesto, esencialmente, interrumpe la tarea de la máquina de estado anidado.
A no ser que se especifique lo contrario, cuando una transacción entra en un estado compuesto, la acción de la máquina
de estado anidado vuelve a iniciarse en el estado inicial (a menos que la transición dirija directamente a un
subestado). Los estados de historial permiten que la máquina de estado vuelva a entrar en el último subestado activo
antes de salir del estado compuesto. En la Figura 3 se presenta un ejemplo de la utilización de los estados de
historial.
Figura 4. Estado de historial.
Normalmente, las máquinas de estado se utilizan para modelar el comportamiento de un objeto a lo largo de su ciclo de
vida. En especial, se necesitan cuando los objetos tienen comportamiento dependiente del estado. Los objetos que pueden
tener máquinas de estado incluyen clases, subsistemas, guiones de uso e interfaces (para afirmar los estados que debe
satisfacer un objeto que realiza la interfaz). En el caso de los sistemas de tiempo real, también se utilizan máquinas
de estado para cápsulas y protocolos (para afirmar los estados que debe satisfacer un objeto que realiza el protocolo).
No todos los objetos necesitan máquinas de estados. Si un objeto tiene un comportamiento simple, por ejemplo, sólo
almacena o recupera datos, el comportamiento del objeto es de estado invariable y, prácticamente, la máquina de estado
carece de interés.
El modelado del ciclo de vida de un objeto implica tres cuestiones: especificar los sucesos a los que puede responder
el objeto, la respuesta a dichos sucesos y el impacto del pasado en el comportamiento actual. Modelar el ciclo de vida
de un objeto también implica decidir el orden en el que puede responder el objeto de modo significativo a los sucesos,
empezando en el momento de la creación del objeto y continuando hasta su destrucción.
Para modelar el ciclo de vida de un objeto:
-
Establezca el contexto de la máquina de estado, si es una clase, un guión de uso o un sistema en su totalidad.
-
Si el contexto es una clase o un guión de uso, recopile las clases contiguas, incluidas las clases padre o
las clases a las que pueden llegar las asociaciones o las dependencias. Las clases contiguas son destinos
candidatos tanto para acciones como para condiciones de vigilancia e inclusión.
-
Si el contexto es el sistema en su totalidad, limite el foco a un comportamiento del sistema y, a
continuación, considere los ciclos de vida de los objetos implicados en dicho aspecto. El ciclo de vida del
sistema completo es, sencillamente, demasiado grande como foco significativo.
-
Establezca estados iniciales y finales para el objeto. Si existen condiciones previas o posteriores de los estados
inicial y final, defínalas también.
-
Determine los sucesos a los que responde el objeto. Se pueden encontrar en las interfaces del objeto. En el caso de
los sistemas de tiempo real, también pueden hallar en los protocolos del objeto.
-
Empezando por el estado inicial al estado final, diseñe los estados de nivel superior en los que puede estar el
objeto. Conecte dichos estados con transiciones desencadenadas por los suceso adecuados. Siga añadiendo
transiciones.
-
Identifique cualquier acción de entrar o salir.
-
Amplíe o simplifique la máquina de estado mediante subestados.
-
Compruebe que todos los sucesos que desencadenan transiciones en la máquina de estado coincidan con los sucesos que
esperan las interfaces que realiza el objeto. Asimismo, compruebe que la máquina de estado maneje todos los sucesos
que esperan las interfaces de los objetos. En el caso de los sistemas de tiempo real, realice comprobaciones
equivalentes para los protocolos de una cápsula y, por último, observe las ubicaciones en las que desea,
explícitamente, que se ignoren los sucesos (por ejemplo, sucesos diferidos).
-
Compruebe que las relaciones, los métodos y las operaciones del objeto inclusivo ofrezcan soporte para todas las
acciones de la máquina de estado.
-
Rastree a través de la máquina de estado, comparándola con las secuencias de sucesos esperados y sus respuestas.
Busque estados inalcanzables y estados en los que la máquina se atasque.
-
Si reorganiza o reestructura la máquina de estado, asegúrese de que la semántica no cambie.
-
Cuando se facilite una opción, utilice la semántica visual de la máquina de estado en lugar de escribir código de
transición detallado. Por ejemplo, no desencadene una transición en varias señales y después utilice código
detallado para gestionar el flujo de control de forma diferente en función de la señal. Utilice transiciones
separadas, desencadenadas por medio de señales separadas. Evite la lógica condicional en el código de transición
que oculta comportamiento adicional.
-
Denomine los estados de acuerdo con lo que se espera y lo que sucede durante el estado. Recuerde que un estado no
es un "punto en el tiempo", sino que es un período durante el que la máquina de estado espera que suceda algo. Por
ejemplo, "waitingForEnd" resulta un nombre más adecuado que "end"; 'timingSomeTask' es mejor que "timeout". No
denomine los estados como si fueran acciones.
-
Denomine de modo exclusivo a todos los estados y las transiciones de una máquina de estado puesto que, de este
modo, se simplifica la depuración a nivel de origen.
-
Utilice con prudencia las variables de estado (atributos que se utilizan para controlar el comportamiento); no las
utilice en lugar de crear nuevos estados. Se pueden utilizar variables de estado si existen pocos estados, con poco
o ningún comportamiento dependiente del estado y, si hay poco o ningún comportamiento que pueda concurrir con o que
sea independiente del objeto que contiene la máquina de estado. Si existe un comportamiento complejo y dependiente
del estado que pueda ser concurrente, o bien, si los sucesos que se deben manejar se pueden originar fuera del
objeto que contiene la máquina de estado, considere la posibilidad de utilizar una colaboración de dos o más
objetos activos (posiblemente, definidos como una composición). En los sistemas de tiempo real, el comportamiento
concurrente, complejo y dependiente del estado se debe modelar utilizando una cápsula que contenga subcápsulas.
-
Si hay más de 5 ± 2 estados en un solo diagrama, considere el uso de subestados. Se aplica el sentido común: diez
estados en un patrón absolutamente normal puede ser correcto, pero dos estados con cuarenta transiciones entre
estos requiere, obviamente, una nueva orientación. Asegúrese de que la máquina de estado resulte comprensible.
-
Denomine las transiciones por lo que desencadena el suceso y lo que sucede durante la transición. Elija nombres que
mejoren la comprensión.
-
Cuando observe un vértice de opción, debe preguntarse si puede delegar la responsabilidad para dicha opción en otro
componente, parecido al modo en el que se presenta al objeto como un conjunto de señales diferentes sobre las que
actuar (por ejemplo, en lugar de una opción en msg->data > x), que el remitente o algún otro actor
intermediario tomen la decisión y envíen una señal con la decisión explicitada en el nombre de señal (por ejemplo,
utilizar señales denominadas isFull e isEmpty, en lugar de tener una señal denominada value y comprobar datos de
mensajes).
-
Denomine la pregunta respondida del vértice de opción de modo descriptivo, por ejemplo, "isThereStillLife" o
"isItTimeToComplain".
-
En cualquier objeto dado, intente que los nombres de los vértices de opción sean exclusivos (por el mismo motivo
que los nombres de transición deben ser exclusivos).
-
¿Hay fragmentos de código demasiado largos en las transiciones? ¿Se van a utilizar funciones en su lugar y se han
capturado fragmentos de código común como funciones? Una transición se debe leer como seudocódigo de alto nivel y
debe cumplir las mismas reglas de longitud o, incluso, reglas más estrictas que las funciones C++. Por ejemplo, una
transición con más de 25 líneas de código se considera excesivamente larga.
-
Las funciones deben denominarse según lo que hacen.
-
Preste especial atención a las acciones de entrar y salir: resulta especialmente fácil hacer cambios y olvidar
cambiar las acciones de entrar y salir.
-
Las acciones de salir se pueden utilizar para proporcionar características de seguridad, por ejemplo, la acción de
salir del estado "heaterOn" desactiva el calentador, mientras que las acciones se utilizan para reforzar una
aserción.
-
Por lo general, los subestados deben contener dos o más estados, a menos que la máquina de estado sea abstracta y
la perfeccionen subclases del elemento inclusivo.
-
Se deben utilizar puntos de opción en lugar de lógica condicional en las acciones o las transiciones. Los puntos de
opción se ven fácilmente, mientras que la lógica condicional del código queda oculta a la vista y se pasa por alto
fácilmente.
-
Evite las condiciones de vigilancia.
-
Si el suceso desencadena varias transiciones, no existe control sobre la condición de vigilancia que se
debe evaluar en primer lugar. Por lo tanto, los resultados son imprevisibles.
-
Más de una condición de vigilancia puede ser "verdadera", pero sólo se puede seguir una transición. La vía
de acceso elegida puede ser imprevisible.
-
Las condiciones de vigilancia no son visuales; resulta más difícil "ver" su presencia.
-
Evite las máquinas de estado que se parecen a los gráficos de flujo.
-
Puede indicar un intento de modelar una abstracción que, en realidad, no se encuentra ahí, por ejemplo:
-
utilizar una clase activa para modelar el comportamiento más adecuado para una clase de datos o
pasiva
-
modelar una clase de datos utilizando una clase de datos y una clase activa que estén estrechamente
acopladas (es decir, la clase de datos se utiliza para pasar información de tipo, pero la clase
activa contiene la mayor parte de los datos que se deben asociar a la clase de datos).
-
Los síntomas siguientes permiten reconocer la utilización inadecuada de las máquinas de estado:
-
mensajes enviados a 'self', básicamente, sólo para reutilizar código
-
pocos estados, con muchos puntos de elección
-
en algunos casos, una máquina de estado sin ciclos. Dichas máquinas de estado son válidas en
aplicaciones de control de procesos, o bien, al intentar controlar una secuencia de sucesos;
normalmente, su presencia durante el análisis representa la degeneración de la máquina de estado en
un gráfico de flujo.
-
Cuando se ha identificado el problema:
-
Considere la división de la clase activa en unidades más pequeñas con responsabilidades más claras.
-
Mueva más comportamiento a una clase de datos que está asociada a la clase activa del problema.
-
Mueva más comportamiento a funciones de clase activa.
-
Haga señales más significativas en lugar de transferirlas a los datos.
Una máquina de estado abstracta es una máquina de estado a la que se debe añadir más detalle antes de que se pueda
utilizar para objetivos prácticos. Se pueden utilizar máquinas de estado abstractas para definir comportamiento
genérico reutilizable, que se perfecciona en elementos de modelo subsiguientes.
Figura 5. Una máquina de estado abstracta.
Considere la máquina de estado abstracta de la Figura 5. La máquina de estado simple que se ilustra es representativa
del nivel de comportamiento más abstracto (el autómata "control") de varios tipos de elementos diferentes en sistemas
dirigidos por sucesos. Aunque comparten este formato de alto nivel, los diferentes tipos de elementos pueden tener
comportamientos detallados muy distintos en el estado de ejecución dependiendo de su objetivo. Por este motivo, lo más
probable es que esta máquina de estado se defina en alguna clase abstracta que sirva como clase raíz para diferentes
clases activas especializadas.
Por tanto, vamos a definir dos perfeccionamientos diferentes de esta máquina de estado abstracta, utilizando la
herencia. Ambos perfeccionamientos, R1 y R2, se muestran en la Figura 6. Con el objeto de simplificar su comprensión,
los elementos heredados de la clase padre se han dibujado utilizando una pluma de color gris.
Figura 6. Dos perfeccionamientos de la máquina de estado de la Figura 5.
Los dos perfeccionamientos se diferencia claramente en el modo de descomponer el estado de ejecución y también de
ampliar la transición de inicio original. Por supuesto, estas opciones sólo se pueden realizar una vez que se conoce el
perfeccionamiento y, por tanto, no se podrían realizar con una transición única de extremo a extremo en la clase
abstracta.
La posibilidad de "continuar" tanto las transiciones entrantes como las transiciones salientes es
fundamental para el tipo de perfeccionamiento descrito más arriba. Puede parecer que los puntos de entrada y los
estados finales, combinados con transiciones, son suficientes para facilitar la semántica. Desgraciadamente, no basta
cuando se deben ampliar numerosas transiciones diferentes.
Lo que necesita el patrón de comportamiento abstracto es un procedimiento para encadenar dos o más
segmentos de transición, en el que todos se ejecuten en el ámbito de un solo paso de ejecución hasta el final.
Significa que las transiciones que entran en un estado jerárquico se dividen en la parte entrante que, en efecto,
termina en el límite de estado y una ampliación que continúa dentro del estado. Asimismo, las transiciones salientes
que proceden de un estado anidado jerárquicamente se segmentan en una parte que termina en el límite de estado
inclusivo y una parte que continúa del límite de estado al estado de destino. Este efecto se puede lograr en UML por
medio de la introducción del concepto estado de cadena. Lo modela un estereotipo (<<chainState>>)
del concepto Estado de UML. Se trata de un estado cuyo único objetivo es "encadenar" transiciones automáticas nuevas
(sin desencadenantes) en una transición de entrada. Un estado de cadena no tiene ninguna acción de entrar, estructura
interna, tarea interna o acción de salir. Tampoco tiene transiciones que desencadenen los sucesos. Puede tener
cualquier número de transiciones de entrada, y una transición saliente sin suceso desencadenante; esta transición se
lanza automáticamente cuando una transición de entrada activa el estado. El objetivo del estado es encadenar una
transición de entrada a una transición de salida separada. Entre las transiciones de entrada y la transición de salida
encadenada, una conecta al otro estado del estado contenedor y la otra conecta a otro estado fuera del estado
contenedor. El objetivo de presentar un estado de cadena es separar la especificación interna del estado contenedor de
su entorno externo; es cuestión de la encapsulación.
En efecto, un estado de cadena representa un estado de "paso a través" que sirve para encadenar una transición a una
transición de continuación específica. Si no se define ninguna transición de continuación, la transición termina en el
estado de cadena, y se debe lanzar alguna transición en un estado inclusivo para que, finalmente, las cosas avancen.
El segmento de máquina de estado de ejemplo de la Figura 7 ilustra estados de cadena y su notación. Los estados de
cadena se representan en un diagrama de máquina de estado por medio de pequeños círculos blancos situados en el estado
jerárquico adecuado (esta notación es similar a los estados inicial y final, a los que se parecen). Los círculos son
iconos de estereotipos del estereotipo del estado de cadena y, por lo general, se dibujan cerca del límite para mayor
comodidad. (De hecho, una variación de notación sería dibujarlas en el borde del estado inclusivo).
Figura 7. Estados de cadena y transiciones encadenadas.
La transición encadenada de este ejemplo consta de los tres segmentos de transición encadenados, e1/a11-/a12-/a13.
Cuando se recibe la señal e1, se toma la transición etiquetada como e1/a11, se ejecuta su acción a11 y, a continuación,
se alcanza el estado encadenado c1. Después de ello, se toma la transición de continuación entre c1 y c2 y, por último,
puesto que c2 también es un estado de cadena, la transición de c2 a S21. Si todos los estados junto con las vías de
acceso tienen acciones de entrar y salir, la secuencia real de la ejecución de la acción se realiza tal como se indica
a continuación:
-
acción de salir de S11
-
acción a11
-
acción de salir de S1
-
acción a12
-
acción de entrar de S2
-
acción a13
-
acción de entrar de S21
Todo ello se ejecuta en el ámbito de un solo paso de ejecución hasta el final.
Se debe comparar con respecto a la semántica de ejecución de acciones de la transición directa e2/a2, que es:
-
acción de salir de S11
-
acción de salir de S1
-
acción a2
-
acción de entrar para estado S2
-
acción de entrar para estado S21
|