leanmind logo leanmind text logo

Blog

BDD

Behaviour-driven Development es una técnica para tomar mejores requisitos de producto.

Aplicando el Patrón Estado (Pattern State) con la Kata Garage Door

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.

switch on switch off

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:

lightswitch code example

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.

door image example

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.

Implementación en C#

En este enfoque hemos creado 3 estados Closing, Opening y Pause

kata uml diagram

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

Implementación en TypeScript

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:

door states

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:

door processes events

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:

events diagrams

Ver código completo:

Door kata ts - Marius9595

Conclusión

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.

Publicado el 23/01/2025 por

¿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?

Impulsamos el crecimiento profesional de tu equipo de developers