Design Patterns - State
TLDR: El State es un patrón que te permite cambiar el comportamiento de un objeto cuando su estado ha cambiado.
El patrón de diseño State es usado para permitir que un objeto altere su comportamiento cuando su estado interno cambia. Los cambios de un estado pueden ser explícitos o en respuesta a algún evento (Ej: Observer pattern)
¿Porque necesitamos el State?
Imagina que estás trabajando en un software para el manejo de las luces de los semáforos, en el que estos tienen 3 estados: Verde, Amarillo y Rojo. Cada estado tiene su propia lógica de negocios, por ejemplo: sabemos que el estado Rojo toma más tiempo en cambiar a el estado amarillo, y que el verde puede tomar menos tiempo en estar encendido que el rojo.
Este tipo de comportamientos son relacionados con una serie de estados en el que el objeto puede estar y esta situación está muy relacionado al concepto de máquinas de estado finito, que básicamente define que hay un número finito de estados en el que el software puede estar. Un ejemplo practico se puede ver en la siguiente imagen
source: https://dwarves.foundation/playbook/finite-state-machine/
En la anterior imagen podemos ver 3 estados. Trabajo (Work), Hogar (Home) y Cama (Bed). En el que hay una serie de flechas que, dependiendo del estado actual, el software podrá saber si se puede o no cambiar a ciertos estados. A esas reglas de cambios se les llama Transitions o Transiciones, que son finitas y predeterminadas. Por ejemplo, si estás en el estado Casa, podrás tomar el tren o dormir.
Una posible implementación para estos escenarios es con operadores condicionales (if
o switch
), que seleccionan el comportamiento apropiado dependiendo del estado actual del objeto y usualmente este estado es un valor en los atributos del objeto. Pero cuando usamos este tipo de acercamientos una de las principales falencias es que una vez comenzamos a agregar más y más estados, dentro de la clase comenzamos a tener comportamientos específicos dependiendo del estado, causando que la mayoría de los métodos contendrán grandes condicionales para poder elegir el comportamiento adecuado de un método de acuerdo con el estado actual.
Aquí es donde entra el patrón de diseño State, que sugiere crear nuevas clases para todos los posibles estados de un objeto y extraer todo el comportamiento especifico al estado en esas clases, en vez de implementar todos los comportamientos en el objeto original. Entonces lo que se haría sería en el objeto original guardar una referencia al estado actual.
En definición, ¿que es el patrón State?
Un State es un patrón en el que el comportamiento de un objeto es determinado por estados y los cambios de este. El objeto parecerá cambiar su clase.
El problema en un ejemplo
Imaginemos que estemos trabajando en un software en el que se requiere tener varios estados dentro de un mismo objeto, como en el caso presentado anteriormente. Para poder codificar esto inicialmente podemos usar operadores condicionales como los if
y los switch
. Así como el siguiente ejemplo:
class TrafficLight {
constructor() {
this.state = "";
}
go() {
switch (this.state) {
case "Red":
console.log(`Cambiando el semáforo a `);
console.log(`Esperando 60 segundos...`);
this.state = "Yellow";
break;
case "Yellow":
console.log(`Cambiando el semáforo a `);
console.log(`Esperando 3 segundos...`);
this.state = "Green";
break;
case "Green":
console.log(`Cambiando el semáforo a `);
console.log(`Esperando 40 segundos...`);
this.state = "Yellow";
break;
default:
break;
}
// ...
}
}
El código anterior no está mal, pero imagina que tengamos que agregar algún otro estado, y otro método para el control del semáforo peatonal. Aquí el código se vuelve más complejo, porque para cada método es necesario agregar un Switch con todos los estados y los cambios que requieren.
Aquí es donde es muy útil el patrón State, que sugiere crear una nueva clase por cada posible estado del objeto. Estas nuevas clases tendrán todo el comportamiento especifico respecto al estado actual, esto mantendrá el código limpio, sin validaciones que determinen si el código aplica a el estado X o Y.
Entonces vamos a refactorizar ese código aplicando el patrón State.
class TrafficLightState {
go() {
/* Abstract method */
}
}
Primero que todo vamos a crear una interfaz que tendrá el método general a utilizar por la clase TrafficLight
y que tiene su variación por cada estado. Después vamos a crear cada estado a continuación.
class Red extends TrafficLightState {
constructor(context) {
super();
this.context = context;
}
go() {
console.log(`Red for 1 minute`);
this.context.change(new Yellow(this.context));
}
}
class Yellow extends TrafficLightState {
constructor(context) {
super();
this.context = context;
}
go() {
console.log(`Yellow for 10 seconds`);
this.context.change(new Green(this.context));
}
}
class Green extends TrafficLightState {
constructor(context) {
super();
this.context = context;
}
go() {
console.log(`Green for 1 minute`);
this.context.change(new Red(this.context));
}
}
En cada clase creada, se extenderá de la interfaz TrafficLightState
y implementaremos el método go()
. En cada método se agregará la lógica de negocio correspondiente a ese estado, y de ser necesario se cambiará de estado a otro. Estos cambios de estado se llaman Transitions o Transiciones, y son codificados en cada clase.
Adicionalmente, como pueden ver en cada una de las clases, se pasa en el constructor una referencia del objeto principal que construiremos a continuación:
class TrafficLight {
constructor() {
this.count = 0;
this.currentState = null;
}
change(newState) {
if (this.count++ >= 10) return;
this.currentState = newState;
this.currentState.go();
}
start(initialState) {
this.currentState = initialState;
this.currentState.go();
}
}
Aquí en esta clase, tenemos el objeto original que usualmente es llamado Context y es el encargado de tener la referencia al estado actual y delega todo el trabajo relacionado con el estado a ese objeto. En el ejemplo esa acción se hace con la ejecución this.currentState.go();
en el que ejecutamos la implementación go()
del estado actual.
Para implementar este código, realizaremos una instancia de la clase TrafficLight
y le pasaremos el estado inicial. Con un propósito explicativo, he agregado en el método change
un contador para evitar quedar en un ciclo infinito.
let light = new TrafficLight();
let initialState = new Red(light);
light.start(initialState);
// Red for 1 minute
// Yellow for 10 seconds
// Green for 1 minute
// Red for 1 minute
// Yellow for 10 seconds
// Green for 1 minute
// Red for 1 minute
// Yellow for 10 seconds
// Green for 1 minute
// Red for 1 minute
// Yellow for 10 seconds
Como podemos observar en el resultado anterior, el objeto TrafficLight
cambió su estado interno varias veces en un ciclo que se ha programado de acuerdo con el ejercicio. Cada estado tenia sus propias condiciones para cambiar al siguiente estado, como por ejemplo el estado Yellow
solo pedía esperar 10 segundos para cambiar al estado Green
y así sucesivamente.
Código completo del ejemplo: https://bit.ly/2ycpbKY
Estructura del patrón State
En este patrón encontramos varios componentes que interactuaran entre si.
- El Context, que en el ejemplo es la clase
TrafficLight
es el encargado de almacenar una referencia a uno de los estados en concreto (currentState
) y delega todo el trabajo especifico del estado a esa referencia. Esta clase también se comunica con el objeto estado vía su interfaz, que en este caso esTrafficLightState
. En esta clase también es importante tener una función que te permita cambiar a un nuevo estado, el nuestro caso es el métodochange(newState)
. - El State, que en el ejemplo es la interfaz
TrafficLightState
, es la encargada de declarar los métodos del estado en especifico. Estos métodos deberían tener la implementación especifica para el estado en concreto. - Los Concrete States, que en el ejemplo son las clases
Red
,Yellow
yGreen
, son las clases cuyo padre es la interfazTrafficLightState
y son las que provee la implementación para el método, de acuerdo con el estado en especifico.
Conclusiones
- El patrón de diseño State debe ser usado cuando tienes diferentes comportamientos dentro de un objeto y estos dependen de su estado actual. Muchas veces el número de estados pueden ser varios y el código en especifico cambia frecuentemente.
- La idea principal que sugiere el patrón es extraer todo ese código en especifico del estado y ponerlo en una nueva clase. Así es más fácil agregar nuevos estados a la aplicación y es más fácil de mantener.
- También es útil usarlo cuando tienes una gran cantidad de condicionales de acuerdo con valores actuales. Esto ayudará a que tengas un código limpio y puedas estar seguro de que cualquier modificación de un estado no afectará los otros estados.
Ejercicio
Se requiere que se construya un software para la lógica de una caja fuerte. En la caja fuerte hay 3 estados:
LOCKED: Que la caja ha sido bloqueada, (O estado inicial)
OPEN: La caja fuerte está abierta y el usuario ha digitado la clave correcta. Después se cerrará la caja fuerte
ERROR: La secuencia es incorrecta, entra en estado error. Y volverá al estado inicial.
Se requiere crear una solución aplicando el patrón State, se pueda manejar el sistema de validación y apertura de la caja fuerte.
Recursos adicionales y bibliografia
Alexander Shvets - Dive into design Patterns
Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides - Design Patterns Elements of Reusable Object-Oriented Software (1994)
The State Pattern - Addy Osmani - Learning JavaScript Design Patterns (2012)
https://www.dofactory.com/javascript/facade-design-pattern
https://loredanacirstea.github.io/es6-design-patterns/#observer