Patrón State: Máquinas de Estado Elegantes
design-patternsbehavioralstate

¿Qué es el Patrón State?
El patrón State permite que un objeto altere su comportamiento cuando su estado interno cambia. El objeto parecerá cambiar de clase, delegando las operaciones al objeto de estado actual.
stateDiagram-v2
[*] --> Draft
Draft --> Pending : confirm()
Pending --> Paid : pay()
Pending --> Cancelled : cancel()
Paid --> Shipped : ship()
Paid --> Cancelled : cancel()
Shipped --> Delivered : deliver()
Delivered --> [*]
Cancelled --> [*]Cuándo Usar State
- Cuando un objeto tiene comportamiento que depende de su estado
- Cuando tienes múltiples condicionales que dependen del estado del objeto
- Cuando necesitas una máquina de estados con transiciones bien definidas
- Cuando quieres evitar código con muchos if/else o switch basados en estado
El Problema
// Sin State Pattern - Código con múltiples condicionales
class Order {
status: 'draft' | 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
confirm(): void {
if (this.status === 'draft') {
this.status = 'pending';
} else {
throw new Error(`Cannot confirm order in ${this.status} state`);
}
}
pay(): void {
if (this.status === 'pending') {
this.status = 'paid';
} else {
throw new Error(`Cannot pay order in ${this.status} state`);
}
}
ship(): void {
if (this.status === 'paid') {
this.status = 'shipped';
} else {
throw new Error(`Cannot ship order in ${this.status} state`);
}
}
// ... más métodos con más condicionales
}La Solución: State Pattern
1. Definir la Interfaz State
interface OrderState {
readonly name: string;
confirm(order: Order): Promise<void>;
pay(order: Order, paymentId: string): Promise<void>;
ship(order: Order, trackingNumber: string): Promise<void>;
deliver(order: Order): Promise<void>;
cancel(order: Order, reason: string): Promise<void>;
refund(order: Order): Promise<void>;
}2. Crear Clase Base con Comportamiento por Defecto
abstract class BaseOrderState implements OrderState {
abstract readonly name: string;
async confirm(_order: Order): Promise<void> {
throw new Error(`Cannot confirm order in ${this.name} state`);
}
async pay(_order: Order, _paymentId: string): Promise<void> {
throw new Error(`Cannot pay order in ${this.name} state`);
}
async ship(_order: Order, _trackingNumber: string): Promise<void> {
throw new Error(`Cannot ship order in ${this.name} state`);
}
async deliver(_order: Order): Promise<void> {
throw new Error(`Cannot deliver order in ${this.name} state`);
}
async cancel(_order: Order, _reason: string): Promise<void> {
throw new Error(`Cannot cancel order in ${this.name} state`);
}
async refund(_order: Order): Promise<void> {
throw new Error(`Cannot refund order in ${this.name} state`);
}
}3. Implementar Estados Concretos
Cada estado solo implementa las transiciones válidas:
/** Estado inicial - puede confirmar o cancelar */
class DraftState extends BaseOrderState {
readonly name = 'draft';
async confirm(order: Order): Promise<void> {
console.log('Order confirmed, awaiting payment');
order.setState(new PendingPaymentState());
}
async cancel(order: Order, reason: string): Promise<void> {
console.log(`Order cancelled: ${reason}`);
order.setState(new CancelledState());
}
}
/** Esperando pago - puede pagar o cancelar */
class PendingPaymentState extends BaseOrderState {
readonly name = 'pending_payment';
async pay(order: Order, paymentId: string): Promise<void> {
console.log(`Payment received: ${paymentId}`);
order.context.paymentId = paymentId;
order.setState(new PaidState());
}
async cancel(order: Order, reason: string): Promise<void> {
console.log(`Order cancelled: ${reason}`);
order.setState(new CancelledState());
}
}
/** Pagado - puede enviar o reembolsar */
class PaidState extends BaseOrderState {
readonly name = 'paid';
async ship(order: Order, trackingNumber: string): Promise<void> {
console.log(`Order shipped: ${trackingNumber}`);
order.context.trackingNumber = trackingNumber;
order.setState(new ShippedState());
}
async refund(order: Order): Promise<void> {
console.log('Processing refund...');
order.setState(new RefundedState());
}
}
/** Enviado - puede entregar */
class ShippedState extends BaseOrderState {
readonly name = 'shipped';
async deliver(order: Order): Promise<void> {
console.log('Order delivered!');
order.setState(new DeliveredState());
}
}
/** Entregado - puede reembolsar */
class DeliveredState extends BaseOrderState {
readonly name = 'delivered';
async refund(order: Order): Promise<void> {
console.log('Processing refund for delivered order...');
order.setState(new RefundedState());
}
}
/** Estados terminales */
class CancelledState extends BaseOrderState {
readonly name = 'cancelled';
}
class RefundedState extends BaseOrderState {
readonly name = 'refunded';
}4. Crear el Context (Order)
interface OrderContext {
orderId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
paymentId?: string;
trackingNumber?: string;
}
class Order {
private state: OrderState = new DraftState();
private history: Array<{ state: string; timestamp: Date }> = [];
public context: OrderContext;
constructor(orderId: string) {
this.context = {
orderId,
items: [],
total: 0,
};
this.recordHistory();
}
setState(state: OrderState): void {
this.state = state;
this.recordHistory();
}
private recordHistory(): void {
this.history.push({
state: this.state.name,
timestamp: new Date(),
});
}
getHistory() {
return [...this.history];
}
getCurrentState(): string {
return this.state.name;
}
// Delegar al estado actual
confirm(): Promise<void> {
return this.state.confirm(this);
}
pay(paymentId: string): Promise<void> {
return this.state.pay(this, paymentId);
}
ship(trackingNumber: string): Promise<void> {
return this.state.ship(this, trackingNumber);
}
deliver(): Promise<void> {
return this.state.deliver(this);
}
cancel(reason: string): Promise<void> {
return this.state.cancel(this, reason);
}
refund(): Promise<void> {
return this.state.refund(this);
}
}Ejemplo de Uso
const order = new Order('ORD-001');
// Flujo normal
await order.confirm(); // draft -> pending_payment
await order.pay('PAY-123'); // pending_payment -> paid
await order.ship('TRACK-456'); // paid -> shipped
await order.deliver(); // shipped -> delivered
// Historial de estados
order.getHistory().forEach(h => {
console.log(`${h.state} at ${h.timestamp.toISOString()}`);
});
// Intentar transición inválida
try {
await order.cancel('Changed mind');
} catch (error) {
console.log(error.message); // Cannot cancel order in delivered state
}Diagrama de Estados
stateDiagram-v2
[*] --> Draft
Draft --> PendingPayment: confirm()
Draft --> Cancelled: cancel()
PendingPayment --> Paid: pay()
PendingPayment --> Cancelled: cancel()
Paid --> Shipped: ship()
Paid --> Refunded: refund()
Shipped --> Delivered: deliver()
Delivered --> Refunded: refund()
Cancelled --> [*]
Refunded --> [*]Resumen
| Aspecto | Descripción |
|---|---|
| Propósito | Cambiar comportamiento según estado interno |
| Problema | Lógica condicional compleja basada en estado |
| Solución | Encapsular cada estado en una clase separada |
| Beneficio | Single Responsibility - cada estado maneja su lógica |
Conclusión
El patrón State es ideal para modelar máquinas de estados complejas de forma limpia y mantenible. Cada estado encapsula su propia lógica, haciendo el código más fácil de entender y extender.
Basado en "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).