Máquina de estados finita con Android (Kotlin): Pros y contras
¡Hola! ¿Cómo estás? En este artículo aprenderás sobre las máquinas de estados finitos, cómo implementar este truco con Kotlin para Android, cuáles son los beneficios y desventajas de este modelo y cuáles son los casos de uso apropiados para implementar este concepto.
¿Qué es una máquina de estados?
La máquina de estados finitos (o en inglés Finite State Machine o FSM) fue introducida a mediados de 1955 por el matemático Edward Moore, poco después de que la FSM fuera optimizada por George H. Mealy y hasta hoy sigue siendo un modelo matemático de extrema importancia para informática y matemáticas, siendo ampliamente utilizado en banca, sistemas de aviación, servicios de atención médica y mucho más.
El FSM representa un sistema computacional. Este modelo abstracto se compone de estados, eventos y transiciones. Los estados representan el modo actual del sistema, el cual puede cambiar según un determinado evento que desencadene una transición del estado actual al siguiente.
Desafortunadamente, el FSM y todo lo que ofrece ha sido pasado por alto por muchos en la informática moderna, ya que resolvería fácilmente muchos problemas de sistemas de mediana y baja complejidad de una manera muy eficiente. Así que ¡NO ENTRES EN PÁNICO!, porque este artículo explica algunos de los casos de uso apropiados para usar FSM y cómo implementar un FSM.
Conceptos de FSM
Como se mencionó anteriormente, el FSM se compone de estados, eventos y transiciones y, afortunadamente, existe un modelo visual que representa cada uno de estos conceptos en formato de diagrama. Para que todo funcione, el FSM también representa un conjunto de reglas que determinan cómo funciona su lógica.
Esas reglas son:
- Cada FSM debe contener un estado inicial, representado por una flecha que viene del vacío a un círculo con el nombre del estado:
- Cada transición debe provenir de un estado de origen a un estado de destino, por lo que cada transición involucra solo 2 estados. Ejemplo a continuación donde salimos del estado Ligado para el estado Desligado (y viceversa) a través del evento Pressionar interruptor:
- La FSM puede tener un estado final, es decir, un estado que no realiza ninguna transición a otro estado de destino. Esto está representado por el doble círculo, como se muestra a continuación en un ejemplo de flujo de producto:
¿Por qué usar una máquina de estados finita y cuáles son los casos de uso apropiados?
El FSM es un modelo ampliamente estudiado, probado y utilizado, por lo que es fiable. Cuando nos enfrentamos a un problema computacional que involucra una cantidad finita de estados conocidos que transitan entre sí a través de eventos, uno puede considerar sin miedo el uso del FSM.
Por supuesto, es posible implementar la gestión estatal sin adoptar el uso del FSM. Sin embargo, al aplicar este concepto en este tipo de escenarios se gana la confianza y la calidad de un modelo matemático que ofrece operaciones lógicas precisas y seguras.
Es apropiado utilizar un FSM en contextos con estados finitos, como en estos ejemplos:
- Estatus de resultado de request: Loading, Failure, Success.
- Estatus de jugador principal: Idle, Walking, Running, Attacking, Defending.
- Flujo de pedido: Pending, Confirmed, Canceled, Finalized.
Es importante resaltar que es necesaria una buena planificación y una buena división de ámbitos para no confundir los contextos y así crear un FSM gigante, complejo y confuso, por lo que es necesario que los ámbitos sean independientes y desacoplados para que se pueda incluso tienen FSM más pequeños e independientes entre sí. Recuerda: aplica el FSM solo a la característica (o alcance) en la que mejor se ajuste.
Pros y contras
Ventajas
- Planificar un FSM ayuda en la proyección y abstracción del proyecto, implicando una mejor comprensión del contexto del sistema para el equipo.
- Previsibilidad: un FSM bien implementado hace que sea más fácil predecir lo que sucederá con el sistema a partir de un estado determinado, insertar y eliminar cambios se vuelve más fácil cuando se puede predecir lo que viene antes o lo que viene después.
- Debug: La FSM facilita el debug porque en cada nueva entrada/salida es posible observar con logs, permitiendo una especie de "historia" de eventos dentro del sistema.
- Confiabilidad: Basada en un modelo bien estructurado y definido, la implementación del FSM es muy confiable, ya que es posible determinar exactamente el flujo a seguir y uno/a está seguro/a de que lo implementado es exactamente lo que se ejecutará, sin sorpresas.
- Facilidad: La implementación de un FSM es simple y, si se aplica en el contexto adecuado, puede facilitar enormemente el mantenimiento y el rendimiento del sistema.
Desventajas
- Si el diseño del FSM se abstrae y diseña incorrectamente, puede llevar a la implementación de un FSM inconsistente e impredecible.
- Inflexibilidad: si bien es fácil agregar/quitar nuevos estados, puede ser difícil hacer muchos cambios a la vez, un proyecto que cambia constantemente de alcance y aún está en fase de conceptualización, representa un gran desafío para la implementación de un FSM, ya que debe basarse en un flujo consistente y bien definido.
- Limitación para lidiar con lo inesperado: dependiendo del contexto del proyecto, el FSM se vuelve limitado, ya que no puede ir más allá de los estados que fueron definidos para él. Si se dejó un vacío no implementado en la delimitación de transiciones y eventos, la máquina puede llegar a estado muerto y no continuar con sus actividades como debe.
- Dificultad en la alineación: los sistemas con lógica de negocio que necesitan muchos estados pueden convertirse en un problema a la hora del mantenimiento, ya que la complejidad puede llegar a ser muy alta y, por tanto, costosa.
Implementación de una máquina de estados finita simple
Definiendo el caso de uso
El caso de uso definido para este tutorial es una función mecánica para controlar el estado de un reproductor de video.
Los estados son:
- Idle: Representa cuando el reproductor arranca y no reproduce ninguna película o video.
- Play: Representa cuando el reproductor está reproduciendo una película.
- Pause: Representa cuando se pausó el reproductor.
- Forward: Representa cuando el reproductor avanza el contenido.
- Rewind: Representa cuando el reproductor rebobina el contenido.
Las restricciones son:
- El estado Idle es el inicial.
- El estado Play se emplea cada vez que el usuario escoge una nueva película o video.
- El estado Pause solo puede usarse si el contenido está en reproducción, es decir, cuando está activo Play.
- El estado Pause puede llevar al estado Play y hacer que el video retome la reproducción.
- El estado Forward puede usarse cuando se reproduce una película, es decir, al activarse el estado Play o cuando se va en dirección opuesta con Rewind.
- El estado Rewind puede usarse cuando se reproduce un video (Play) o cuando se avanza en dirección opuesta (Forward).
Para permitir esas transiciones, dejaremos al usuario 4 botones y una lista de películas: Play, Pause, Forward y Rewind. A continuación se muestra el diagrama del FSM definido para este caso:
Etapa 1. Delimitando los estados
Para crear los estados, vamos a usar el tipo enum class, donde podemos definirlos como constantes dentro de un mismo grupo llamado PlayerStates.
Etapa 2. Delimitando los eventos
Siguiendo el diagrama de FSM en el caso de player, tenemos 4 eventos. Para delimitarlos, usaremos el tipo sealed class, donde podemos definir los eventos como objetos o classes identificados en el mismo grupo Event:
Etapa 3. Delimitando las transiciones
Para implementar las transiciones, necesitamos crear un método que permita tratar la lógica de las restricciones de cada evento basado en el evento activado. Para eso, generaremos el método triggerEvent en una clase que represente una máquina de estados finita.
Este método debe recibir como parámetro el evento activado, el cual será tratado internamente según su tipo, y el resultado de este tratamiento, que dependerá del tipo de transición, devolverá el nuevo estado del FSM.
También necesitamos mantener la referencia del estado actual de la máquina, que comienza como idle, y la referencia de la película actual que se está reproduciendo, que comienza vacía, ya que comenzamos sin película.
En el evento PlayPressed, debemos actualizar el estado actual de la máquina para Playing. En caso de que escojas una nueva película, debes actualizar la actual.
Ahora, en el evento PausePressed, solo podemos permitir el estado Pause. Si el estado actual es Playing, haremos esta restricción:
En cuanto al evento ForwardPressed, como definimos en las restricciones, solo debe alcanzarse si el estado actual es Playing o Rewinding.
Por último, el evento RewindPressed, que siguiendo la línea de ForwardPressed, solo puede activarse si el estado actual fuera Playing o Forwarding.
Implementación del ViewModel
Ahora implementaremos el ViewModel, que va a consumir la FSM y tenderá el puente con View. Para eso, usaremos una clase llamada MainViewModel que se extiende desde el tipo ViewModel, y agregaremos en esta clase los métodos correspondientes a los eventos que creamos en el FSM.
Para actualizar View de acuerdo con los cambios en la FSM, usaremos una propiedad del tipo LiveData llamada playerData. Este LiveData mantendrá actualizado el par de datos PlayerStates, correspondiente al estado del reproductor, y String, correspondiente al nombre del filme.
Etapa 4. Implementación de View
La implementación de View será muy simple: la organización de los componentes en pantalla debe quedar como en la imagen a continuación. A título de mantener este tutorial enfocado en FSM, no implementaremos una instancia real de reproductor, pero sí una abstracción de uno usando: ImageButton para forward, play, pause y rewind; una ListView que representa el catálogo de películas y, finalmente, un TextView para representar el estado actual del player y el nombre de la película que se reproduce.
En la clase MainActivity instanciaremos nuestro MainViewModel creado en la Etapa 3 usando ViewModelProvider y crearemos los métodos responsables de controlar las views.
En el método registerMoviesList crearemos una lista de películas e integraremos esos datos en ListView. También se debe implementar el evento play al hacer clic en una película o video de la lista.
En el método registerObserver implementaremos ahora la observación del LiveData playerData creado en la Etapa 3. Esta propiedad es reactiva y, con cada actualización, viene un nuevo estado y película de la FSM. De esa forma, podemos actualizar nuestro TextView. En caso de que el video sea inexistente, mostraremos al usuario un texto informando que puede escoger una película.
Finalmente, vamos a integrar los botones con el método registerButtons:
Resultado
Ahora probaremos el resultado de nuestro tutorial:
https://www.youtube.com/watch?v=qOiGOkVtRRU
Pruebas unitarias
Para complementar nuestro conocimiento y traer más confianza a nuestro FSM, vamos a implementar algunos escenarios de prueba. Es un ejercicio práctico para agregar más casos de prueba.
Conclusão
En este artículo, vimos los conceptos principales de un FSM y algunos ejemplos de casos de uso importantes. hay muchos otros casos de uso para aprender y practicar, lo que te ayudará a crear mejores FSM y más optimizados. Tómate la libertad de editar y agregar más estados y transiciones en el código implementado aquí.
¿Te gustó el artículo? Guarda esta página para referencias futursa y, por supuesto, sigue estudiando y practicando el uso de FSM para así convertirte en un/a mejor DEV que aporte conceptos informáticos fundamentales para optimizar y crear código más limpio.
¡Hasta la próxima!
Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.