Observer dejó una pregunta abierta: nada impide hoy cambiarEstado(ENTREGADO) sobre un pedido recién creado y sin pagar. Y el problema es más hondo que validar transiciones: casi todo lo que se puede hacer con un pedido depende de dónde esté en su ciclo de vida. ¿Se puede cancelar? Gratis si está creado, con penalización si está en preparación, imposible en reparto. ¿Se puede modificar? Solo antes de pagar. El código actual responde con un switch sobre el estado... repetido en cada método, creciendo con cada estado nuevo. State propone la reificación que ya esperas a estas alturas del módulo: cada estado es un objeto, con el comportamiento de ese estado dentro, y el pedido delega en su estado actual — que además decide a cuál se transita.
Contenido
- El problema en PideYa: el ciclo de vida del pedido
- El primer intento: switch sobre un enum
- Intención y estructura del patrón
- El diagrama de estados
- Implementación Java completa
- Variantes de diseño
- State vs. switch: la comparación honesta
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- Ejercicios y conclusión
El problema en PideYa: el ciclo de vida del pedido
Las reglas de negocio, tal como las dicta operaciones:
| Estado | ¿Pagar? | ¿Modificar líneas? | ¿Cancelar? | Transiciones salientes |
|---|---|---|---|---|
| CREADO | Sí | Sí | Sí, gratis | PAGADO, CANCELADO |
| PAGADO | No (ya está) | No | Sí, con devolución total | EN_PREPARACION, CANCELADO |
| EN_PREPARACION | No | No | Sí, con penalización del 20% | EN_REPARTO, CANCELADO |
| EN_REPARTO | No | No | No (ya va de camino) | ENTREGADO |
| ENTREGADO | No | No | No | — (final) |
| CANCELADO | No | No | No | — (final) |
Fíjate en la naturaleza del problema: no es un comportamiento con variantes (eso sería otra cosa, y tiene su lección justo después); es el mismo objeto comportándose como seis objetos distintos según el momento. La palabra técnica es que Pedido es una máquina de estados finitos: estados, transiciones permitidas y comportamiento por estado.
El primer intento: switch sobre un enum
public class Pedido {
private EstadoPedidoEnum estado = EstadoPedidoEnum.CREADO;
public void pagar() {
switch (estado) {
case CREADO -> { cobrar(); estado = EstadoPedidoEnum.PAGADO; }
default -> throw new TransicionInvalidaException("No se puede pagar en " + estado);
}
}
public void cancelar() {
switch (estado) {
case CREADO -> estado = EstadoPedidoEnum.CANCELADO;
case PAGADO -> { devolverTodo(); estado = EstadoPedidoEnum.CANCELADO; }
case EN_PREPARACION -> { devolverConPenalizacion(); estado = EstadoPedidoEnum.CANCELADO; }
default -> throw new TransicionInvalidaException("No se puede cancelar en " + estado);
}
}
public void modificarLinea(...) { switch (estado) { /* ... */ } }
public void marcarEnReparto() { switch (estado) { /* ... */ } }
// ... un switch por operación
}Es honesto decirlo: para seis estados y cuatro operaciones estables, esto funciona y es legible. Los problemas aparecen con la evolución:
- La lógica de cada estado está esparcida por todos los métodos: para saber "qué se puede hacer EN_PREPARACION" hay que leer un
caseen cadaswitchde la clase. El estado no existe como concepto en el código; existe como etiqueta repetida. - Añadir un estado (llega
EN_ESPERA_REPARTIDOR, cortesía de la CentralReparto) significa tocar todos los switches — y olvidarse de uno no da error de compilación si haydefault. - El comportamiento por estado crece: penalizaciones que dependen del tiempo en preparación, notificaciones distintas por estado... cada
caseengorda hasta quePedidoes un archivador de condicionales. - La combinación estado × operación es propensa al caso olvidado: el bug de Observer (entregar sin pagar) era exactamente un caso sin
defaultdefensivo.
Intención y estructura del patrón
Intención (GoF): permitir que un objeto altere su comportamiento cuando su estado interno cambia. El objeto parecerá cambiar de clase.
"Parecerá cambiar de clase" es la frase clave: un Pedido en reparto se comporta como otra clase que uno creado — porque, por debajo, su comportamiento vive en otra clase.
classDiagram
class Pedido {
-estado: EstadoPedido
+pagar()
+cancelar()
+modificarLinea(...)
+marcarEnReparto()
~transitarA(nuevo: EstadoPedido)
}
class EstadoPedido {
<<interface>>
+pagar(p: Pedido)
+cancelar(p: Pedido)
+modificarLinea(p: Pedido, ...)
+marcarEnReparto(p: Pedido)
+nombre() String
}
class EstadoCreado
class EstadoPagado
class EstadoEnPreparacion
class EstadoEnReparto
class EstadoEntregado
class EstadoCancelado
EstadoPedido <|.. EstadoCreado
EstadoPedido <|.. EstadoPagado
EstadoPedido <|.. EstadoEnPreparacion
EstadoPedido <|.. EstadoEnReparto
EstadoPedido <|.. EstadoEntregado
EstadoPedido <|.. EstadoCancelado
Pedido o-- EstadoPedido : delega en el actual
| Rol GoF | En PideYa |
|---|---|
| Context (mantiene el estado actual y delega) | Pedido |
| State (interfaz con las operaciones dependientes del estado) | EstadoPedido |
| ConcreteState | EstadoCreado, EstadoPagado, EstadoEnPreparacion, EstadoEnReparto, EstadoEntregado, EstadoCancelado |
El diagrama de estados
La máquina completa, en el diagrama UML pensado exactamente para esto (lo presentamos en la lección de UML; aquí rinde al máximo):
stateDiagram-v2
[*] --> CREADO
CREADO --> PAGADO : pagar()
CREADO --> CANCELADO : cancelar() / gratis
PAGADO --> EN_PREPARACION : cocina acepta
PAGADO --> CANCELADO : cancelar() / devolución total
EN_PREPARACION --> EN_REPARTO : repartidor recoge
EN_PREPARACION --> CANCELADO : cancelar() / penalización 20%
EN_REPARTO --> ENTREGADO : entrega confirmada
ENTREGADO --> [*]
CANCELADO --> [*]
Consejo de oficio: dibuja este diagrama antes de escribir una línea del patrón. Es el contrato con negocio (¿de verdad no se puede cancelar en reparto?), la lista exacta de clases a escribir, y la fuente de los tests (cada flecha, un test que pasa; cada no-flecha, un test que espera excepción).
Implementación Java completa
La interfaz State, con implementaciones por defecto que rechazan: así cada estado concreto solo escribe lo que permite, y lo no escrito queda prohibido automáticamente — el caso olvidado se vuelve imposible:
public interface EstadoPedido {
default void pagar(Pedido p) { rechazar("pagar", p); }
default void cancelar(Pedido p) { rechazar("cancelar", p); }
default void modificarLinea(Pedido p, LineaPedido l) { rechazar("modificar", p); }
default void marcarEnPreparacion(Pedido p){ rechazar("preparar", p); }
default void marcarEnReparto(Pedido p) { rechazar("repartir", p); }
default void marcarEntregado(Pedido p) { rechazar("entregar", p); }
String nombre();
private void rechazar(String operacion, Pedido p) {
throw new TransicionInvalidaException(
"No se puede " + operacion + " un pedido en estado " + nombre());
}
}Los estados concretos. Cada clase es la columna de la tabla de negocio hecha código — compacta, cohesionada, con nombre propio:
public class EstadoCreado implements EstadoPedido {
@Override
public void pagar(Pedido p) {
p.cobrarTotal(); // trabajo del contexto
p.transitarA(new EstadoPagado()); // transición decidida AQUÍ
}
@Override
public void cancelar(Pedido p) {
p.transitarA(new EstadoCancelado()); // gratis: sin devolución
}
@Override
public void modificarLinea(Pedido p, LineaPedido linea) {
p.aplicarModificacion(linea); // único estado que lo permite
}
@Override
public String nombre() { return "CREADO"; }
}
public class EstadoEnPreparacion implements EstadoPedido {
@Override
public void cancelar(Pedido p) {
p.devolverConPenalizacion(new BigDecimal("0.20"));
p.transitarA(new EstadoCancelado());
}
@Override
public void marcarEnReparto(Pedido p) {
p.transitarA(new EstadoEnReparto());
}
@Override
public String nombre() { return "EN_PREPARACION"; }
}
public class EstadoEntregado implements EstadoPedido {
// Estado final: no sobrescribe nada — todo rechazado por defecto
@Override
public String nombre() { return "ENTREGADO"; }
}El contexto. Pedido queda limpio de condicionales: expone las operaciones y delega; y transitarA es el único punto por donde cambia el estado — el lugar perfecto para conectar con Observer:
public class Pedido {
private EstadoPedido estado = new EstadoCreado();
public void pagar() { estado.pagar(this); }
public void cancelar() { estado.cancelar(this); }
public void modificarLinea(LineaPedido l) { estado.modificarLinea(this, l); }
public void marcarEnReparto() { estado.marcarEnReparto(this); }
public void marcarEntregado() { estado.marcarEntregado(this); }
/** Única puerta de cambio de estado; visibilidad de paquete: solo los estados la usan. */
void transitarA(EstadoPedido nuevo) {
EstadoPedido anterior = this.estado;
this.estado = nuevo;
RegistroEventos.INSTANCIA.info("Pedido " + id + ": " + anterior.nombre() + " → " + nuevo.nombre());
notificarObservadores(anterior); // Observer difunde lo que State autoriza
}
// cobrarTotal(), devolverConPenalizacion(...), aplicarModificacion(...): trabajo real,
// invocado por los estados; los datos siguen encapsulados aquí.
}Las dos piezas del módulo encajan como estaban destinadas: State decide qué transiciones son legales; Observer difunde las que ocurren. El bug de "entregar sin pagar" ya no puede existir — EstadoCreado no sobrescribe marcarEntregado, así que la llamada muere en una TransicionInvalidaException antes de notificar nada.
Y el reparto de responsabilidades importa: los estados deciden qué pasa y a dónde se va; el contexto ejecuta el trabajo (cobrarTotal) y custodia sus datos. Estados que manosean los campos del pedido son el primer paso hacia el barro.
Variantes de diseño
- ¿Estados con o sin datos propios? Los nuestros son sin estado propio (todo el dato vive en
Pedido): pueden ser compartidos — constantes reutilizadas en vez denewen cada transición (EstadoPagado.INSTANCIA, espíritu Flyweight). Si un estado necesita datos propios (EN_REPARTO guardando el repartidor asignado), se instancia por transición y los lleva dentro. - ¿Quién decide la transición? Aquí, cada estado (lo habitual: la regla "de CREADO se va a PAGADO" es conocimiento del estado). Alternativa: el contexto o una tabla de transiciones externa deciden, y los estados solo ejecutan comportamiento — más declarativo, menos orientado a objetos.
- Acciones de entrada/salida: métodos
alEntrar(Pedido)/alSalir(Pedido)en la interfaz, invocados portransitarA— sitio idóneo para efectos por estado (al entrar en EN_REPARTO, avisar a la CentralReparto). - State con enum: en Java, un enum puede implementar métodos por constante (
CREADO { void pagar(...) {...} }, PAGADO {...}) — el patrón State completo con la garantía de exhaustividad del enum. Excelente para máquinas sin datos por estado; se queda corto cuando los estados los necesitan (los enum no tienen instancias por transición).
State vs. switch: la comparación honesta
| Criterio | switch sobre enum |
Patrón State |
|---|---|---|
| Estados y operaciones pocos y estables | Gana: menos piezas, todo a la vista | Burocracia |
| Lógica por estado voluminosa/creciente | Métodos-archivador | Gana: una clase cohesionada por estado |
| Añadir un estado | Tocar N switches (sin ayuda del compilador si hay default) |
Añadir una clase; lo demás intacto |
| Añadir una operación | Un método nuevo con su switch | Tocar la interfaz y (con defaults) solo los estados que la permiten |
| Ver la máquina completa de un vistazo | Regular (esparcida por métodos) | Mal (esparcida por clases) — mantén el stateDiagram como documentación |
| Datos por estado | Incómodo (campos que solo valen a veces) | Natural (campos del estado concreto) |
La regla honesta: el switch no es el villano; es la solución correcta para máquinas pequeñas y estables. State se gana el pan cuando el comportamiento por estado pesa y la máquina evoluciona — exactamente el caso del pedido de PideYa, cuyo ciclo de vida es el corazón cambiante del negocio.
Cuándo usarlo y cuándo no
Úsalo cuando:
- El comportamiento de un objeto depende de su estado y debe cambiar en ejecución al transitar.
- Las operaciones tienen condicionales grandes y repetidos sobre el mismo discriminante de estado.
- Los estados tienen comportamiento sustancial o datos propios, o la máquina crece con el negocio.
Evítalo cuando:
- La máquina es pequeña y estable: enum + switch (o enum con métodos) es más directo.
- El "estado" es solo un dato consultado, sin comportamiento asociado: un campo normal basta.
- Las "variantes de comportamiento" no forman ciclo de vida (no hay transiciones): eso es Strategy — la distinción exacta, en una línea aquí y en detalle en la comparativa: State transita solo entre estados que se conocen; Strategy es elegida desde fuera e ignora a sus hermanas.
Relación con otros patrones
- Strategy: estructuralmente gemelos (contexto que delega en un objeto intercambiable); la diferencia es de intención y de quién cambia el objeto — mención hecha, careo en la siguiente lección y en la comparativa.
- Observer: State autoriza transiciones, Observer las difunde; conectados en
transitarA. - Singleton/Flyweight: estados sin datos compartidos como instancias únicas.
- Memento: para retroceder la máquina a un punto anterior, se fotografía el contexto (incluido qué estado era).
- Command: los comandos del panel (
ComandoCancelarPedido) invocan operaciones cuya legalidad decide ahora State — eldeshacer()debe contar con laTransicionInvalidaException.
Errores comunes
- Transiciones desde fuera: un
pedido.setEstado(new EstadoEntregado())público destruye la máquina — cualquiera salta las reglas. La puerta (transitarA) debe ser inaccesible al exterior (paquete/privada). - Estados que tocan los datos del contexto directamente: los estados deciden, el contexto ejecuta; si
EstadoPagadomanipula campos dePedido, la encapsulación se rompe y los estados se acoplan a las tripas. - Olvidar el comportamiento por defecto: sin los
defaultque rechazan, cada estado debe implementar todo, y el caso no contemplado vuelve comonullo silencio en vez de excepción clara. - Explosión de estados por combinar dimensiones: si el pedido necesita "estado de pago" × "estado de cocina" independientes, no crees
PagadoYEnHorno: son dos máquinas paralelas, cada una con su State. - Duplicar el discriminante: mantener además un enum
estadoque hay que sincronizar a mano con el objeto estado — dos fuentes de verdad. Si necesitas el nombre para persistir o mostrar, derívalo del objeto (estado.nombre()), no lo guardes aparte.
Ejercicios
Ejercicio 1: nuevo estado sin tocar los viejos
Operaciones introduce EN_ESPERA_REPARTIDOR: tras EN_PREPARACION, si no hay repartidor, el pedido espera; cuando la CentralReparto asigna uno, pasa a EN_REPARTO; se puede cancelar con devolución total (el retraso es culpa nuestra). Implementa EstadoEnEsperaRepartidor e indica qué clases existentes cambian.
Ejercicio 2: dibujar antes de programar
Modela con un stateDiagram-v2 de mermaid la máquina de estados de una incidencia de soporte: ABIERTA → EN_CURSO (cuando un agente la toma) → RESUELTA (con solución) → CERRADA (el cliente confirma); desde RESUELTA puede volver a EN_CURSO (el cliente reclama); desde ABIERTA y EN_CURSO puede pasar a CERRADA directamente si el cliente la retira.
Ejercicio 3: ¿State, switch o Strategy?
Justifica la herramienta para cada caso: (a) el semáforo de disponibilidad de un repartidor: DISPONIBLE ↔ OCUPADO ↔ DESCANSO, sin comportamiento asociado más que el propio valor; (b) el documento de alta de un restaurante: BORRADOR / EN_REVISION / PUBLICADO / RETIRADO, con permisos de edición, validaciones y notificaciones distintas por estado, y estados nuevos previstos; (c) tres formas de calcular el reparto de la propina entre repartidores, elegible por país.
Soluciones
Solución 1:
public class EstadoEnEsperaRepartidor implements EstadoPedido {
@Override
public void cancelar(Pedido p) {
p.devolverTodo(); // culpa nuestra: sin penalización
p.transitarA(new EstadoCancelado());
}
@Override
public void marcarEnReparto(Pedido p) { // lo invoca la asignación de la central
p.transitarA(new EstadoEnReparto());
}
@Override
public String nombre() { return "EN_ESPERA_REPARTIDOR"; }
}Cambia solo EstadoEnPreparacion: su marcarEnReparto decide ahora entre transitar a EnReparto (hay repartidor) o a EnEsperaRepartidor (no lo hay) — es el estado origen de la flecha nueva, y las flechas son conocimiento de su origen. Pedido, la interfaz y los otros cuatro estados quedan intactos. Compara con el enfoque switch: habrían cambiado todos los métodos con switch.
Solución 2:
stateDiagram-v2
[*] --> ABIERTA
ABIERTA --> EN_CURSO : agente la toma
ABIERTA --> CERRADA : cliente la retira
EN_CURSO --> RESUELTA : solución propuesta
EN_CURSO --> CERRADA : cliente la retira
RESUELTA --> EN_CURSO : cliente reclama
RESUELTA --> CERRADA : cliente confirma
CERRADA --> [*]
(El valor del ejercicio está en las preguntas que el diagrama obliga a hacer: ¿puede reabrirse una CERRADA? Según lo enunciado, no — y ahora está escrito.)
Solución 3: (a) enum a secas (ni siquiera switch): tres valores sin comportamiento — aplicar State sería la sobreingeniería de la lección 01-06; (b) State: comportamiento sustancial por estado, transiciones con reglas y máquina en crecimiento — el caso de manual; (c) Strategy: son algoritmos alternativos elegidos por configuración (país), sin transiciones entre ellos — ningún cálculo de propina "transita" a otro. Que es, precisamente, la lección siguiente.
Conclusión
State reifica el ciclo de vida: cada etapa del pedido es ahora una clase que concentra su comportamiento y conoce sus salidas, Pedido delega sin un solo condicional, y las transiciones ilegales mueren en la puerta con nombre y apellidos — el hueco que Observer dejó abierto queda cerrado, y ambos patrones colaboran en transitarA: State autoriza, Observer difunde. Te llevas también el criterio honesto (switch para máquinas pequeñas y estables; State cuando el comportamiento pesa y la máquina evoluciona) y el hábito de oro: primero el diagrama de estados, después el código.
En el careo final del ejercicio asomó la frontera exacta con el siguiente patrón: cálculos alternativos que no transitan, elegidos desde fuera. PideYa tiene dos de libro esperando: los gastos de envío (por distancia, tarifa plana, gratis por promoción) y la asignación de repartidor que la CentralReparto dejó pendiente de extraer (más cercano, menos cargado, round-robin). Familias de algoritmos intercambiables tras una interfaz: nos vemos en Strategy.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
