sábado, 24 de agosto de 2013

Ejemplo del patrón de diseño State

Java
Un patrón de diseño aplicado adecuadamente para resolver un problema puede ayudar enormemente a simplificar el código y facilitar el mantenimiento. Si tenemos un código que es difícil de mantener y entender, hay código duplicado y no tiene ninguna organización puede que aplicar un patrón de diseño nos resuelva el problema en gran parte.

Hace ya un tiempo comente cuales son los principales patrones de diseño y hice una entrada con un ejemplo del patrón de diseño Command. En esta entrada pondré un ejemplo del patrón de diseño State.

El patrón de diseño State nos puede ser de mucha utilidad en los casos que por ejemplo una entidad tenga asociado un grafo de estados con transiciones permitidas y no permitidas entre algunos estados. En función del estado, sus datos y la transición la entidad puede comportarse de forma diferente. Por ejemplo, supongamos que tenemos una entidad Compra que a lo largo de su vida en la aplicación pasa por diferentes estados:
  • creada: la compra se acaba de crear.
  • en espera: se ha hecho una compra y se está esperando que el pago sea correcto.
  • verificada: el pago es correcto y se está esperando a enviar el producto.
  • cancelada: la compra se ha cancelado porque el usuario no quiere ya el producto, no hay existencias u otro motivo.
  • enviada: el pedido ha sido enviado.
Y tiene diferentes transiciones como:
  • comprar: la compra pasa de creada a en espera de verificarla.
  • verificar: la compra pasa de en espera a verificada y esperando a enviarse.
  • cancelar: la compra se puede cancelar excepto una vez que ya se ha enviado.
  • enviar: la compra se envía al usuario y ya no puede cancelarse.
Diagrama de estados

Si diseñamos este flujo de estados sin el patrón State probablemente acabemos con una clase con un montón de condiciones y métodos de bastantes líneas sin una organización clara a simple vista. Para evitarlo aplicaremos el patrón State a este pequeño flujo de estados. En cuanto a código este patrón se basa en dos ideas:
  • Cada estado será representado una clase.
  • Cada una de estas clases contendrá un método por cada posible transición.
Y estas dos simples ideas son suficientes para guiar la tarea de codificación. Por lo tanto tendremos los siguientes clases que representarán a los estados: CreadaCompraState, EnEsperaCompraState, VerificadaCompraState, CanceladaCompraState y EnvidaCompraState.

package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import java.math.BigDecimal;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaEnvio;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaPago;
public interface CompraState {
void comprar(BigDecimal precio, FormaPago formaPago, FormaEnvio formaEnvio);
void verificar();
void cancelar();
void enviar();
}

Según el diagrama de estados y transiciones no todas las transiciones son posibles, una compra en espera no puede enviarse. Pero en el código estamos haciendo que todos los estados tengan todos los métodos que representan todas las transiciones, la forma de hacer en el código que una transición no sea posible para un determinado estado es lanzando una excepción en su correspondiente método, el método enviar del estado en espera, lanzará una excepción ya que es este estado aún la compra no puede enviarse. La clase abstracta AbstractState implementará la interfaz CompraState y lanzará una excepción en todos los métodos, las clases que extiendan de esta podrán redefinir los métodos que necesiten utilizando la propiedad de la programación orientada a objetos del polimorfismo, esta clase abstracta nos permitirá implementar en cada clase de estado únicamente los métodos con las transiciones válidas. Las clases nos podrían quedar de la siguiente forma:

package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import java.math.BigDecimal;
import java.text.MessageFormat;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaEnvio;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaPago;
public abstract class AbstractCompraState implements CompraState {
private Compra compra;
public AbstractCompraState(Compra compra) {
this.compra = compra;
}
public Compra getCompra() {
return compra;
}
@Override
public void comprar(BigDecimal precio, FormaPago formaPago, FormaEnvio formaEnvio) {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede comprarse", compra.getEstado().getClass().getSimpleName()));
}
@Override
public void verificar() {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede verificarse", compra.getEstado().getClass().getSimpleName()));
}
@Override
public void cancelar() {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede cancelarse", compra.getEstado().getClass().getSimpleName()));
}
@Override
public void enviar() {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede enviarse", compra.getEstado().getClass().getSimpleName()));
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import java.math.BigDecimal;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaEnvio;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaPago;
import es.com.blogspot.elblogdepicodev.pattern.state.CompraStateFactory.Estado;
public class CreadaCompraState extends AbstractCompraState {
public CreadaCompraState(Compra compra) {
super(compra);
}
@Override
public void comprar(BigDecimal precio, FormaPago formaPago, FormaEnvio formaEnvio) {
getCompra().setPrecio(precio);
getCompra().setFormaPago(formaPago);
getCompra().setFormaEnvio(formaEnvio);
getCompra().setEstado(Estado.EN_ESPERA);
}
@Override
public void cancelar() {
getCompra().setEstado(Estado.CANCELADA);
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
import es.com.blogspot.elblogdepicodev.pattern.state.CompraStateFactory.Estado;
public class VerificadaCompraState extends AbstractCompraState {
public VerificadaCompraState(Compra compra) {
super(compra);
}
@Override
public void enviar() {
getCompra().setEstado(Estado.ENVIADA);
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
public class EnviadaCompraState extends AbstractCompraState {
public EnviadaCompraState(Compra compra) {
super(compra);
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
public class CanceladaCompraState extends AbstractCompraState {
public CanceladaCompraState(Compra compra) {
super(compra);
}
}
Para que los métodos puedan acceder y manipular los datos de la compra se les pasa como parámetro en el constructor. La factoría CompraStateFactory encapsula la lógica para construir cada uno de los estados. La implementación de cada uno de los estado podría ser la siguiente.

package es.com.blogspot.elblogdepicodev.pattern.state;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CanceladaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CreadaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.EnEsperaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.EnviadaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.VerificadaCompraState;
public class CompraStateFactory {
public enum Estado {
CREADA, EN_ESPERA, VERIFICADA, CANCELADA, ENVIADA
}
public static CompraState buildState(Estado estado, Compra compra) {
CompraState cs = null;
switch (estado) {
case CREADA:
cs = new CreadaCompraState(compra);
break;
case EN_ESPERA:
cs = new EnEsperaCompraState(compra);
break;
case VERIFICADA:
cs = new VerificadaCompraState(compra);
break;
case CANCELADA:
cs = new CanceladaCompraState(compra);
break;
case ENVIADA:
cs = new EnviadaCompraState(compra);
break;
default:
throw new IllegalArgumentException();
}
return cs;
}
}
Finalmente, la clase Compra podría ser de la siguiente forma:

package es.com.blogspot.elblogdepicodev.pattern.state;
import java.math.BigDecimal;
import es.com.blogspot.elblogdepicodev.pattern.state.CompraStateFactory.Estado;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CompraState;
public class Compra {
public enum FormaPago {
PAYPAL, TARJETA_CREDITO
}
public enum FormaEnvio {
UPS, TNT, SEUR
}
private CompraState estado;
private BigDecimal precio;
private FormaPago formaPago;
private FormaEnvio formaEnvio;
public Compra() {
setEstado(Estado.CREADA);
}
public CompraState getEstado() {
return estado;
}
public void setEstado(CompraState estado) {
this.estado = estado;
}
public void setEstado(Estado estado) {
this.estado = CompraStateFactory.buildState(estado, this);
}
public BigDecimal getPrecio() {
return precio;
}
public void setPrecio(BigDecimal precio) {
this.precio = precio;
}
public FormaPago getFormaPago() {
return formaPago;
}
public void setFormaPago(FormaPago formaPago) {
this.formaPago = formaPago;
}
public FormaEnvio getFormaEnvio() {
return formaEnvio;
}
public void setFormaEnvio(FormaEnvio formaEnvio) {
this.formaEnvio = formaEnvio;
}
}
view raw Compra.java hosted with ❤ by GitHub

El patrón State puede facilitarnos bastante la vida como programadores pero si el diagrama de estados fuese más complejo, con mucha lógica de negocio y el flujo dependiense de información independiente de la compra quizá deberíamos evaluar su si un motor de procesos (BPMS) como Activiti y un sistema de reglas de negocio (BRMS) como Drools sería más adecuado.

El código fuente completo de ejemplo los puedes obtener de mi repositorio de github con los siguiente comandos:

$ git clone git://github.com/picodotdev/elblogdepicodev.git
$ cd elblogdepicodev/PatronState
$ ./gradlew test
view raw git.sh hosted with ❤ by GitHub
Referencia:
Patrones de diseño en la programación orientada a objetos
Ejemplo del patrón de diseño Command y programación concurrente en Java
Ejemplo del patrón de diseño No Operation