Programación · Patrones de diseño
Patrón State explicado con ejemplos
Respuesta corta
State es un patrón de diseño de comportamiento que permite que un objeto cambie su comportamiento cuando cambia su estado interno, de modo que parece cambiar de clase. Cada estado se convierte en una clase aparte que implementa la misma interfaz, y el objeto principal, llamado contexto, delega su trabajo en el objeto de estado actual. Las transiciones ocurren al reemplazar ese objeto de estado, lo que elimina las largas cadenas de switch o if que comprueban un campo de estado en cada método.
Intención y el problema que resuelve
Intención: permitir que un objeto altere su comportamiento cuando cambia su estado interno. El objeto parecerá cambiar de clase. State es uno de los patrones de comportamiento del libro de la Banda de los Cuatro (Gang of Four, GoF); su ejemplo clásico es una conexión TCP que se comporta de forma distinta cuando está escuchando, establecida o cerrada.
Toma un pedido en línea. Puede estar pendiente, pagado, enviado o cancelado. Pagar solo funciona cuando está pendiente. Enviar solo funciona cuando está pagado. Cancelar se permite cuando está pendiente o pagado, pero no después de enviarse. Una primera versión guarda una cadena status y cada método empieza con un switch sobre ella. Con cuatro acciones y cuatro estados son dieciséis casos repartidos en cuatro métodos. Añadir un nuevo estado, como "reembolsado", significa editar cada método, y es fácil olvidarse de uno.
Con el patrón State, cada estado se convierte en una clase que sabe qué significa cada acción en ese estado. El pedido guarda una referencia a su objeto de estado actual y le reenvía cada acción.
Estructura y participantes
| Rol | Responsabilidad |
|---|---|
| Contexto | El objeto que usan los clientes; guarda el objeto de estado actual y delega en él el comportamiento dependiente del estado |
| Estado | Interfaz que declara las operaciones cuyo comportamiento depende del estado |
| Estado concreto | Implementa el comportamiento de un estado y decide cuál es el estado siguiente |
Las transiciones pueden decidirlas los propios estados (cada estado fija el siguiente en el contexto, como abajo) o el contexto mediante una tabla de transiciones. Dejar que decidan los estados mantiene junta la lógica relacionada; una tabla central hace visibles todas las transiciones en un solo lugar.
Implementación en TypeScript
Una clase base ofrece el comportamiento por defecto: rechazar la acción. Cada estado concreto sobrescribe solo las acciones que son válidas para él.
abstract class OrderState {
abstract readonly name: string;
pay(order: Order): void { this.reject("pay"); }
ship(order: Order): void { this.reject("ship"); }
cancel(order: Order): void { this.reject("cancel"); }
protected reject(action: string): never {
throw new Error("Cannot " + action + " an order that is " + this.name);
}
}
class Pending extends OrderState {
readonly name = "pending";
pay(order: Order) { order.setState(new Paid()); }
cancel(order: Order) { order.setState(new Cancelled()); }
}
class Paid extends OrderState {
readonly name = "paid";
ship(order: Order) { order.setState(new Shipped()); }
cancel(order: Order) { order.setState(new Cancelled()); } // refund here
}
class Shipped extends OrderState { readonly name = "shipped"; }
class Cancelled extends OrderState { readonly name = "cancelled"; }
class Order {
private state: OrderState = new Pending();
setState(next: OrderState) { this.state = next; }
get status() { return this.state.name; }
pay() { this.state.pay(this); }
ship() { this.state.ship(this); }
cancel() { this.state.cancel(this); }
}
const order = new Order();
order.pay();
order.ship();
console.log(order.status); // shipped
try { order.cancel(); } catch (e) { console.log((e as Error).message); }
// Cannot cancel an order that is shippedPara añadir un estado "reembolsado" escribes una clase nueva y cambias solo los estados que pueden llevar a él. Order en sí no cambia.
Antes y después
Las mismas reglas escritas con un campo de estado y sentencias switch. Esto funciona con pocos estados, pero cada método repite la lista completa:
type Status = "pending" | "paid" | "shipped" | "cancelled";
class OrderBefore {
status: Status = "pending";
ship() {
switch (this.status) {
case "paid": this.status = "shipped"; break;
default: throw new Error("Cannot ship an order that is " + this.status);
}
}
// pay() and cancel() repeat the same switch
}
const o = new OrderBefore();
try { o.ship(); } catch (e) { console.log((e as Error).message); }
// Cannot ship an order that is pendingPara casos pequeños, esta versión está perfectamente bien y podría decirse que es más fácil de leer. En TypeScript, un tipo unión para Status permite al compilador comprobar que se maneja cada caso. Pasa a clases de estado cuando el comportamiento de cada estado crece más allá de una o dos líneas.
Cuándo usarlo y cuándo no
- Úsalo cuando un objeto se comporta de forma muy distinta según su estado y las mismas comprobaciones de estado aparecen en muchos métodos.
- Úsalo cuando los estados y las transiciones cambian con el tiempo y quieres las reglas de cada estado en un solo lugar.
- Úsalo para flujos de trabajo (pedidos, documentos, tickets), conexiones de protocolo, personajes de juegos y componentes de interfaz como los reproductores multimedia.
- No lo uses cuando solo hay dos o tres estados con comportamiento trivial; un enum o tipo unión y un
switches más simple. - Para máquinas complejas con estados anidados o paralelos, considera una biblioteca de máquinas de estado o la notación de statecharts en lugar de clases escritas a mano.
Usos en el mundo real
- Las implementaciones de protocolos de red modelan estados de conexión como escuchando, establecida y cerrada, que es el ejemplo del libro GoF.
- XState es una biblioteca de JavaScript y TypeScript para máquinas de estado y statecharts, usada para la lógica de interfaz y los flujos de trabajo.
- La especificación SCXML del W3C define un formato XML para statecharts.
- El desarrollo de juegos usa objetos de estado para los personajes (quieto, corriendo, saltando), donde cada estado maneja la entrada de forma distinta.
State frente a Strategy y otros patrones
| Patrón | En qué se diferencia de State |
|---|---|
| Strategy | La estructura de clases es casi la misma. Una estrategia normalmente la elige el cliente y no cambia por sí sola; los estados se reemplazan unos a otros como resultado de las propias acciones del objeto. |
| Flyweight | Los objetos de estado sin campos pueden compartirse entre todos los contextos como flyweights o constantes. |
| Singleton | Los objetos de estado sin estado propio suelen ser instancias únicas compartidas. |
| Memento | Memento guarda los datos de un objeto para restaurarlos más tarde; State cambia qué comportamiento usa el objeto. |
Preguntas frecuentes
¿Cuál es la diferencia entre State y Strategy?
Ambos delegan el comportamiento en un objeto intercambiable. En Strategy, el cliente suele elegir el algoritmo y este permanece fijo. En State, los objetos de estado se conocen entre sí y cambian el contexto a un nuevo estado como parte del funcionamiento normal.
¿El patrón State es lo mismo que una máquina de estados finitos?
El patrón State es una forma de implementar una máquina de estados finitos en código orientado a objetos: cada estado es una clase y cada transición reemplaza el objeto de estado actual. Las máquinas también pueden implementarse con tablas de transiciones o bibliotecas.
¿Quién debe decidir el siguiente estado: el contexto o el estado?
Ambos funcionan. Cuando deciden los estados, la lógica de cada estado se mantiene junta. Cuando decide el contexto o una tabla de transiciones, todas las transiciones son visibles en un solo lugar y los estados no necesitan conocerse entre sí.
¿Se pueden compartir los objetos de estado?
Sí, si no guardan datos propios. Entonces una instancia de cada estado puede ser compartida por todos los objetos de contexto, lo que ahorra reservas de memoria.