Patrón de diseño de estado en Java

1. Información general

En este tutorial, presentaremos uno de los patrones de diseño de comportamiento de GoF: el patrón de estado.

Al principio, daremos una descripción general de su propósito y explicaremos el problema que intenta resolver. Luego, veremos el diagrama UML del estado y la implementación del ejemplo práctico.

2. Patrón de diseño estatal

La idea principal del patrón de estado es permitir que el objeto cambie su comportamiento sin cambiar su clase. Además, al implementarlo, el código debería permanecer más limpio sin muchas declaraciones if / else.

Imagine que tenemos un paquete que se envía a una oficina de correos, el paquete en sí se puede pedir, luego se entrega a una oficina de correos y finalmente un cliente lo recibe. Ahora, dependiendo del estado actual, queremos imprimir su estado de entrega.

El enfoque más simple sería agregar algunos indicadores booleanos y aplicar declaraciones simples if / else dentro de cada uno de nuestros métodos en la clase. Eso no lo complicará mucho en un escenario simple. Sin embargo, podría complicar y contaminar nuestro código cuando obtengamos más estados para procesar, lo que resultará en aún más declaraciones if / else.

Además, toda la lógica para cada uno de los estados se distribuiría entre todos los métodos. Ahora, aquí es donde se podría considerar el uso del patrón de estado. Gracias al patrón de diseño State, podemos encapsular la lógica en clases dedicadas, aplicar el Principio de Responsabilidad Única y el Principio Abierto / Cerrado, tener un código más limpio y mantenible.

3. Diagrama UML

En el diagrama UML, vemos que la clase Context tiene un Estado asociado que va a cambiar durante la ejecución del programa.

Nuestro contexto va a delegar el comportamiento a la implementación estatal. En otras palabras, todas las solicitudes entrantes serán manejadas por la implementación concreta del estado.

Vemos que la lógica está separada y agregar nuevos estados es simple: se reduce a agregar otra implementación de estado si es necesario.

4. Implementación

Diseñemos nuestra aplicación. Como ya se mencionó, el paquete se puede pedir, entregar y recibir, por lo tanto, tendremos tres estados y la clase de contexto.

Primero, definamos nuestro contexto, que será una clase Package :

public class Package { private PackageState state = new OrderedState(); // getter, setter public void previousState() { state.prev(this); } public void nextState() { state.next(this); } public void printStatus() { state.printStatus(); } }

Como podemos ver, contiene una referencia para administrar el estado, observe los métodos previousState (), nextState () y printStatus () donde delegamos el trabajo al objeto de estado. Los estados se vincularán entre sí y cada estado establecerá otro basado en esta referencia pasada a ambos métodos.

El cliente interactuará con la clase Package , pero no tendrá que lidiar con la configuración de los estados, todo lo que el cliente tiene que hacer es ir al estado anterior o siguiente.

A continuación, vamos a tener PackageState que tiene tres métodos con las siguientes firmas:

public interface PackageState { void next(Package pkg); void prev(Package pkg); void printStatus(); }

Esta interfaz será implementada por cada clase estatal concreta.

El primer estado concreto será OrderedState :

public class OrderedState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new DeliveredState()); } @Override public void prev(Package pkg) { System.out.println("The package is in its root state."); } @Override public void printStatus() { System.out.println("Package ordered, not delivered to the office yet."); } }

Aquí, apuntamos al siguiente estado que ocurrirá después de que se solicite el paquete. El estado ordenado es nuestro estado raíz y lo marcamos explícitamente. Podemos ver en ambos métodos cómo se maneja la transición entre estados.

Echemos un vistazo a la clase DeliveredState :

public class DeliveredState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new ReceivedState()); } @Override public void prev(Package pkg) { pkg.setState(new OrderedState()); } @Override public void printStatus() { System.out.println("Package delivered to post office, not received yet."); } }

Nuevamente, vemos la vinculación entre los estados. El paquete está cambiando su estado de pedido a entregado, el mensaje en printStatus () también cambia.

El último estado es ReceivedState :

public class ReceivedState implements PackageState { @Override public void next(Package pkg) { System.out.println("This package is already received by a client."); } @Override public void prev(Package pkg) { pkg.setState(new DeliveredState()); } }

Aquí es donde llegamos al último estado, solo podemos retroceder al estado anterior.

Ya vemos que hay alguna recompensa ya que un estado conoce al otro. Los estamos haciendo estrechamente acoplados.

5. Prueba

Veamos cómo se comporta la implementación. Primero, verifiquemos si las transiciones de configuración funcionan como se esperaba:

@Test public void givenNewPackage_whenPackageReceived_thenStateReceived() { Package pkg = new Package(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(DeliveredState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(ReceivedState.class)); }

Luego, verifique rápidamente si nuestro paquete puede retroceder con su estado:

@Test public void givenDeliveredPackage_whenPrevState_thenStateOrdered() { Package pkg = new Package(); pkg.setState(new DeliveredState()); pkg.previousState(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); }

Después de eso, verifiquemos el cambio de estado y veamos cómo la implementación del método printStatus () cambia su implementación en tiempo de ejecución:

public class StateDemo { public static void main(String[] args) { Package pkg = new Package(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); } }

Esto nos dará el siguiente resultado:

Package ordered, not delivered to the office yet. Package delivered to post office, not received yet. Package was received by client. This package is already received by a client. Package was received by client.

A medida que cambiamos el estado de nuestro contexto, el comportamiento cambia, pero la clase sigue siendo la misma. Además de la API que utilizamos.

Además, ha ocurrido la transición entre los estados, nuestra clase cambió su estado y consecuentemente su comportamiento.

6. Desventajas

El inconveniente del patrón de estado es la recompensa al implementar la transición entre los estados. Eso hace que el estado esté codificado, lo cual es una mala práctica en general.

But, depending on our needs and requirements, that might or might not be an issue.

7. State vs. Strategy Pattern

Both design patterns are very similar, but their UML diagram is the same, with the idea behind them slightly different.

First, the strategy pattern defines a family of interchangeable algorithms. Generally, they achieve the same goal, but with a different implementation, for example, sorting or rendering algorithms.

In state pattern, the behavior might change completely, based on actual state.

Next, in strategy, the client has to be aware of the possible strategies to use and change them explicitly. Whereas in state pattern, each state is linked to another and create the flow as in Finite State Machine.

8. Conclusion

El patrón de diseño de estado es excelente cuando queremos evitar declaraciones primitivas if / else . En cambio, extraemos la lógica para separar clases y dejamos que nuestro objeto de contexto delegue el comportamiento a los métodos implementados en la clase de estado. Además, podemos aprovechar las transiciones entre los estados, donde un estado puede alterar el estado del contexto.

En general, este patrón de diseño es excelente para aplicaciones relativamente simples, pero para un enfoque más avanzado, podemos echar un vistazo al tutorial de Spring's State Machine.

Como de costumbre, el código completo está disponible en el proyecto GitHub.