Por Mario S. Pinto Miranda y Rubén Zamora
Redactores: Mario Pinto y Rubén Zamora
El patrón de diseño ‘Estado’ (conocido en inglés como ‘State Pattern’) se clasifica como un patrón de comportamiento. Permite encapsular y hacer explícito el comportamiento de un objeto, según el estado en el que está, al mismo tiempo que asegura la transición a otros estados de una manera controlada y declarada (se conocen los estados posibles y cómo cambian de uno a otro). Para ilustrar esto, presentaremos el siguiente ejemplo sencillo: modelar un interruptor de luz. Al llamar al método presionar, su estado pasa de OFF a ON y viceversa.
Entonces, en código, esto se representaría de la siguiente manera:
using EasyExample.States;
namespace EasyExample;
public class LightSwitch
{
private State state;
public LightSwitch()
{
state = new Off();
}
public void ChangeState(State state)
{
this.state = state;
}
public void Press()
{
changeState(this.state.Switch());
Console.WriteLine(this.state.Description());
}
}
Y sus estados:
namespace EasyExample.States;
public interface State
{
State Switch();
string Description();
}
// ---
public class On : State
{
public State Switch() {
return new Off()
}
public string Description()
{
return "The light is ON";
}
}
// ---
public class Off : State
{
public State Switch() {
return new On()
}
public string Description()
{
return "The light is OFF";
}
}
Al ejecutar el programa nos daría el siguiente resultado:
Tal y como decíamos, en este ejemplo hemos encapsulado los estados On y Off, haciendo explícitas las transiciones entre ellos. Si necesitan más información sobre el patrón, pueden apoyarse en este enlace.
En la Kata que hemos realizado, uno de los viernes en las formaciones, que propuso el compañero Fran Palacios la Killer Garage Door de CodeWars. Utilizar el patrón de estado para modelar las órdenes que el microcontrolador de comportamiento pasará a la puerta del garaje se ajusta muy bien.
La kata consiste en controlar la posición de la puerta en base a eventos que va detectando cada segundo. Los eventos son:
Un ejemplo de lo que entra …P…..P…..
se convertiría en 00012345543210
, como las posiciones que va tomando la puerta cada segundo.
En muchas ocasiones, hemos visto que se puede resolver esta Kata haciendo una máquina de estados, pero sentimos que esto se aleja de un diseño simple, al cual, creemos que podemos llegar aplicando el Patrón Estado.
Aquí hemos tenido dos enfoques en la primera iteración, uno implementado en C# y otro en TypeScript.
En este enfoque hemos creado 3 estados Closing
, Opening
y Pause
Cada una de estas clases implementa una interfaz común llamada State
. Esta interfaz define los métodos que se pueden llamar en cada estado. Por ejemplo, en la clase Pause
, la implementación del método Handle()
cambiará el estado de la puerta a Opening
en caso de que se haya pulsado el botón (’P’
).
public class Pause : State
{
public void Handle(GarageDoor garageDoor, char @event)
{
var isButtonPressed = @event == 'P';
switch (isButtonPressed)
{
case true when garageDoor.position == FullyClosed:
garageDoor.ChangeState(new Opening());
break;
{ ... }
default:
garageDoor.ChangeState(new Pause());
break;
}
}
{ ... }
}
Esto hará que, mientras la puerta se mantenga en ese estado, su posición se incremente progresivamente hasta llegar a su máxima apertura, que es 5.
La clase principal GarageDoor
utiliza un objeto State
para representar el estado actual de la puerta. Cuando se llama a un método en GarageDoor
, delega la llamada al objeto State
correspondiente. Por ejemplo, si se llama al método ProcessEvents()
en GarageDoor
,este delegará la llamada al objeto State
actual, que puede ser una instancia de Pause
.
Así pues, quedaría la clase GarageDoor
ocultando mucha lógica que se podría poner en el método ProcessEvents
en sus diferentes estados.
public class GarageDoor
{
private State state;
internal int position;
internal State lastDirection;
public GarageDoor()
{
lastDirection = new Pause();
position = Constants.FullyClosed;
state = new Pause();
}
public void ChangeState(State state)
{
this.state = state;
}
public string ProcessEvents(string events)
{
return new string(
events.ToCharArray()
.Select(@event =>
{
// Pattern State
state.Handle(this, @event);
position = state.ProcessEvent(position);
return position.ToString()[0];
})
.ToArray()
);
}
}
Ver el código completo en GitHub
En esta implementación, se ha extendido el número de estados con el fin de atomizar la lógica lo máximo posible, al mismo tiempo que se hace más explícito cómo funciona el Door. El diagrama previo a la implementación es el siguiente:
Siendo Closed el estado inicial, todo se reduce a estados encapsulados y autoexplicativos del problema del dominio. Esto se refleja en cómo queda el artefacto Door:
export interface DoorState {
processEvents(events: string): string;
}
import { DoorState } from "./door_state/DoorState";
import { Closed } from "./door_state/Closed";
export class Door {
private state: DoorState = new Closed(this);
processEvents(events: string): string {
return this.state.processEvents(events);
}
changeState(state: DoorState) {
this.state = state;
}
}
Es decir, se ha delegado todo a los DoorStates, que se implementarán según el diagrama anterior.
Por otro lado, a nivel de detalles de implementación, el approach escogido es similar a la recursividad aplicada en WordWrap, es decir, se delega a cada estado la tarea de procesar una parte del string de eventos hasta donde tenga la responsabilidad de procesar. Cuando acabe su responsabilidad y deba producirse un cambio debido a los eventos, la parte restante a procesar se pasará a este nuevo estado, que hará lo mismo, y así sucesivamente, acorde a los eventos presentes en el string. Finalmente se irían concatenando todos los subprocesos para dar una traducción requerida. Para hacer esto más fácil de entender, veamos el siguiente gráfico:
Como se puede observar, cada estado interpreta la información la manera más lógica para el. Por ejemplo, el evento “P” para Opening representa el inicio de la apertura hacia la posición 1, mientras que para Closing es el inicio del cierre desde a posición 4. De está manera se evita un if que evalúe en que posición estamos de la puerta; el propio estado lo hace explícito. Del mismo modo para Open y Closed, la lógica se ha reducido tanto que solo procesan “.” en sus posiciones respectivas “5” y “0” haciendo muy simple su implementación.
Como ejemplo de implementación de los estados, podemos ver el de CLOSED que solo transicionará a opening (ver diagrama inicial):
**import { DoorState } from './DoorState';
import { Door } from '../door';
import { Opening } from './Opening';
export class Closed implements DoorState {
constructor(private door: Door) {}
processEvents(events: string): string {
let index = 0;
let eventsProcessed = '';
const eventsToProcess = events.split('');
while (eventsToProcess[index] === '.' && index < eventsToProcess.length) {
const closed = '0';
eventsProcessed += closed;
index++;
}
const buttonWasPressed = 'P';
if (eventsToProcess[index] === buttonWasPressed) {
this.door.changeState(new Opening(this.door));
const restOfEvents = events.substring(index);
return eventsProcessed + this.door.processEvents(restOfEvents);
}
return eventsProcessed;
}
}**
Uno de los aspectos más interesantes es que el diagrama, anteriormente dibujado, puede autogenerarse ahora desde nuestro código fuente:
Ver código completo:
En conclusión, el patrón estado se puede utilizar para modelar el comportamiento de la puerta de garaje en la Kata Killer Garage Door de CodeWars. Al utilizar este patrón, podemos separar el comportamiento de la puerta en diferentes clases, lo que hace que el código sea más fácil de entender y mantener. Por otro lado, hay que tener en cuenta que es más fácil hacer TDD cuando los estados están bien definidos y se quieren comprobar sus transiciones, en lugar de trabajar con múltiples propiedades cuya combinación representa un estado.
Agradecimientos a Adrián Ferrera por el feedback que nos ayudó a entender mejor el patrón.
¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.
Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?
Pero espera 🖐 que tenemos un conflicto interno. A nosotros las newsletter nos parecen 💩👎👹 Por eso hemos creado la LEAN LISTA, la primera lista zen, disfrutona y que suena a rock y reggaeton del sector de la programación. Todos hemos recibido newsletters por encima de nuestras posibilidades 😅 por eso este es el compromiso de la Lean Lista