La máquina de estados UML , [1] anteriormente conocida como diagrama de estados UML , es una extensión del concepto matemático de autómata finito en aplicaciones informáticas tal como se expresa en la notación del lenguaje de modelado unificado (UML).
Los conceptos detrás de esto tratan sobre organizar la forma en que un dispositivo, programa de computadora u otro proceso (a menudo técnico) funciona de tal manera que una entidad o cada una de sus subentidades esté siempre exactamente en uno de varios estados posibles y donde haya transiciones condicionales bien definidas entre estos estados.
La máquina de estados UML es una variante basada en objetos del diagrama de estados de Harel , [2] adaptado y ampliado por UML. [1] [3] El objetivo de las máquinas de estados UML es superar las principales limitaciones de las máquinas de estados finitos tradicionales conservando sus principales beneficios. Los diagramas de estados UML introducen los nuevos conceptos de estados anidados jerárquicamente y regiones ortogonales, al tiempo que amplían la noción de acciones. Las máquinas de estados UML tienen las características tanto de las máquinas Mealy como de las máquinas de Moore . Admiten acciones que dependen tanto del estado del sistema como del evento desencadenante, como en las máquinas Mealy, así como acciones de entrada y salida, que están asociadas con estados en lugar de transiciones, como en las máquinas de Moore. [4]
El término "máquina de estados UML" puede referirse a dos tipos de máquinas de estados: máquinas de estados de comportamiento y máquinas de estados de protocolo . Las máquinas de estados de comportamiento se pueden utilizar para modelar el comportamiento de entidades individuales (por ejemplo, instancias de clase), un subsistema, un paquete o incluso un sistema completo. Las máquinas de estados de protocolo se utilizan para expresar protocolos de uso y se pueden utilizar para especificar los escenarios de uso legales de clasificadores, interfaces y puertos.
Muchos sistemas de software están controlados por eventos , lo que significa que esperan continuamente la ocurrencia de algún evento externo o interno , como un clic del mouse, la presión de un botón, un tic de tiempo o la llegada de un paquete de datos. Después de reconocer el evento, estos sistemas reaccionan realizando el cálculo apropiado que puede incluir la manipulación del hardware o la generación de eventos "suaves" que activan otros componentes de software internos. (Es por eso que los sistemas controlados por eventos también se denominan sistemas reactivos ). Una vez que se completa el manejo del evento, el sistema vuelve a esperar el próximo evento.
La respuesta a un evento generalmente depende tanto del tipo de evento como del estado interno del sistema y puede incluir un cambio de estado que conduzca a una transición de estado . El patrón de eventos, estados y transiciones de estado entre esos estados se puede abstraer y representar como una máquina de estados finitos (FSM).
El concepto de una FSM es importante en la programación basada en eventos porque hace que el manejo de eventos dependa explícitamente tanto del tipo de evento como del estado del sistema. Cuando se utiliza correctamente, una máquina de estados puede reducir drásticamente la cantidad de rutas de ejecución a través del código, simplificar las condiciones probadas en cada punto de ramificación y simplificar el cambio entre diferentes modos de ejecución. [5] Por el contrario, el uso de la programación basada en eventos sin un modelo FSM subyacente puede llevar a los programadores a producir código de aplicación propenso a errores, difícil de extender y excesivamente complejo. [6]
UML conserva la forma general de los diagramas de estado tradicionales . Los diagramas de estado UML son gráficos dirigidos en los que los nodos denotan estados y los conectores denotan transiciones de estado. Por ejemplo, la Figura 1 muestra un diagrama de estado UML correspondiente a la máquina de estados del teclado de la computadora. En UML, los estados se representan como rectángulos redondeados etiquetados con nombres de estado. Las transiciones, representadas como flechas, están etiquetadas con los eventos desencadenantes seguidos opcionalmente por la lista de acciones ejecutadas. La transición inicial se origina a partir del círculo sólido y especifica el estado predeterminado cuando el sistema se inicia por primera vez. Cada diagrama de estado debe tener una transición de este tipo, que no debe estar etiquetada, ya que no se activa por un evento. La transición inicial puede tener acciones asociadas.
Un evento es algo que sucede y que afecta al sistema. Estrictamente hablando, en la especificación UML, [1] el término evento se refiere al tipo de ocurrencia en lugar de a una instancia concreta de esa ocurrencia. Por ejemplo, Keystroke es un evento para el teclado, pero cada pulsación de una tecla no es un evento sino una instancia concreta del evento Keystroke. Otro evento de interés para el teclado podría ser el encendido, pero encenderlo mañana a las 10:05:36 será solo una instancia del evento Powerstroke.
Un evento puede tener parámetros asociados , lo que permite que la instancia del evento transmita no solo la ocurrencia de algún incidente interesante, sino también información cuantitativa sobre esa ocurrencia. Por ejemplo, el evento Keystroke generado al presionar una tecla en un teclado de computadora tiene parámetros asociados que transmiten el código de escaneo de caracteres, así como el estado de las teclas Shift, Ctrl y Alt.
Una instancia de evento sobrevive a la ocurrencia instantánea que la generó y puede transmitir esta ocurrencia a una o más máquinas de estado. Una vez generada, la instancia de evento pasa por un ciclo de vida de procesamiento que puede constar de hasta tres etapas. Primero, la instancia de evento se recibe cuando se acepta y espera su procesamiento (por ejemplo, se coloca en la cola de eventos ). Luego, la instancia de evento se envía a la máquina de estado, momento en el que se convierte en el evento actual. Finalmente, se consume cuando la máquina de estado termina de procesar la instancia de evento. Una instancia de evento consumida ya no está disponible para su procesamiento.
Cada máquina de estados tiene un estado que rige la reacción de la máquina de estados a los eventos. Por ejemplo, cuando pulsas una tecla en un teclado, el código de carácter generado será un carácter en mayúscula o minúscula, dependiendo de si la tecla Bloq Mayús está activada. Por lo tanto, el comportamiento del teclado se puede dividir en dos estados: el estado "predeterminado" y el estado "bloqueado con mayúsculas". (La mayoría de los teclados incluyen un LED que indica que el teclado está en el estado "bloqueado con mayúsculas"). El comportamiento de un teclado depende solo de ciertos aspectos de su historial, es decir, si se ha presionado la tecla Bloq Mayús, pero no, por ejemplo, de cuántas teclas se han presionado anteriormente y exactamente cuáles. Un estado puede abstraer todas las secuencias de eventos posibles (pero irrelevantes) y capturar solo las relevantes.
En el contexto de las máquinas de estado de software (y especialmente de las FSM clásicas), el término estado se entiende a menudo como una única variable de estado que puede asumir sólo un número limitado de valores determinados a priori (por ejemplo, dos valores en el caso del teclado, o de forma más general, algún tipo de variable con un tipo de enumeración en muchos lenguajes de programación). La idea de la variable de estado (y del modelo FSM clásico) es que el valor de la variable de estado define por completo el estado actual del sistema en un momento dado. El concepto de estado reduce el problema de identificar el contexto de ejecución en el código a probar sólo la variable de estado en lugar de muchas variables, eliminando así gran parte de la lógica condicional.
En la práctica, sin embargo, interpretar todo el estado de la máquina de estados como una única variable de estado rápidamente se vuelve impráctico para todas las máquinas de estados más allá de las muy simples. De hecho, incluso si tenemos un único entero de 32 bits en nuestro estado de máquina, podría contribuir a más de 4 mil millones de estados diferentes, y conducirá a una explosión de estados prematura . Esta interpretación no es práctica, por lo que en las máquinas de estados UML el estado completo de la máquina de estados se divide comúnmente en (a) una variable de estado enumerable y (b) todas las demás variables que se denominan estado extendido . Otra forma de verlo es interpretar la variable de estado enumerable como un aspecto cualitativo y el estado extendido como aspectos cuantitativos de todo el estado. En esta interpretación, un cambio de variable no siempre implica un cambio de los aspectos cualitativos del comportamiento del sistema y, por lo tanto, no conduce a un cambio de estado. [7]
Las máquinas de estado complementadas con variables de estado extendidas se denominan máquinas de estado extendidas y las máquinas de estado UML pertenecen a esta categoría. Las máquinas de estado extendidas pueden aplicar el formalismo subyacente a problemas mucho más complejos de lo que es práctico sin incluir variables de estado extendidas. Por ejemplo, si tenemos que implementar algún tipo de límite en nuestra FSM (por ejemplo, limitar el número de pulsaciones de teclas en el teclado a 1000), sin estado extendido necesitaríamos crear y procesar 1000 estados, lo que no es práctico; sin embargo, con una máquina de estado extendida podemos introducir una key_count
variable, que se inicializa a 1000 y se decrementa con cada pulsación de tecla sin cambiar la variable de estado .
El diagrama de estados de la Figura 2 es un ejemplo de una máquina de estados extendida, en la que la condición completa del sistema (llamada estado extendido ) es la combinación de un aspecto cualitativo (la variable de estado ) y los aspectos cuantitativos (las variables de estado extendidas ) .
La ventaja obvia de las máquinas de estado extendidas es la flexibilidad. Por ejemplo, cambiar el límite de pulsaciones key_count
de 1000 a 10000 teclas no complicaría en absoluto la máquina de estado extendida. La única modificación necesaria sería cambiar el valor de inicialización de la key_count
variable de estado extendida durante la inicialización.
Sin embargo, esta flexibilidad de las máquinas de estados extendidas tiene un precio, debido al complejo acoplamiento entre los aspectos "cualitativos" y "cuantitativos" del estado extendido. El acoplamiento se produce a través de las condiciones de protección asociadas a las transiciones, como se muestra en la Figura 2.
Las condiciones de protección (o simplemente protecciones) son expresiones booleanas que se evalúan dinámicamente en función del valor de las variables de estado extendidas y los parámetros de eventos. Las condiciones de protección afectan el comportamiento de una máquina de estados al habilitar acciones o transiciones solo cuando se evalúan como VERDADERAS y deshabilitarlas cuando se evalúan como FALSAS. En la notación UML, las condiciones de protección se muestran entre corchetes (por ejemplo, [key_count == 0]
en la Figura 2).
La necesidad de guardias es la consecuencia inmediata de añadir variables de estado extendidas de memoria al formalismo de la máquina de estados. Si se utilizan con moderación, las variables de estado extendidas y las guardias constituyen un mecanismo poderoso que puede simplificar los diseños. Por otro lado, es muy fácil abusar de los estados extendidos y las guardias. [8]
Cuando se envía una instancia de evento, la máquina de estados responde realizando acciones , como cambiar una variable, realizar operaciones de E/S, invocar una función, generar otra instancia de evento o cambiar a otro estado. Todos los valores de parámetros asociados con el evento actual están disponibles para todas las acciones causadas directamente por ese evento.
El cambio de un estado a otro se denomina transición de estado y el evento que lo provoca se denomina evento desencadenante o, simplemente, desencadenador . En el ejemplo del teclado, si el teclado está en el estado "predeterminado" cuando se presiona la tecla Bloq Mayús, el teclado entrará en el estado "bloqueado_mayúsculas". Sin embargo, si el teclado ya está en el estado "bloqueado_mayúsculas", al presionar Bloq Mayús se producirá una transición diferente: del estado "bloqueado_mayúsculas" al estado "predeterminado". En ambos casos, presionar Bloq Mayús es el evento desencadenante.
En las máquinas de estado extendidas, una transición puede tener una protección, lo que significa que la transición puede "activarse" solo si la protección se evalúa como VERDADERA. Un estado puede tener muchas transiciones en respuesta al mismo disparador, siempre que tengan protecciones que no se superpongan; sin embargo, esta situación podría crear problemas en la secuencia de evaluación de las protecciones cuando se produce el disparador común. La especificación UML [1] no estipula intencionalmente ningún orden en particular; más bien, UML pone la carga sobre el diseñador para idear las protecciones de tal manera que el orden de su evaluación no importe. En la práctica, esto significa que las expresiones de protección no deberían tener efectos secundarios, al menos ninguno que altere la evaluación de otras protecciones que tengan el mismo disparador.
Todos los formalismos de máquinas de estados, incluidas las máquinas de estados UML, suponen universalmente que una máquina de estados completa el procesamiento de cada evento antes de poder comenzar a procesar el siguiente evento. Este modelo de ejecución se denomina ejecución hasta su finalización o RTC.
En el modelo RTC, el sistema procesa eventos en pasos RTC discretos e indivisibles. Los nuevos eventos entrantes no pueden interrumpir el procesamiento del evento actual y deben almacenarse (normalmente en una cola de eventos ) hasta que la máquina de estados vuelva a estar inactiva. Esta semántica evita por completo cualquier problema de concurrencia interna dentro de una única máquina de estados. El modelo RTC también evita el problema conceptual de procesar acciones asociadas con transiciones, donde la máquina de estados no está en un estado bien definido (está entre dos estados) durante la duración de la acción. Durante el procesamiento de eventos, el sistema no responde (no es observable), por lo que el estado mal definido durante ese tiempo no tiene importancia práctica.
Sin embargo, tenga en cuenta que RTC no significa que una máquina de estado tenga que monopolizar la CPU hasta que se complete el paso de RTC. [1] La restricción de preempción solo se aplica al contexto de tarea de la máquina de estado que ya está ocupada procesando eventos. En un entorno multitarea , otras tareas (no relacionadas con el contexto de tarea de la máquina de estado ocupada) pueden estar ejecutándose, posiblemente preemptando la máquina de estado que se está ejecutando actualmente. Mientras otras máquinas de estado no compartan variables u otros recursos entre sí, no hay peligros de concurrencia .
La principal ventaja del procesamiento RTC es la simplicidad. Su mayor desventaja es que la capacidad de respuesta de una máquina de estados está determinada por su paso RTC más largo. Conseguir pasos RTC cortos a menudo puede complicar significativamente los diseños en tiempo real.
Aunque las FSM tradicionales son una herramienta excelente para abordar problemas más pequeños, también se sabe en general que tienden a volverse inmanejables, incluso para sistemas moderadamente complejos. Debido al fenómeno conocido como explosión de estados y transiciones , la complejidad de una FSM tradicional tiende a crecer mucho más rápido que la complejidad del sistema que describe. Esto sucede porque el formalismo de la máquina de estados tradicional inflige repeticiones. Por ejemplo, si intenta representar el comportamiento de una calculadora de bolsillo simple con una FSM tradicional, notará inmediatamente que muchos eventos (por ejemplo, las pulsaciones de los botones Borrar o Apagar) se manejan de manera idéntica en muchos estados. Una FSM convencional que se muestra en la figura siguiente no tiene medios para capturar tal similitud y requiere repetir las mismas acciones y transiciones en muchos estados. Lo que falta en las máquinas de estados tradicionales es el mecanismo para factorizar el comportamiento común para compartirlo en muchos estados.
Las máquinas de estados UML abordan precisamente esta deficiencia de las FSM convencionales. Proporcionan una serie de características para eliminar las repeticiones, de modo que la complejidad de una máquina de estados UML ya no se dispara, sino que tiende a representar fielmente la complejidad del sistema reactivo que describe. Obviamente, estas características son muy interesantes para los desarrolladores de software, porque sólo ellas hacen que todo el enfoque de la máquina de estados sea verdaderamente aplicable a los problemas de la vida real.
La innovación más importante de las máquinas de estado UML con respecto a las FSM tradicionales es la introducción de estados anidados jerárquicamente (por eso los diagramas de estado también se denominan máquinas de estado jerárquicas o HSM ). La semántica asociada con la anidación de estados es la siguiente (consulte la Figura 3): si un sistema está en el estado anidado, por ejemplo "resultado" (llamado subestado ), también está (implícitamente) en el estado circundante "encendido" (llamado superestado ). Esta máquina de estado intentará manejar cualquier evento en el contexto del subestado, que conceptualmente está en el nivel inferior de la jerarquía. Sin embargo, si el subestado "resultado" no prescribe cómo manejar el evento, el evento no se descarta silenciosamente como en una máquina de estado "plana" tradicional; en cambio, se maneja automáticamente en el contexto de nivel superior del superestado "encendido". Esto es lo que significa que el sistema está en el estado "resultado" y "encendido". Por supuesto, la anidación de estados no se limita a un solo nivel, y la simple regla de procesamiento de eventos se aplica recursivamente a cualquier nivel de anidación.
Los estados que contienen otros estados se denominan estados compuestos ; por el contrario, los estados sin estructura interna se denominan estados simples . Un estado anidado se denomina subestado directo cuando no está contenido por ningún otro estado; de lo contrario, se denomina subestado anidado transitivamente .
Como la estructura interna de un estado compuesto puede ser arbitrariamente compleja, cualquier máquina de estados jerárquica puede considerarse como una estructura interna de algún estado compuesto (de nivel superior). Es conceptualmente conveniente definir un estado compuesto como la raíz última de la jerarquía de la máquina de estados. En la especificación UML, cada máquina de estados tiene una región (la raíz abstracta de cada jerarquía de máquinas de estados), [9] que contiene todos los demás elementos de toda la máquina de estados. La representación gráfica de esta región que lo encierra todo es opcional.
Como puede ver, la semántica de la descomposición jerárquica de estados está diseñada para facilitar la reutilización del comportamiento. Los subestados (estados anidados) solo necesitan definir las diferencias con los superestados (estados que los contienen). Un subestado puede heredar fácilmente [6] el comportamiento común de sus superestados simplemente ignorando los eventos que se manejan comúnmente, que luego son manejados automáticamente por estados de nivel superior. En otras palabras, la anidación jerárquica de estados permite la programación por diferencias . [10]
El aspecto de la jerarquía de estados que se enfatiza con más frecuencia es la abstracción , una técnica antigua y poderosa para lidiar con la complejidad. En lugar de abordar todos los aspectos de un sistema complejo al mismo tiempo, a menudo es posible ignorar (abstraer) algunas partes del sistema. Los estados jerárquicos son un mecanismo ideal para ocultar detalles internos porque el diseñador puede ampliar o reducir fácilmente la imagen para ocultar o mostrar estados anidados.
Sin embargo, los estados compuestos no solo ocultan la complejidad, sino que también la reducen activamente a través del poderoso mecanismo de procesamiento jerárquico de eventos. Sin esa reutilización, incluso un aumento moderado de la complejidad del sistema podría llevar a un aumento explosivo en el número de estados y transiciones. Por ejemplo, la máquina de estados jerárquica que representa la calculadora de bolsillo (Figura 3) evita repetir las transiciones Clear y Off en prácticamente todos los estados. Evitar la repetición permite que el crecimiento de los HSM se mantenga proporcional al crecimiento de la complejidad del sistema. A medida que crece el sistema modelado, la oportunidad de reutilización también aumenta y, por lo tanto, contrarresta potencialmente el aumento desproporcionado en el número de estados y transiciones típico de los FSM tradicionales.
El análisis por descomposición jerárquica de estados puede incluir la aplicación de la operación "OR exclusivo" a cualquier estado dado. Por ejemplo, si un sistema está en el superestado "on" (Figura 3), puede darse el caso de que también esté en el subestado "operand1" O en el subestado "operand2" O en el subestado "opEntered" O en el subestado "result". Esto llevaría a la descripción del superestado "on" como un estado "OR".
Los diagramas de estados UML también introducen la descomposición AND complementaria. Esta descomposición significa que un estado compuesto puede contener dos o más regiones ortogonales (ortogonal significa compatible e independiente en este contexto) y que estar en un estado compuesto de este tipo implica estar en todas sus regiones ortogonales simultáneamente. [11]
Las regiones ortogonales resuelven el problema frecuente de un aumento combinatorio en el número de estados cuando el comportamiento de un sistema se fragmenta en partes independientes y activas simultáneamente. Por ejemplo, además del teclado principal, un teclado de computadora tiene un teclado numérico independiente. De la discusión anterior, recuerde los dos estados del teclado principal ya identificados: "predeterminado" y "bloqueado mayúsculas" (consulte la Figura 1). El teclado numérico también puede estar en dos estados: "números" y "flechas", dependiendo de si Num Lock está activo. El espacio de estados completo del teclado en la descomposición estándar es, por lo tanto, el producto cartesiano de los dos componentes (teclado principal y teclado numérico) y consta de cuatro estados: "predeterminado–números", "predeterminado–flechas", "bloqueado mayúsculas–números" y "bloqueado mayúsculas–flechas". Sin embargo, esta sería una representación poco natural porque el comportamiento del teclado numérico no depende del estado del teclado principal y viceversa. El uso de regiones ortogonales permite evitar la mezcla de comportamientos independientes como un producto cartesiano y, en cambio, permanecer separados, como se muestra en la Figura 4.
Nótese que si las regiones ortogonales son completamente independientes entre sí, su complejidad combinada es simplemente aditiva, lo que significa que el número de estados independientes necesarios para modelar el sistema es simplemente la suma k + l + m + ... , donde k, l, m, ... denotan números de estados OR en cada región ortogonal. Sin embargo, el caso general de dependencia mutua, por otro lado, resulta en complejidad multiplicativa, por lo que en general, el número de estados necesarios es el producto k × l × m × ... .
En la mayoría de las situaciones de la vida real, las regiones ortogonales serían sólo aproximadamente ortogonales (es decir, no verdaderamente independientes). Por lo tanto, los diagramas de estados UML proporcionan una serie de formas para que las regiones ortogonales se comuniquen y sincronicen sus comportamientos. Entre estos ricos conjuntos de mecanismos (a veces complejos), tal vez la característica más importante es que las regiones ortogonales pueden coordinar sus comportamientos enviándose instancias de eventos entre sí.
Aunque las regiones ortogonales implican independencia de ejecución (permitiendo más o menos concurrencia), la especificación UML no requiere que se asigne un hilo de ejecución separado a cada región ortogonal (aunque esto se puede hacer si se desea). De hecho, lo más común es que las regiones ortogonales se ejecuten dentro del mismo hilo. [12] La especificación UML solo requiere que el diseñador no dependa de ningún orden particular para que las instancias de eventos se envíen a las regiones ortogonales relevantes.
Cada estado en un diagrama de estados UML puede tener acciones de entrada opcionales , que se ejecutan al ingresar a un estado, así como acciones de salida opcionales , que se ejecutan al salir de un estado. Las acciones de entrada y salida están asociadas con estados, no con transiciones. Independientemente de cómo se ingrese o salga de un estado, se ejecutarán todas sus acciones de entrada y salida. Debido a esta característica, los diagramas de estados se comportan como máquinas de Moore . La notación UML para las acciones de entrada y salida de estado es colocar la palabra reservada "entrada" (o "salida") en el estado justo debajo del compartimento del nombre, seguida de la barra diagonal y la lista de acciones arbitrarias (consulte la Figura 5).
El valor de las acciones de entrada y salida es que proporcionan medios para garantizar la inicialización y la limpieza , de forma muy similar a los constructores y destructores de clases en la programación orientada a objetos . Por ejemplo, considere el estado "door_open" de la Figura 5, que corresponde al comportamiento del horno tostador mientras la puerta está abierta. Este estado tiene un requisito crítico de seguridad muy importante: desactive siempre el calentador cuando la puerta esté abierta. Además, mientras la puerta esté abierta, la lámpara interna que ilumina el horno debe encenderse.
Por supuesto, este comportamiento podría modelarse añadiendo acciones apropiadas (desactivar el calentador y encender la luz) a cada ruta de transición que conduzca al estado "door_open" (el usuario puede abrir la puerta en cualquier momento durante el "horneado" o "tostado" o cuando el horno no se utiliza en absoluto). No se debe olvidar apagar la lámpara interna con cada transición que abandone el estado "door_open". Sin embargo, esta solución provocaría la repetición de acciones en muchas transiciones. Más importante aún, este enfoque deja el diseño propenso a errores durante las modificaciones posteriores del comportamiento (por ejemplo, el próximo programador que trabaje en una nueva característica, como el tostado superior, podría simplemente olvidarse de desactivar el calentador en la transición a "door_open").
Las acciones de entrada y salida permiten implementar el comportamiento deseado de una manera más segura, sencilla e intuitiva. Como se muestra en la Figura 5, se podría especificar que la acción de salida de "calentamiento" desactive el calentador, la acción de entrada a "puerta_abierta" encienda la lámpara del horno y la acción de salida de "puerta_abierta" apague la lámpara. El uso de acciones de entrada y salida es preferible a colocar una acción en una transición porque evita la codificación repetitiva y mejora la función al eliminar un riesgo de seguridad; (calentador encendido mientras la puerta está abierta). La semántica de las acciones de salida garantiza que, independientemente de la ruta de transición, el calentador se desactivará cuando la tostadora no esté en el estado de "calentamiento".
Debido a que las acciones de entrada se ejecutan automáticamente cada vez que se ingresa a un estado asociado, a menudo determinan las condiciones de operación o la identidad del estado, de manera muy similar a como un constructor de clase determina la identidad del objeto que se está construyendo. Por ejemplo, la identidad del estado "calentamiento" está determinada por el hecho de que el calentador está encendido. Esta condición debe establecerse antes de ingresar a cualquier subestado de "calentamiento" porque las acciones de entrada a un subestado de "calentamiento", como "tostar", dependen de la inicialización adecuada del superestado "calentamiento" y realizan solo las diferencias de esta inicialización. En consecuencia, el orden de ejecución de las acciones de entrada siempre debe proceder desde el estado más externo al estado más interno (de arriba hacia abajo).
No es sorprendente que este orden sea análogo al orden en el que se invocan los constructores de clases. La construcción de una clase siempre comienza en la raíz de la jerarquía de clases y continúa a través de todos los niveles de herencia hasta llegar a la clase que se instancia. La ejecución de las acciones de salida, que corresponde a la invocación del destructor, se realiza en el orden exactamente inverso (de abajo hacia arriba).
Muy comúnmente, un evento causa que sólo se ejecuten algunas acciones internas pero no conduce a un cambio de estado (transición de estado). En este caso, todas las acciones ejecutadas comprenden la transición interna . Por ejemplo, cuando uno escribe en un teclado, este responde generando diferentes códigos de caracteres. Sin embargo, a menos que se presione la tecla Bloq Mayús, el estado del teclado no cambia (no ocurre ninguna transición de estado). En UML, esta situación debe modelarse con transiciones internas, como se muestra en la Figura 6. La notación UML para transiciones internas sigue la sintaxis general utilizada para acciones de salida (o entrada), excepto que en lugar de la palabra entrada (o salida) la transición interna se etiqueta con el evento desencadenante (por ejemplo, vea la transición interna desencadenada por el evento ANY_KEY en la Figura 6).
En ausencia de acciones de entrada y salida, las transiciones internas serían idénticas a las autotransiciones (transiciones en las que el estado de destino es el mismo que el estado de origen). De hecho, en una máquina Mealy clásica , las acciones se asocian exclusivamente con transiciones de estado, por lo que la única forma de ejecutar acciones sin cambiar de estado es a través de una autotransición (representada como un bucle dirigido en la Figura 1 de la parte superior de este artículo). Sin embargo, en presencia de acciones de entrada y salida, como en los diagramas de estados UML, una autotransición implica la ejecución de acciones de entrada y salida y, por lo tanto, es distintivamente diferente de una transición interna.
A diferencia de una autotransición, nunca se ejecutan acciones de entrada o salida como resultado de una transición interna, incluso si la transición interna se hereda de un nivel superior de la jerarquía que el estado activo en ese momento. Las transiciones internas heredadas de superestados en cualquier nivel de anidación actúan como si estuvieran definidas directamente en el estado activo en ese momento.
La anidación de estados combinada con acciones de entrada y salida complica significativamente la semántica de transición de estados en los HSM en comparación con los FSM tradicionales. Cuando se trabaja con estados anidados jerárquicamente y regiones ortogonales, el simple término estado actual puede ser bastante confuso. En un HSM, más de un estado puede estar activo a la vez. Si la máquina de estados está en un estado de hoja que está contenido en un estado compuesto (que posiblemente está contenido en un estado compuesto de nivel superior, y así sucesivamente), todos los estados compuestos que contienen directa o transitivamente el estado de hoja también están activos. Además, debido a que algunos de los estados compuestos en esta jerarquía pueden tener regiones ortogonales, el estado activo actual está representado en realidad por un árbol de estados que comienza con la región única en la raíz y baja hasta los estados simples individuales en las hojas. La especificación UML se refiere a un árbol de estados de este tipo como configuración de estados. [1]
En UML, una transición de estado puede conectar directamente dos estados cualesquiera. Estos dos estados, que pueden ser compuestos, se designan como la fuente principal y el destino principal de una transición. La Figura 7 muestra un ejemplo de transición simple y explica los roles de los estados en esa transición. La especificación UML prescribe que realizar una transición de estado implica ejecutar las acciones en la siguiente secuencia predefinida (consulte la Sección 14.2.3.9.6 de OMG Unified Modeling Language (OMG UML) [1] ):
La secuencia de transición es fácil de interpretar en el caso simple de que tanto la fuente principal como el destino principal estén anidados en el mismo nivel. Por ejemplo, la transición T1 que se muestra en la Figura 7 provoca la evaluación de la protección g(); seguida de la secuencia de acciones: a(); b(); t(); c(); d();
y e()
; suponiendo que la protección g()
se evalúa como VERDADERA.
Sin embargo, en el caso general de estados de origen y destino anidados en diferentes niveles de la jerarquía de estados, puede que no sea inmediatamente obvio de cuántos niveles de anidamiento se necesita salir. La especificación UML [1] prescribe que una transición implica salir de todos los estados anidados desde el estado activo actual (que puede ser un subestado directo o transitivo del estado de origen principal) hasta, pero sin incluir, el estado ancestro común mínimo (LCA) de los estados de origen y destino principales. Como indica el nombre, el LCA es el estado compuesto más bajo que es simultáneamente un superestado (ancestro) tanto de los estados de origen como de destino. Como se describió anteriormente, el orden de ejecución de las acciones de salida es siempre desde el estado anidado más profundamente (el estado activo actual) hacia arriba en la jerarquía hasta el LCA pero sin salir del LCA. Por ejemplo, el LCA(s1,s2) de los estados "s1" y "s2" que se muestra en la Figura 7 es el estado "s".
El ingreso a la configuración del estado de destino comienza desde el nivel en el que se dejaron las acciones de salida (es decir, desde dentro del LCA). Como se describió anteriormente, las acciones de ingreso deben ejecutarse comenzando desde el estado de nivel más alto hacia abajo en la jerarquía de estados hasta el estado de destino principal. Si el estado de destino principal es compuesto, la semántica UML prescribe "profundizar" en su submáquina recursivamente utilizando las transiciones iniciales locales. La configuración del estado de destino se ingresa por completo solo después de encontrar un estado de hoja que no tiene transiciones iniciales.
Antes de UML 2, [1] la única semántica de transición en uso era la transición externa , en la que siempre se sale de la fuente principal de la transición y siempre se ingresa al destino principal de la transición. UML 2 preservó la semántica de "transición externa" para compatibilidad con versiones anteriores, pero también introdujo un nuevo tipo de transición llamada transición local (consulte la Sección 14.2.3.4.4 de Lenguaje de modelado unificado (UML) [1] ). Para muchas topologías de transición, las transiciones externas y locales son en realidad idénticas. Sin embargo, una transición local no causa salida y reingreso al estado de la fuente principal si el estado del destino principal es un subestado de la fuente principal. Además, una transición de estado local no causa salida y reingreso al estado del destino principal si el destino principal es un superestado del estado de la fuente principal.
La figura 8 contrasta las transiciones locales (a) y externas (b). En la fila superior, se ve el caso de la fuente principal que contiene el objetivo principal. La transición local no provoca la salida de la fuente, mientras que la transición externa provoca la salida y el reingreso a la fuente. En la fila inferior de la figura 8, se ve el caso del objetivo principal que contiene la fuente principal. La transición local no provoca la entrada al objetivo, mientras que la transición externa provoca la salida y el reingreso al objetivo.
A veces, un evento llega en un momento particularmente inconveniente, cuando una máquina de estados se encuentra en un estado que no puede manejar el evento. En muchos casos, la naturaleza del evento es tal que se puede posponer (dentro de ciertos límites) hasta que el sistema ingrese a otro estado, en el que esté mejor preparado para manejar el evento original.
Las máquinas de estados UML proporcionan un mecanismo especial para diferir eventos en estados. En cada estado, puede incluir una cláusula [event list]/defer
. Si ocurre un evento en la lista de eventos diferidos del estado actual, el evento se guardará (se diferirá) para su procesamiento futuro hasta que se ingrese a un estado que no incluya el evento en su lista de eventos diferidos. Al ingresar a dicho estado, la máquina de estados UML recuperará automáticamente cualquier evento guardado que ya no esté diferido y luego consumirá o descartará estos eventos. Es posible que un superestado tenga una transición definida en un evento que está diferido por un subestado. En consonancia con otras áreas en la especificación de las máquinas de estados UML, el subestado tiene prioridad sobre el superestado, el evento se diferirá y la transición para el superestado no se ejecutará. En el caso de regiones ortogonales donde una región ortogonal difiere un evento y otra lo consume, el consumidor tiene prioridad y el evento se consume y no se difiere.
Los diagramas de estados de Harel, precursores de las máquinas de estados UML, se inventaron como "un formalismo visual para sistemas complejos" [2] , por lo que desde su inicio se asociaron inseparablemente con la representación gráfica en forma de diagramas de estados. Sin embargo, es importante entender que el concepto de máquina de estados UML trasciende cualquier notación particular, gráfica o textual. La especificación UML [1] hace evidente esta distinción al separar claramente la semántica de la máquina de estados de la notación.
Sin embargo, la notación de los diagramas de estados UML no es puramente visual. Cualquier máquina de estados no trivial requiere una gran cantidad de información textual (por ejemplo, la especificación de acciones y protecciones). La sintaxis exacta de las expresiones de acción y protección no está definida en la especificación UML, por lo que muchas personas usan inglés estructurado o, más formalmente, expresiones en un lenguaje de implementación como C , C++ o Java . [13] En la práctica, esto significa que la notación de diagramas de estados UML depende en gran medida del lenguaje de programación específico .
Sin embargo, la mayor parte de la semántica de los diagramas de estados está fuertemente sesgada hacia la notación gráfica. Por ejemplo, los diagramas de estados representan pobremente la secuencia de procesamiento, ya sea el orden de evaluación de los guardias o el orden de envío de eventos a regiones ortogonales. La especificación UML evita estos problemas al poner la carga sobre el diseñador para que no dependa de ninguna secuencia particular. Sin embargo, es el caso de que cuando las máquinas de estados UML se implementan realmente, existe inevitablemente un control total sobre el orden de ejecución, lo que da lugar a críticas de que la semántica UML puede ser innecesariamente restrictiva. De manera similar, los diagramas de estados requieren una gran cantidad de equipo de plomería (pseudoestados, como uniones, bifurcaciones, uniones, puntos de elección, etc.) para representar gráficamente el flujo de control. En otras palabras, estos elementos de la notación gráfica no agregan mucho valor a la representación del flujo de control en comparación con el código estructurado simple .
La notación y la semántica UML están realmente orientadas a las herramientas UML computarizadas . Una máquina de estados UML, tal como se representa en una herramienta, no es sólo el diagrama de estados, sino más bien una mezcla de representación gráfica y textual que captura con precisión tanto la topología de estados como las acciones. Los usuarios de la herramienta pueden obtener varias vistas complementarias de la misma máquina de estados, tanto visuales como textuales, mientras que el código generado es sólo una de las muchas vistas disponibles.