State
Un patrón de comportamiento: el objeto delega en su estado actual qué puede hacer, y ese estado puede cambiar a lo largo de su vida.
State es un patrón de comportamiento que permite que un objeto cambie su comportamiento cuando cambia su estado interno, dando la impresión de que cambió de clase.
El problema
Un pedido de AndesShop pasa por los estados PENDING, PAID, SHIPPED, DELIVERED y CANCELLED, y lo que se puede hacer con él depende de en cuál esté: se puede cancelar si está PENDING o PAID, pero no si ya está SHIPPED; se puede marcar como enviado solo si está PAID. Modelar esto con un campo status y un condicional gigante en cada método (cancel(), markAsShipped(), etc.) que revisa en qué estado está, funciona al principio — pero cada estado nuevo obliga a revisar y actualizar todos esos condicionales, en todos los métodos, y es fácil olvidarse de uno.
La solución
State extrae el comportamiento específico de cada estado a su propia clase, y todas implementan la misma interfaz. El pedido delega en su estado actual las decisiones sobre qué hacer, y es el propio objeto de estado quien decide, además, a qué estado pasar después.
classDiagram
class Order {
<<context>>
-state OrderState
+cancel() void
+markAsPaid() void
+markAsShipped() void
~setState(OrderState) void
}
class OrderState {
<<interface>>
+cancel(Order) void
+markAsPaid(Order) void
+markAsShipped(Order) void
}
class PendingState
class PaidState
class ShippedState
class DeliveredState
class CancelledState
Order --> "1" OrderState : state actual
OrderState <|.. PendingState
OrderState <|.. PaidState
OrderState <|.. ShippedState
OrderState <|.. DeliveredState
OrderState <|.. CancelledState
PendingState ..> PaidState : markAsPaid
PendingState ..> CancelledState : cancel
PaidState ..> ShippedState : markAsShipped
PaidState ..> CancelledState : cancel
ShippedState ..> DeliveredState : deliverEjemplo en Java
interface OrderState {
void cancel(Order order);
void markAsShipped(Order order);
}
class PendingState implements OrderState {
public void cancel(Order order) {
order.setState(new CancelledState());
}
public void markAsShipped(Order order) {
throw new IllegalStateException("Cannot ship an unpaid order");
}
}
class PaidState implements OrderState {
public void cancel(Order order) {
order.setState(new CancelledState());
}
public void markAsShipped(Order order) {
order.setState(new ShippedState());
}
}
class ShippedState implements OrderState {
public void cancel(Order order) {
throw new IllegalStateException("Cannot cancel a shipped order");
}
public void markAsShipped(Order order) {
throw new IllegalStateException("Already shipped");
}
}
class CancelledState implements OrderState {
public void cancel(Order order) {
throw new IllegalStateException("Already cancelled");
}
public void markAsShipped(Order order) {
throw new IllegalStateException("Cannot ship a cancelled order");
}
}
class Order {
private OrderState state = new PendingState();
// Package-private a propósito: la transición la disparan los propios estados,
// que viven en este paquete. Si fuera pública, cualquier cliente podría poner
// un pedido en SHIPPED sin pasar por PaidState, rompiendo la invariante que
// este patrón busca preservar.
void setState(OrderState state) { this.state = state; }
public void cancel() { state.cancel(this); }
public void markAsShipped() { state.markAsShipped(this); }
}
Order order = new Order();
order.markAsShipped(); // lanza excepción: todavía está PENDING
Cuándo usarlo
- Cuando el comportamiento de un objeto cambia según su estado interno, y ese comportamiento involucra varios métodos que necesitan chequear el mismo campo de estado.
- Cuando el número de estados posibles y las transiciones entre ellos son lo bastante complejos como para que un único condicional se vuelva difícil de mantener.
Cuándo evitarlo
Con dos o tres estados de comportamiento casi idéntico entre sí, un enum con algún condicional simple puede ser más directo que crear una clase por estado.
Ventajas y desventajas
| Ventajas | Desventajas |
|---|---|
| Elimina condicionales grandes repartidos en varios métodos | Puede ser excesivo si hay pocos estados y poca diferencia de comportamiento entre ellos |
| Cada estado queda encapsulado en su propia clase, fácil de entender y modificar por separado | Agrega una clase nueva por cada estado |
| Las transiciones quedan explícitas, en vez de escondidas en asignaciones sueltas de un campo |
Relación con otros patrones
- Es la forma orientada a objetos de proteger las transiciones válidas de un aggregate. El tema de arquitectura llama invariante justamente a lo que acá impide que un pedido enviado se cancele.
- Se implementa de forma muy similar a Strategy — ambos delegan comportamiento a un objeto intercambiable — pero con intención distinta: Strategy lo elige el código cliente; State lo cambian los propios objetos de estado entre sí, según transiciones internas.
