La máquina de estado rara vez es utilizada por los desarrolladores móviles. Aunque la mayoría conoce los principios del trabajo y lo implementa fácilmente de forma independiente. En el artículo, descubriremos qué tareas resuelve la máquina de estado utilizando el ejemplo de las aplicaciones de iOS. La historia se aplica en la naturaleza y está dedicada a los aspectos prácticos del trabajo.
Debajo del corte, encontrará una transcripción ampliada del discurso de Alexander Sychev (
Brain89 ) en
AppsConf , en el que compartió sus opciones para usar la máquina de estado en el desarrollo de aplicaciones que no son de juegos.
Sobre el orador: Alexander Sychev ha estado involucrado en el desarrollo de iOS durante ocho años, tiempo durante el cual participó en la creación de aplicaciones simples y clientes complejos para redes sociales y el sector financiero. Por el momento, es un líder técnico en Sberbank.
Llegan a la programación desde muchas áreas, con diferentes antecedentes y experiencia, así que primero recordamos la teoría básica.
Declaración del problema.

Una máquina de estados es una abstracción matemática que consta de tres elementos principales:
- muchos estados internos
- el conjunto de señales de entrada que determinan la transición del estado actual al siguiente,
- conjuntos de estados finales, tras la transición a la cual el autómata completa su trabajo ("admite la palabra de entrada x").
Condición
Por estado queremos decir una variable o un grupo de variables que determinan el comportamiento de un objeto. Por ejemplo, en la aplicación estándar de iOS "Configuración
" hay un elemento "Negrita" ("Básico → Acceso universal"). El valor de este elemento le permite cambiar entre dos opciones para mostrar texto en la pantalla del dispositivo.

Al enviar la misma señal "Cambiar el valor del interruptor de palanca
" , obtenemos una reacción diferente del sistema: ya sea el estilo de fuente habitual o en negrita: todo es simple. Al estar en diferentes estados y recibir la misma señal, un objeto reacciona de manera diferente a un cambio de estado.
Tareas tradicionales
En la práctica, los programadores a menudo se encuentran con una máquina de estados finitos.
Aplicaciones de juegos
Esto es lo primero que viene a la mente: como parte del juego, casi todo está determinado por el estado actual del juego. Entonces, Apple asume el uso de máquinas de estado principalmente en aplicaciones de juegos (discutiremos esto en detalle más adelante).
Los siguientes ejemplos ilustran el comportamiento del sistema cuando procesa la misma señal pero con un estado interno diferente. Por ejemplo:
● el personaje del juego puede tener diferentes puntos fuertes: uno con armadura mecánica y con una pistola láser, y el otro está débilmente bombeado. Dependiendo de este estado, se determina el comportamiento de los enemigos: atacan o huyen.

● el juego está en pausa: no es necesario dibujar el cuadro actual; El jugador en el modo de menú o en el proceso del juego: el renderizado es completamente diferente.
Análisis de texto
Una de las tareas de análisis de texto más populares asociadas con el uso de una máquina de estado son los filtros de spam. Deje que haya un conjunto de palabras de parada y una secuencia de entrada. Debe filtrar esta secuencia o no mostrarla en absoluto.

Formalmente, esta es la tarea de encontrar una subcadena en una cadena. Para resolverlo, se utiliza el algoritmo Knut-Morris-Pratt, cuya implementación de software es una máquina de estados finitos. El estado es el desplazamiento de la secuencia de entrada y el número de caracteres encontrados en el patrón - palabra de parada.
Además,
al analizar expresiones regulares , a menudo se usan máquinas de estados finitos.
Procesamiento de consultas paralelas
Una máquina de estados es una de las opciones para implementar el procesamiento de solicitudes y ejecutar un conjunto estricto de instrucciones.

Por ejemplo, en el servidor web nginx, las solicitudes de entrada de varios protocolos se procesan utilizando máquinas de estado. Dependiendo del protocolo específico, se selecciona una implementación específica de la máquina de estado y, en consecuencia, se ejecuta un conjunto de instrucciones bien conocido.
En general, se obtienen dos clases de problemas:
- administrar la lógica de un objeto complejo con un estado interno complejo,
- formación de control y flujos de datos (descripción del algoritmo).
Obviamente, tales tareas comunes se encuentran en la práctica de cualquier programador. Por lo tanto, el uso de una máquina de estado es posible, incluso en aplicaciones de contenido que no son de juegos, que están involucradas en la mayoría de los desarrolladores móviles.
A continuación, analizaremos dónde y cuándo se puede usar la máquina de estado para crear aplicaciones típicas de iOS.
La mayoría de las aplicaciones móviles tienen una arquitectura en capas. Hay tres capas base.
- Capa de presentación
- Capa lógica empresarial.
- Un conjunto de ayudantes, clientes de red, etc. (Core layer).
Como se indicó anteriormente, la máquina de estado controla objetos con un comportamiento complejo, es decir, Con condición compleja. Dichos objetos están definitivamente en la capa de presentación, porque toma decisiones al procesar la entrada del usuario o los mensajes del sistema operativo. Veamos los diferentes enfoques para su ejecución.

En la metáfora arquitectónica clásica de Modelo-Vista-Controlador, el estado estará en el controlador: decide qué se muestra en la Vista y cómo responder a las señales de entrada: presionar un botón, cambiar el control deslizante, etc. Es lógico que una de las implementaciones del controlador sea una máquina de estado.

En VIPER, el estado está en el presentador: es él quien determina la transición de navegación específica desde la pantalla actual y la visualización de datos en la Vista.

En Model-View-ViewModel, el estado está en ViewModel. Independientemente de si tenemos aglutinantes reactivos o no, el comportamiento del módulo definido en la metáfora MVVM se registrará en ViewModel. Obviamente, su implementación a través de una máquina de estado es una opción aceptable.

Los objetos complejos con un conjunto no trivial de estados también se encuentran en la capa de lógica de negocios de la aplicación. Por ejemplo, un cliente de red que, según se establezca o no una conexión con el servidor, envía o bloquea solicitudes. O un objeto para trabajar con una base de datos que necesita traducir funciones de lenguaje en una consulta SQL, ejecutarla, obtener una respuesta, traducirla en objetos, etc.

En tareas más específicas, como un módulo de pago, en el que un conjunto más amplio de estados, lógica compleja, el uso de una máquina de estados también es correcto.
Como resultado, encontramos que en las aplicaciones móviles hay muchos objetos cuyo estado y lógica de comportamiento se describen más complicados que con una oración. Deben ser capaces de gestionar.
Considere un
ejemplo real y comprenda en qué punto se necesita realmente una máquina de estados finitos, y dónde su aplicación no está justificada.

Considere el ViewController de la aplicación Championship iOS, un recurso deportivo popular. Este controlador muestra un conjunto de comentarios en forma de tabla. Los usuarios ingresan la descripción del partido, ven fotos, leen las noticias y dejan sus comentarios. La pantalla es bastante simple: la capa subyacente proporciona datos, se procesa y se muestra en la pantalla.

Se pueden transmitir datos reales o un error a la pantalla. Entonces aparece el primer operador condicional, la primera rama, que determina el comportamiento adicional de la aplicación.
La siguiente pregunta es qué hacer si no hay datos. ¿Es esta condición un error? Lo más probable es que no: no todas las noticias tienen comentarios de los usuarios. Por ejemplo, el hockey en Egipto es de poco interés para cualquiera; en un artículo de este tipo generalmente no hay comentarios. Este es el comportamiento normal y el estado normal de la pantalla que debe poder mostrar. Entonces aparece el segundo operador condicional.
Es lógico suponer que también hay un estado de inicio en el que el usuario espera datos (por ejemplo, cuando la pantalla de comentarios solo aparece en la pantalla). En este caso, muestre el indicador de carga correctamente. Esta es la tercera declaración condicional.
Entonces, ya tenemos cuatro estados en una pantalla simple, cuya lógica de visualización se describe a través de la construcción if-else-if-else.

Pero, ¿y si hay más de esos estados? El desarrollo iterativo de la pantalla conduce a una intrincada maraña de construcciones condicionales, un grupo de banderas o una engorrosa caja de interruptores múltiples. Este código da miedo. Imagina que el desarrollador que lo apoyará sabe dónde vives y tiene una motosierra que siempre lleva consigo. Y realmente quiere estar a la altura de su pequeña pero bien merecida pensión.
Creo que en este caso vale la pena considerar si vale la pena dejar tal implementación en la aplicación.
Desventajas
Comprendamos lo que no nos gusta de este código.

En primer lugar, es
difícil de leer .
Dado que el código se lee mal, significa que será difícil para un nuevo desarrollador averiguar qué se implementa exactamente en un lugar particular del proyecto. En consecuencia, pasará mucho tiempo analizando la lógica de comportamiento de la aplicación; el
costo de soporte y desarrollo aumentará .
Este código no es
flexible . Si necesita agregar un nuevo estado que no se sigue de la escalera actual, ¡es posible que no tenga éxito! Si necesita un pasaje pasante, deje de pasar abruptamente los controles en esta escalera, ¿cómo hacerlo? Casi nada
Además, con este enfoque,
no hay protección contra los estados ficticios . Cuando las transiciones se describen a través de un caso de cambio, lo más probable es que se implemente el comportamiento predeterminado. Este estado es lógico en términos del comportamiento del programa, pero difícilmente lógico en términos de la lógica humana o comercial de la aplicación.
¿Cuál puede ser la solución a las deficiencias indicadas? Por supuesto, esta es la construcción de la lógica de cada módulo / controlador / objeto complejo, no basada en la intuición, sino utilizando un buen enfoque formal. Por ejemplo, una máquina de estados finitos.
Gameplaykit
Como
ejemplo, tomamos lo que ofrece Apple. Dentro del marco del marco de GameplayKit, hay dos clases que nos ayudan a trabajar con la máquina de estados.
El nombre del marco deja en claro que Apple quería ser utilizado en juegos. Pero
en aplicaciones que no sean de juego, será útil.

La clase
GKState define el estado. Para describirlo, debe realizar pasos simples. Heredamos de esta clase, establecemos el nombre del estado y definimos tres métodos.
- isValidNextState: si el estado actual es válido en función del anterior.
- didEnterFrom: acciones sobre la transición a este estado.
- willExitTo: acciones al salir de este estado.
GKStateMachine es una clase de máquina de estados. Es aún más fácil. Es suficiente realizar dos acciones.
- Pasamos el conjunto de estados de entrada a la matriz escrita a través del inicializador.
- Hacemos transiciones dependiendo de las señales de entrada usando el método enter. A través de él, también se establece el estado inicial.
Puede ser confuso que cualquier clase se pase como argumento al método
enter . Pero debe tenerse en cuenta que un objeto de cualquier clase no se puede definir en una variedad de estados
; esto prohíbe la escritura estricta. En consecuencia, si establece una clase arbitraria como la siguiente clase de estado, no ocurrirá nada y el método enter devolverá falso.
Estados y transiciones entre ellos
Habiendo familiarizado con el marco de Apple, volvamos al ejemplo. Es necesario describir los estados y las transiciones entre ellos. Debe hacer esto de la manera más comprensible. Hay dos opciones comunes: una tabla o un gráfico de transición. El gráfico de transición, en mi opinión, es una opción más comprensible. Está en UML de forma estandarizada. Por lo tanto, lo elegimos.
En el gráfico de transición hay estados que se describen por nombres y flechas que conectan estos estados para describir las transiciones. En el ejemplo, hay un estado inicial
, esperamos datos
, y hay tres estados que se pueden alcanzar desde el inicial: datos recibidos, sin datos y error.

En la implementación, obtenemos cuatro clases pequeñas.

Analicemos el estado "Datos pendientes". En la entrada, vale la pena mostrar el indicador de descarga. Y cuando salgas de este estado
, escóndelo. Para hacer esto, debe tener un enlace débil al ViewController, que es controlado por la máquina de estado creada.

Parámetros de la máquina
El segundo paso que debe hacerse
es establecer los parámetros de la máquina de estados. Para hacer esto, cree estados y transfiéralos al objeto autómata.

También asegúrese de establecer el estado inicial

En principio, todo, la máquina está lista. Ahora es necesario procesar reacciones a eventos externos, cambiando el estado del autómata.

Recordemos la declaración del problema. Obtuvimos una escalera de if-else, en base a la cual se decidió qué acción se debería realizar. Como control para un autómata simple, tal opción de implementación puede ser (de hecho, un simple interruptor
, esta es una implementación primitiva de una máquina de estados finitos), pero prácticamente no nos deshacemos de los inconvenientes mencionados anteriormente.
Hay otro enfoque que le permitirá alejarse de estas escaleras. Lo proponen los clásicos de la programación
: la llamada "pandilla de cuatro".

Hay un patrón de diseño especial, que se llama "Estado".

Este es un patrón de comportamiento similar a una estrategia que describe una abstracción de máquina de estado. Permite que el objeto cambie su comportamiento según el estado. El objetivo principal de la aplicación
es encapsular el comportamiento y los datos asociados con un estado particular en una clase separada. Por lo tanto, la máquina de estados, que inicialmente tomó la decisión de qué estado causar, ahora transmitirá una señal, la traducirá a un estado y el estado tomará una decisión. Descargue parcialmente la escalera y el código será más agradable de usar.
El marco estándar no sabe cómo. Sugiere que
GKStateMachine tomará la decisión. Por lo tanto, expandimos la máquina de estados finitos con un nuevo método, donde, como configuración, pasamos la descripción de todas las variables condicionales que determinan de manera única el siguiente estado. Dentro de este método, puede delegar la selección del siguiente estado al actual.

Es una buena práctica describir el estado con un objeto y siempre transmitirlo, en lugar de escribir muchos, muchos parámetros de entrada. A continuación, delegamos la elección del siguiente estado al actual. Esa es toda la actualización.
Ventajas de GameplayKit.- Biblioteca estándar No es necesario descargar nada, use cocoapods o carthage.
- La biblioteca es bastante fácil de aprender.
- Hay dos implementaciones a la vez: en Objective-C y en Swift.
Desventajas- Las realizaciones de estados y transiciones están estrechamente relacionadas.
Se viola el principio de responsabilidad exclusiva: el estado sabe a dónde va y cómo. - Los estados duplicados no se controlan de ninguna manera.
Se pasa una matriz a la máquina de estado, no muchos estados. Si transfiere varios estados idénticos, se utilizará el último de la lista.
¿Qué más son las implementaciones de máquinas de estados finitos? Echa un vistazo a GitHub.
Implementaciones de Objective-C

TransitionKit
Esta es la biblioteca de Objective-C más popular durante mucho tiempo, sin deficiencias identificadas en GamePlayKit. Nos permite implementar una máquina de estado y todas las acciones asociadas con ella en bloques.
El estado está separado de las transiciones .
Dentro de TransitionKit hay 2 clases.
- TKState: para configurar estados y acciones de entrada y salida.
- TKEvent es una clase para describir la transición.
TKEvent une algunos estados a otros. El evento en sí mismo se define simplemente por una cadena.
Además, hay beneficios adicionales.
Puede transferir datos útiles durante la transición . Esto funciona igual que cuando se usa NSNotificationCenter. Toda la carga útil viene en forma de un diccionario userInfo, y el usuario analiza la información.
La transición errónea tiene una descripción . Cuando intentamos hacer una transición inexistente (imposible), no solo obtenemos el valor NO al regresar del método de transición, sino también una descripción detallada del error, que es útil al depurar una máquina de estados.

TransitionKit se utiliza en la popular cosechadora de red RestKit. Este es un buen ejemplo de cómo se puede usar una máquina de estado en el núcleo de la aplicación al implementar operaciones de red.

RestKit tiene una clase especial, RKOperationStateMachine, para administrar operaciones concurrentes. En la entrada, acepta la operación que se está procesando y la cola para su ejecución.

Internamente, la máquina de estados es muy simple: tres estados (listo, ejecutado, completado) y dos transiciones: inicio y finalización. Después del inicio del procesamiento (y en cualquier transición), la máquina de estados comienza a controlar un bloque de código de usuario predefinido en la cola especificada al crear la cola.
Una operación asociada con su autómata transfiere eventos externos al autómata y realiza transiciones entre estados y todas las acciones relacionadas. La máquina de estado se encarga de
- ejecución de código asincrónico
- ejecución de código atómico durante las transiciones,
- control de transición
- Cancelación de operaciones.
- la corrección del cambio de las variables de estado de operación: isReady, isExecuting, isFinished.
Cambio
Además de TransitionKit, vale la pena mencionar por separado
Shift , una pequeña biblioteca implementada como una categoría sobre NSObject. Este enfoque le permite convertir cualquier objeto en una máquina de estados, describiendo su estado en forma de constantes de cadena y acciones en bloques durante las transiciones. Por supuesto, este es más un proyecto de capacitación, pero bastante interesante y le permite probar lo que es una máquina de estado a un costo mínimo.
Implementaciones rápidas

Hay muchas implementaciones de máquinas de estados finitos en Swift. Señalaré uno (
comentario : desafortunadamente, el proyecto no se ha desarrollado en los últimos dos años después del informe, pero vale la pena contar las ideas contenidas en el artículo).
SwiftyStateMachine
En SwiftyStateMachine, la máquina de estado está representada por una estructura no estable; a través de los métodos didSet de la propiedad, puede detectar fácilmente los cambios de estado.
En esta biblioteca, la máquina de estados se define a través de la tabla de correspondencia de estados y transiciones entre ellos. Este esquema se describe por separado del objeto que controlará la máquina. Esto se implementa a través de un conmutador anidado.
Las características clave, las ventajas de esta biblioteca son.- La necesidad de describir completamente el esquema de transiciones de estado.
Esto le permite obtener un error en la etapa de compilación si no se procesa la transición para un estado específico. - Control estricto de las señales de entrada.
No puede pasar una señal a una máquina de estado que no esté definida o que esté definida para otra máquina de estado. - Separar el circuito y el objeto que controla le permite ahorrar tiempo al inicializar la máquina.
- Visualización utilizando el lenguaje de descripción de gráficos DOT.
Existe un lenguaje de marcado gráfico para trabajar con diagramas de estado: DOT. Esta biblioteca lo usa para indicar cómo se representará la máquina de estado.

Conclusión
.
- .
, . , . , .
- .
( ).
- .
, , . - . , SwiftyStateMachine , , . .
- .
, . , , . .
.

. , . , , switch case: , , — .

. . , . , , , . .

, , . .

—
. : , — .


«-»
.

, . .
app coordinators — , , : , . , .
, app coordinator , state machine. . , app coordinators state machine, , , ,
. , , , . .

, state machine , , .
state machine , if-else. , .
Este año, en Apps Conf 2018, que se llevará a cabo los días 8 y 9 de octubre, Alexander planea discutir cinco principios básicos de programación orientada a objetos y los límites de su aplicabilidad.
Para obtener más informes de desarrollo móvil, consulte nuestro canal de YouTube . Y si desea recibir información sobre nuevas transcripciones e informes interesantes, suscríbase al boletín .