Máquinas automáticas contra el código de espagueti


"Me encantan los spaghetti westerns, odio el código de spaghetti"

El "código de espagueti" es una expresión ideal para describir el software que es un caos humeante desde un punto de vista cognitivo y estético. En este artículo, hablaré sobre un plan de tres puntos para destruir un código de espagueti:

  • Discutimos por qué el código de espagueti no es tan sabroso.
  • Presentamos una nueva mirada a lo que realmente hace el código.
  • Estamos discutiendo Frame Machine Notation (FMN) , que ayuda a los desarrolladores a desentrañar una bola de pasta.

Todos sabemos lo difícil que es leer el código de otra persona. Esto puede deberse al hecho de que la tarea en sí es difícil o porque la estructura del código es demasiado ... "creativa". A menudo, estos dos problemas van de la mano.

Los desafíos son tareas difíciles y, por lo general, nada más que un descubrimiento revolucionario puede simplificarlos. Sin embargo, sucede que la estructura del software en sí agrega complejidad innecesaria, y vale la pena resolver este problema.

La fealdad del código de espagueti radica en su compleja lógica condicional. Y aunque la vida puede ser difícil de imaginar sin las muchas construcciones difíciles de si-entonces-otro, este artículo le mostrará una mejor solución.


Para ilustrar la situación con el código de espagueti, primero debemos cambiar esto:


Pasta crujiente

En esto:


Al dente!

Empecemos a cocinar.

Estado implícito


Para hacer pasta, definitivamente necesitamos agua para cocinar. Sin embargo, incluso un elemento aparentemente simple que involucra un código de espagueti puede ser muy confuso.

Aquí hay un ejemplo simple:

(temp < 32) 

¿Qué hace realmente esta verificación? Obviamente, divide la recta numérica en dos partes, pero ¿ qué significan estas partes? Creo que puede hacer una suposición lógica, pero el problema es que el código en realidad no se comunica explícitamente .

Si realmente confirmo que ella verifica si el agua es SÓLIDA [aprox. carril: según la escala Fahrenheit, el agua se congela a +32 grados] , ¿qué significará lógicamente el retorno falso?

 if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) } 

Aunque el cheque dividió los números en dos grupos, de hecho, hay tres estados lógicos: sólido, líquido y gaseoso (SÓLIDO, LÍQUIDO, GAS).

Es decir, esta recta numérica:


dividido por verificación de condición de la siguiente manera:

 if (temp < 32) { 


 } else { 


 } 

Observe lo que sucedió porque es muy importante para comprender la naturaleza del código de espagueti. Una verificación booleana dividió el espacio numérico en dos partes, pero NO categorizó el sistema como una estructura lógica real de (SÓLIDO, LÍQUIDO, GAS). En cambio, el cheque dividió el espacio en (SÓLIDO, todo lo demás).

Aquí hay una verificación similar:

 if (temp > 212) { // GAS water } else { // not GAS water. is (SOLID | LIQUID) } 

Visualmente, se verá así:

 if (temp > 212) { 


 } else { 


 } 

Tenga en cuenta que:

  1. el conjunto completo de posibles estados no se anuncia en ninguna parte
  2. en ninguna parte de las construcciones condicionales se declaran estados lógicos verificables o grupos de estados
  3. algunos estados se agrupan indirectamente por la estructura de la lógica condicional y la ramificación

Tal código es frágil, pero muy común, y no tan grande como para causar problemas con su soporte. Así que empeoremos la situación.


Nunca me gustó tu código de todos modos

El código que se muestra arriba implica la existencia de tres estados de la materia: SÓLIDO, LÍQUIDO, GAS. Sin embargo, según datos científicos, de hecho, hay cuatro estados observables en los que se incluye plasma (PLASMA) (de hecho, hay muchos otros, pero esto será suficiente para nosotros). Aunque nadie está preparando una pasta de plasma, si este código se publica en Github, y luego algún estudiante de posgrado que estudie física de alta energía lo bifurcará, también tendremos que mantener este estado.

Sin embargo, cuando se agrega plasma, el código que se muestra arriba ingenuamente hará lo siguiente:

 if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) + (PLASMA?) // how did PLASMA get in here?? } if (temp > 212) { // GAS water + (PLASMA) // again with the PLASMA!! } else { // not GAS water. is (SOLID | LIQUID) } 

Es probable que el antiguo código, cuando se agrega a muchos estados de plasma, se rompa en las ramas más. Desafortunadamente, nada en la estructura del código ayuda a informar la existencia de un nuevo estado o influir en los cambios. Además, es probable que cualquier error no sea visible, es decir, encontrarlo será lo más difícil. Solo di no a los insectos en el espagueti.

En resumen, el problema es este: las comprobaciones booleanas se utilizan para determinar los estados indirectamente . Los estados lógicos a menudo no se declaran y no son visibles en el código. Como vimos anteriormente, cuando el sistema agrega nuevos estados lógicos, el código existente puede romperse. Para evitar esto, los desarrolladores deben volver a examinar cada comprobación condicional individual y ramificación para asegurarse de que las rutas de código sigan siendo válidas para todos sus estados lógicos. Esta es la razón principal de la degradación de los fragmentos de código grandes a medida que se vuelven más complejos.

Aunque no hay formas de deshacerse por completo de las verificaciones de datos condicionales, cualquier técnica que las minimice reducirá la complejidad del código.

Veamos ahora una implementación típica orientada a objetos de una clase que crea un modelo muy simple del volumen de agua. La clase gestionará los cambios en el estado de la sustancia del agua. Después de estudiar los problemas de la solución clásica a este problema, discutimos una nueva notación llamada Frame y mostramos cómo puede hacer frente a las dificultades que hemos descubierto.

Primero hierva el agua ...


La ciencia dio nombres a todas las transiciones posibles que una sustancia puede hacer cuando cambia la temperatura.


Nuestra clase es muy simple (y no particularmente útil). Responde a los desafíos de realizar transiciones entre estados y cambia la temperatura hasta que se vuelve adecuada para el estado objetivo deseado:

(Nota: escribí este pseudocódigo. Úselo en su trabajo solo bajo su propio riesgo y riesgo).

 class WaterSample { temp:int Water(temp:int) { this.temp = temp } // gas -> solid func depose() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do depose while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // gas -> liquid func condense() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do condense while (temp > WATER_GAS_TEMP) decreaseTemp(1) } // liquid -> gas func vaporize() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do vaporize while (temp < WATER_GAS_TEMP) increaseTemp(1) } // liquid -> solid func freeze() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do freeze while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // solid -> liquid func melt() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do melt while (temp < WATER_SOLID_TEMP) increaseTemp(1) } // solid -> gas func sublimate() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do sublimate while (temp < WATER_GAS_TEMP) increaseTemp(1) } func getState():string { if (temp < WATER_SOLID_TEMP) return "SOLID" if (temp > WATER_GAS_TEMP) return "GAS" return "LIQUID" } } 

En comparación con el primer ejemplo, este código tiene ciertas mejoras. Primero, los números "mágicos" codificados (32, 212) se reemplazan por las constantes de los límites de temperatura del estado (WATER_SOLID_TEMP, WATER_GAS_TEMP). Este cambio comienza a hacer que los estados sean más explícitos, aunque indirectamente.

Las verificaciones para "programación defensiva" también aparecen en este código, que restringe la llamada al método si está en un estado inadecuado para la operación. Por ejemplo, el agua no puede congelarse si no es un líquido; esto viola la ley (de la naturaleza). Pero la adición de condiciones de vigilancia complica la comprensión del propósito del código. Por ejemplo:

 // liquid -> solid if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() 

Esta verificación condicional hace lo siguiente:

  1. Comprueba si la temp inferior a la temperatura límite de GAS
  2. Comprueba si la temp excede la temperatura límite SÓLIDO
  3. Devuelve un error si una de estas comprobaciones no es verdadera

Esta lógica es confusa. En primer lugar, estar en estado líquido está determinado por lo que la sustancia no es: un sólido o un gas.

 (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // is liquid? 

En segundo lugar, el código verifica si el agua es líquida para determinar si es necesario devolver un error.

 !(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // Seriously? 

La primera vez para entender esta doble negación de estados no es fácil. Aquí hay una simplificación que reduce ligeramente la complejidad de la expresión:

 bool isLiquidWater = (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) if (!isLiquidWater) throw new IllegalStateError() 

Este código es más fácil de entender porque el estado isLiquidWater es explícito .

Ahora estamos explorando técnicas que arreglan un estado explícito como la mejor manera de resolver problemas. Con este enfoque, los estados lógicos del sistema se convierten en la estructura física del software, lo que mejora el código y simplifica su comprensión.

Notación de máquina de marco


Frame Machine Notation (FMN) es un lenguaje de dominio específico (DSL) que define un enfoque categórico, metodológico y simple para definir e implementar varios tipos de máquinas . Para simplificar, llamaré a los autómatas de Frame simplemente "máquinas", porque esta notación puede definir criterios teóricos para cualquier tipo diferente (máquinas de estado, autómatas de tiendas y la evolución superior de autómatas: máquinas de Turing). Para conocer los diferentes tipos de máquinas y su aplicación, recomiendo estudiar la página en Wikipedia .

Aunque la teoría de los autómatas puede ser interesante (una declaración MUY dudosa), en este artículo nos centraremos en la aplicación práctica de estos poderosos conceptos para construir sistemas y escribir código.

Para resolver este problema, Frame presenta una notación estandarizada que funciona en tres niveles integrados:

  1. Texto DSL para definir controladores Frame con sintaxis elegante y concisa
  2. Un conjunto de patrones de codificación de referencia para implementar clases orientadas a objetos en forma de máquinas que Frame llama "controladores"
  3. Notación visual en la que se usa FMN para expresar operaciones complejas que son difíciles de representar gráficamente - Notación visual de marco (FVN)

En este artículo, consideraré los dos primeros puntos: FMN y patrones de referencia, y dejaré la discusión de FVN para futuros artículos.

Frame es una notación que tiene varios aspectos importantes:

  1. FMN tiene objetos de primer nivel relacionados con el concepto de autómatas, que no están disponibles en lenguajes orientados a objetos.
  2. La especificación FMN define patrones de implementación estándar en pseudocódigo que demuestran cómo se puede implementar la notación FMN.
  3. FMN pronto podrá compilar (trabajo en progreso) en cualquier lenguaje orientado a objetos

Nota: la implementación de referencia se utiliza para demostrar la equivalencia absoluta de la notación FMN y una forma sencilla de implementarla en cualquier lenguaje orientado a objetos. Puedes elegir cualquier método.

Ahora te presentaré los dos objetos de primer nivel más importantes en Frame: Frame Events y Frame Controllers .

Eventos de marco


FrameEvents son una parte integral de la simplicidad de la notación FMN. FrameEvent se implementa como una estructura o clase que al menos tiene las siguientes variables miembro:

  • ID del mensaje
  • diccionario o lista de parámetros
  • devolver objeto

Aquí está el pseudocódigo de la clase FrameEvent:

 class FrameEvent { var _msg:String var _params:Object var _return:Object FrameEvent(msg:String, params:Object = null) { _msg = msg _params = params } } 

La notación de marco utiliza el símbolo @ , que identifica el objeto FrameEvent. Cada uno de los atributos FrameEvent requeridos tiene un token especial para acceder a él:

 @|message| :  -    _msg @[param1] :  []      @^ :              _return 

A menudo no tenemos que especificar con qué FrameEvent funciona. Como la mayoría de los contextos funcionan con un solo FrameEvent a la vez, la notación definitivamente se puede simplificar para que solo use selectores de atributos. Por lo tanto, podemos simplificar el acceso:

 |buttonClick| // Select for a "buttonClick" event _msg [firstName] = "Mark" // Set firstName _params property to "Mark" ^ = "YES" // Set the _return object to "YES" 

Tal notación puede parecer extraña al principio, pero pronto veremos cómo una sintaxis tan simple para eventos simplifica enormemente la comprensión del código FMN.

Controladores de marco


Un controlador de marco es una clase orientada a objetos, ordenada de manera bien definida para implementar una máquina de marco. Los tipos de controlador se identifican con el prefijo # :

 #MyController 

esto es equivalente al siguiente pseudocódigo orientado a objetos:

 class MyController {} 

Obviamente esta clase no es particularmente útil. Para que pueda hacer algo, el controlador necesita al menos un estado para responder a los eventos.

Los controladores están estructurados de tal manera que contienen bloques de varios tipos, que se identifican mediante un guión que rodea el nombre del tipo de bloque:

 #MyController<br> -block 1- -block 2- -block 3- 

Un controlador no puede tener más de una instancia de cada bloque, y los tipos de bloque pueden contener solo ciertos tipos de subcomponentes. En este artículo, examinamos solo el bloque -machine- , que solo puede contener estados. Los estados se identifican por el token de prefijo $ .

Aquí vemos el FMN para un controlador que contiene una máquina con un solo estado:

 #MyController // controller declaration -machine- // machine block $S1 // state declaration 

Aquí está la implementación del código FMN anterior:

 class MyController { // -machine- var _state(e:FrameEvent) = S1 // initialize state variable // to $S1 func S1(e:FrameEvent) { // state $S1 does nothing } } 

La implementación del bloque de máquina consta de los siguientes elementos:

  1. _state variable, que se refiere a una función del estado actual. Se inicializa con la primera función de estado en el controlador.
  2. uno o más métodos de estado

El método de estado de trama se define como una función con la siguiente firma:

 func MyState(e:FrameEvent); 

Después de definir estos fundamentos de la implementación del bloque de máquina, podemos ver qué tan bien interactúa el objeto FrameEvent con la máquina.

Unidad de interfaz


La interacción de FrameEvents que controla el funcionamiento de la máquina es la esencia misma de la simplicidad y el poder de la notación Frame. Sin embargo, aún no hemos respondido a la pregunta, ¿de dónde provienen los FrameEvents? ¿Cómo entran en el controlador para controlarlo? Una opción: los clientes externos pueden crear e inicializar FrameEvents, y luego llamar directamente al método señalado por la variable miembro _state:

 myController._state(new FrameEvent("buttonClick")) 

Una alternativa mucho mejor sería crear una interfaz común que envuelva una llamada directa a la variable miembro _state:

 myController.sendEvent(new FrameEvent("buttonClick")) 

Sin embargo, la forma más sencilla, que corresponde a la forma habitual de crear software orientado a objetos, es crear métodos comunes que envíen un evento en nombre del cliente a la máquina interna:

 class MyController { func buttonClick() { FrameEvent e = new FrameEvent("buttonClick") _state(e) return e._return } } 

Frame define la sintaxis para un bloque de interfaz que contiene métodos que convierten las llamadas en una interfaz común para FrameEvents.

 #MyController -interface- buttonClick ... 

El bloque de interface tiene muchas otras características, pero este ejemplo nos da una idea general de cómo funciona. Daré más explicaciones en los siguientes artículos de la serie.

Ahora continuemos estudiando el funcionamiento del autómata Frame.

Controladores de eventos


Aunque hemos mostrado cómo definir un automóvil, todavía no tenemos una notación con la que hacer nada. Para procesar eventos, necesitamos 1) poder seleccionar el evento que necesita ser procesado y 2) adjuntarlo al comportamiento que se está realizando.

Aquí hay un controlador Frame simple que proporciona la infraestructura para manejar eventos:

 #MyController // controller declaration -machine- // machine block $S1 // state declaration |e1| ^ // e1 event handler and return 

Como se indicó anteriormente, para acceder al atributo _msg evento _msg , la notación FMN usa corchetes de líneas verticales:

 |messageName| 

FMN también usa un token de exponente que representa la declaración de retorno. El controlador que se muestra arriba se implementará de la siguiente manera:

 class MyController { // #MyController // -machine- var _state(e:FrameEvent) = S1 func S1(e:FrameEvent) { // $S1 if (e._msg == "e1") { // |e1| return // ^ } } } 

Aquí vemos cuán claramente la notación FMN corresponde a un patrón de implementación que es fácil de entender y codificar.

Una vez establecidos estos aspectos básicos de eventos, controladores, máquinas, estados y controladores de eventos, podemos proceder a resolver problemas reales con su ayuda.

Máquinas de enfoque único


Arriba miramos un controlador sin estado que era bastante inútil.

 #MyController 

Un paso más arriba en la cadena alimentaria de la utilidad es una clase con un solo estado, que, aunque no es inútil, es simplemente aburrido. Pero al menos él al menos está haciendo algo .

Primero, veamos cómo se implementará una clase con un solo estado (implícito):

 class Mono { String status() { return "OFF" } } 

Aquí no se declara ni se implica ningún estado, pero supongamos que si el código hace algo, el sistema está en el estado "En funcionamiento".

También presentaremos una idea importante: las llamadas a la interfaz se considerarán similares a enviar un evento a un objeto. Por lo tanto, el código anterior puede considerarse como un método para transmitir el estado | la clase Mono, siempre en el estado $ Working.

Esta situación se puede visualizar utilizando la tabla de enlace de eventos:


Ahora veamos FMN, que demuestra la misma funcionalidad y coincide con la misma tabla de enlace:

 #Mono -machine- $Working |status| ^("OFF") 

Así es como se ve la implementación:

 class Mono { // #Mono // -machine- var _state(e:FrameEvent) = Working // initialize start state func Working(e:FrameEvent) { // $Working if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } } 

Puede notar que también introdujimos una nueva notación para la declaración return , lo que significa evaluar la expresión y devolver el resultado a la interfaz:

 ^(return_expr) 

Este operador es equivalente

 @^ = return_expr 

o solo

 ^ = return_expr 

Todos estos operadores son funcionalmente equivalentes y puede usar cualquiera de ellos, pero ^(return_expr) parece el más expresivo.

Encender la estufa


Hasta ahora hemos visto un controlador con 0 estados y un controlador con 1 estado. Todavía no son muy útiles, pero ya estamos al borde de algo interesante.

Para cocinar nuestra pasta, primero debes encender la estufa. La siguiente es una clase Switch simple con una sola variable booleana:

 class Switch { boolean _isOn; func status() { if (_isOn) { return "ON"; } else { return "OFF"; } } } 

Aunque a primera vista esto no es obvio, el código que se muestra arriba implementa la siguiente tabla de enlaces de eventos:


A modo de comparación, aquí hay una FMN para el mismo comportamiento:

 #Switch1 -machine- $Off |status| ^("OFF") $On |status| ^("ON") 

Ahora vemos cómo la notación Frame coincide exactamente con el propósito de nuestro código: adjuntar un evento (llamada al método) al comportamiento en función del estado en el que se encuentra el controlador. Además, la estructura de implementación también corresponde a la tabla de enlace:

 class Switch1 { // #Switch1 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { // $Off if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { // $On if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

La tabla le permite comprender rápidamente el propósito del controlador en sus diversos estados. Tanto la estructura de notación de trama como el patrón de implementación tienen ventajas similares.

Sin embargo, nuestro interruptor tiene un problema funcional notable. Se inicializa en el estado $ Off, pero no se puede cambiar al estado $ On! Para hacer esto, necesitamos ingresar un operador de cambio de estado .

Cambiar estado


La declaración de cambio de estado es la siguiente:

 ->> $NewState 

Ahora podemos usar este operador para cambiar entre $ Off y $ On:

 #Switch2 -machine- $Off |toggle| ->> $On ^ |status| ^("OFF") $On |toggle| ->> $Off ^ |status| ^("ON") 

Y aquí está la tabla de enlace de eventos correspondiente:


Nuevo evento | alternar | ahora desencadena un cambio que simplemente recorre los dos estados. ¿Cómo se puede implementar una operación de cambio de estado?

En ninguna parte es más fácil. Aquí está la implementación de Switch2:

 class Switch2 { // #Switch2 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^("OFF") } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

También puede realizar la última mejora en Switch2 para que no solo le permita cambiar entre estados, sino que también establezca explícitamente el estado:

 #Switch3 -machine- $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON") 

A diferencia del evento | toggle |, if | turnOn | transmitido cuando Switch3 ya está activado o | desactivado | cuando ya está desactivado, el mensaje se ignora y no sucede nada. Esta pequeña mejora le da al cliente la capacidad de indicar explícitamente el estado en el que debería estar el conmutador:

 class Switch3 { // #Switch3 // -machine- var _state(e:FrameEvent) = Off /********************************** $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") ***********************************/ func Off(e:FrameEvent) { if (e._msg == "turnOn") { // |turnOn| _state = On // ->> $On return // ^ } if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } /********************************** $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON") ***********************************/ func On(e:FrameEvent) { if (e._msg == "turnOff") { // |turnOff| _state = Off // ->> $Off return // ^ } if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^ } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

El paso final en la evolución de nuestro interruptor muestra lo fácil que es comprender el propósito del controlador FMN. El código relevante demuestra lo fácil que es implementarlo usando mecanismos Frame.

¡Después de haber creado la máquina Switch, podemos encender el fuego y comenzar a cocinar!

Estado de sonido


Un aspecto clave, aunque sutil, de los autómatas es que el estado actual de la máquina es el resultado de una situación (por ejemplo, encendido) o algún tipo de análisis de datos o del entorno. Cuando la máquina cambia al estado deseado, está implícito. que la situación no cambiará sin el conocimiento del automóvil.

Sin embargo, esta suposición no siempre es cierta. En algunas situaciones, se requiere verificación (o "detección") de los datos para determinar el estado lógico actual:

  1. estado restaurado inicial : cuando la máquina se restaura desde un estado constante
  2. estado externo : define la "situación real" que existe en el entorno en el momento de la creación, restauración u operación de la máquina
  3. estado interno volátil : cuando parte de los datos internos administrados por una máquina en funcionamiento pueden cambiar fuera del control de la máquina

En todos estos casos, los datos, el entorno o ambos deben ser "sondeados" para determinar la situación y establecer el estado de la máquina en consecuencia. Idealmente, esta lógica booleana se puede implementar en una sola función que defina el estado lógico correcto. Para admitir este patrón, la notación de cuadros tiene un tipo especial de función que sondea el universo y determina la situación en el momento actual. Dichas funciones se indican con el prefijo $ antes del nombre del método que devuelve un enlace al estado :

 $probeForState() 

En nuestra situación, dicho método se puede implementar de la siguiente manera:

 func probeForState():FrameState { if (temp < 32) return Solid if (temp < 212) return Liquid return Gas } 

Como podemos ver, el método simplemente devuelve una referencia a la función de estado correspondiente al estado lógico correcto. Esta función de detección se puede utilizar para ingresar el estado correcto:

 ->> $probeForState() 

El mecanismo de implementación se ve así:

 _state = probeForState() 

El método de detección de estado es un ejemplo de notación de trama para gestionar el estado de una manera determinada. A continuación, también aprenderemos la notación importante para administrar FrameEvents.

Herencia conductual y despachador


La herencia de comportamiento y el despachador es un poderoso paradigma de programación y el último tema sobre la notación de marcos en este artículo.

El uso de marcos utiliza la herencia de comportamiento , no la herencia de datos u otros atributos. Para este estado, FrameEvents se envían a otros estados si el estado inicial no maneja el evento (o, como veremos en los próximos artículos, solo quiere transmitirlo). Esta cadena de eventos puede llegar a cualquier profundidad deseada.

Para esto, las máquinas se pueden implementar utilizando una técnica llamada método de encadenamiento . La notación FMN para enviar eventos de un estado a otro es el despachador => :

 $S1 => $S2 

Esta declaración FMN se puede implementar de la siguiente manera:

 func S1(e:FrameEvent) { S2(e) // $S1 => $S2 } 

Ahora vemos lo fácil que es encadenar métodos de estado. Apliquemos esta técnica a una situación bastante difícil:

 #Movement -machine- $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) $AtAttention => $Motionless |isStanding| ^(true) $LyingDown => $Motionless |isStanding| ^(false) $Moving |isMoving| ^(true) $Motionless |getSpeed| ^(0) |isMoving| ^(false) 

En el código anterior, vemos que hay dos estados básicos: $ Moving y $ Motionless, y los otros cinco estados heredan una funcionalidad importante de ellos. El enlace de evento nos muestra claramente cómo se verán los enlaces en general:


Gracias a las técnicas que hemos aprendido, la implementación será muy simple:

 class Movement { // #Movement // -machine- /********************************** $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) ***********************************/ func Walking(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 3 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Walking => $Moving } /********************************** $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) ***********************************/ func Running(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 6 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Running => $Moving } /********************************** $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) ***********************************/ func Crawling(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = .5 return } if (e._msg == "isStanding") { e._return = false return } Moving(e) // $Crawling => $Moving } /********************************** $AtAttention => $Motionless |isStanding| ^(true) ***********************************/ func AtAttention(e:FrameEvent) { if (e._msg == "isStanding") { e._return = true return } Motionless(e) // $AtAttention => $Motionless } /********************************** $LyingDown => $Motionless |isStanding| ^(false) ***********************************/ func LyingDown(e:FrameEvent) { if (e._msg == "isStanding") { e._return = false return } Motionless(e) // $AtAttention => $Motionless } /********************************** $Moving |isMoving| ^(true) ***********************************/ func Moving(e:FrameEvent) { if (e._msg == "isMoving") { e._return = true return } } /********************************** $Motionless |getSpeed| ^(0) |isMoving| ^(false) ***********************************/ func Motionless(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 0 return } if (e._msg == "isMoving") { e._return = false return } } } 

Maquina de agua


Ahora tenemos los conocimientos básicos sobre FMN, lo que nos permite comprender cómo volver a implementar la clase WaterSample con estados y de una manera mucho más inteligente. También lo haremos útil para nuestro físico de estudiantes de posgrado y le agregaremos un nuevo estado de $ Plasma:


Así es como se ve la implementación completa de FMN:

 #WaterSample -machine- $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |getState| ^("SOLID") $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError() 

Como puede ver, tenemos el estado inicial de $ Begin, que responde al mensaje | crear | y conserva el valor temp. La función de detección primero verifica el valor inicial temppara determinar el estado lógico, y luego realiza la transición de la máquina a este estado.

Todos los estados físicos ($ Sólido, $ Líquido, $ Gas, $ Plasma) heredan el comportamiento protector del $ Estado predeterminado. Todos los eventos que no son válidos para el estado actual se pasan al estado $ Default, que arroja un error InvalidStateError. Esto muestra cómo se puede implementar una programación defensiva simple usando la herencia de comportamiento.

Y ahora la implementación:

 class WaterSample { // -machine- var _state(e:FrameEvent) = Begin /********************************** $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ ***********************************/ func Begin(e:FrameEvent) { if (e._msg == "create") { setTemp(e["temp"]) _state = probeForState() return } } /********************************** $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |sublimate| ^("SOLID") ***********************************/ func Solid(e:FrameEvent) { if (e._msg == "melt") { doMelt() _state = Liquid return } if (e._msg == "sublimate") { doSublimate() _state = Gas return } if (e._msg == "getState") { e._return = "SOLID" return } Default(e) } /********************************** $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") ***********************************/ func Liquid(e:FrameEvent) { if (e._msg == "freeze") { doFreeze() _state = Solid return } if (e._msg == "vaporize") { doVaporize() _state = Gas return } if (e._msg == "getState") { e._return = "LIQUID" return } Default(e) } /********************************** $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") ***********************************/ func Gas(e:FrameEvent) { if (e._msg == "condense") { doCondense() _state = Liquid return } if (e._msg == "depose") { doDepose() _state = Solid return } if (e._msg == "ionize") { doIonize() _state = Plasma return } if (e._msg == "getState") { e._return = "GAS" return } Default(e) } /********************************** $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") ***********************************/ func Plasma(e:FrameEvent) { if (e._msg == "recombine") { doRecombine() _state = Gas return } if (e._msg == "getState") { e._return = "PLASMA" return } Default(e) } /********************************** $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError() ***********************************/ func Default(e:FrameEvent) { if (e._msg == "melt") { throw new InvalidStateError() } if (e._msg == "sublimate") { throw new InvalidStateError() } if (e._msg == "freeze") { throw new InvalidStateError() } if (e._msg == "vaporize") { throw new InvalidStateError() } if (e._msg == "condense") { throw new InvalidStateError() } if (e._msg == "depose") { throw new InvalidStateError() } if (e._msg == "ionize") { throw new InvalidStateError() } if (e._msg == "recombine") { throw new InvalidStateError() } if (e._msg == "getState") { throw new InvalidStateError() } } } 

Conclusión


Automata es un concepto básico de informática que se ha utilizado durante demasiado tiempo solo en áreas especializadas de desarrollo de software y hardware. La tarea principal de Frame es crear una notación para describir autómatas y establecer patrones simples para escribir código o "mecanismos" para su implementación. Espero que la notación Frame cambie la forma en que los programadores ven las máquinas, proporcionando una manera fácil de ponerlas en práctica en las tareas de programación cotidianas y, por supuesto, salvarlas de los espaguetis en el código.


Terminator come pasta (foto de Suzuki san)
En futuros artículos, basados ​​en los conceptos que hemos aprendido, crearemos un poder y una expresividad aún mayores de la notación FMN. Con el tiempo, ampliaré la discusión al estudio del modelado visual, que incluye FMN y resuelve los problemas de comportamiento incierto en los enfoques modernos de modelado de software.

Source: https://habr.com/ru/post/446330/


All Articles