🚦 El Patrón de diseño State
El patrón de diseño State permite que un objeto modifique su comportamiento cuando su estado interno cambia. Parece como si el objeto cambiara de clase.
¿Cómo gestionas un objeto que reacciona de forma totalmente distinta a los mismos eventos dependiendo de en qué estado se encuentre, sin llenar tu código de if ($estado == 'A') ... elseif ($estado == 'B')?
La respuesta es encapsular cada estado en una clase propia y delegar el comportamiento al objeto de estado actual.

📱 1. Tu teléfono móvil
Imagina tu móvil y el botón de “Home” (el contexto):
- Estado desbloqueado: Si pulsas Home, vas a la pantalla de inicio.
- Estado bloqueado: Si pulsas Home, se enciende la pantalla para pedirte la clave.
- Estado sin batería: Si pulsas Home, no pasa nada (o sale el icono de batería baja).
El botón es el mismo, pero la reacción depende del Estado. El móvil no tiene un if gigante comprobando en qué estado está cada vez que pulsas. Cada estado sabe qué debe hacer.
¿Qué problema resuelve el Patrón State?
🛠️ 2. Los roles del patrón
La clase principal. Mantiene una referencia al estado actual y delega en él todas las acciones. Expone un método cambiarEstado() para que los estados hagan transiciones.
Define los métodos comunes que todos los estados deben implementar. Es el contrato: play(), stop(), next()... todos los estados tienen que responder a esos métodos.
Cada clase implementa cómo responder a los eventos en ese estado. También pueden decidir cambiar el estado del contexto (transición).
El Contexto no sabe qué estado tiene. Solo sabe que tiene algo que implementa la interfaz. Cuando llamas a $contexto->play(), el Contexto delega en $this->estado->play($this) y el estado concreto hace su magia.
🧑💻 3. Ejemplo práctico en PHP: Reproductor de música
Vamos a implementar un reproductor con tres estados: Listo (parado), Reproduciendo y Bloqueado. Cada estado reacciona diferente a play() y stop().
<?php
// 1. Interfaz Estado
interface EstadoAudio
{
public function play(Reproductor $reproductor): void;
public function stop(Reproductor $reproductor): void;
public function bloquear(Reproductor $reproductor): void;
}
// 2. Contexto
class Reproductor
{
private EstadoAudio $estado;
private string $cancionActual = 'Ninguna';
public function __construct(EstadoAudio $estadoInicial)
{
$this->cambiarEstado($estadoInicial);
}
public function cambiarEstado(EstadoAudio $estado): void
{
$this->estado = $estado;
echo " 🔄 Estado → " . get_class($estado) . "\n";
}
public function getEstado(): EstadoAudio
{
return $this->estado;
}
public function setCancion(string $cancion): void
{
$this->cancionActual = $cancion;
}
public function getCancion(): string
{
return $this->cancionActual;
}
// El contexto delega TODO al estado actual
public function play(): void { $this->estado->play($this); }
public function stop(): void { $this->estado->stop($this); }
public function bloquear(): void { $this->estado->bloquear($this); }
}
// 3. Estados Concretos
class EstadoListo implements EstadoAudio
{
public function play(Reproductor $reproductor): void
{
$reproductor->setCancion('Estopa - Tu Calorro');
echo " ▶️ Reproduciendo: {$reproductor->getCancion()}\n";
$reproductor->cambiarEstado(new EstadoReproduciendo());
}
public function stop(Reproductor $reproductor): void
{
echo " ⏹️ Ya está parado, nene.\n";
}
public function bloquear(Reproductor $reproductor): void
{
echo " 🔒 Reproductor bloqueado.\n";
$reproductor->cambiarEstado(new EstadoBloqueado());
}
}
class EstadoReproduciendo implements EstadoAudio
{
public function play(Reproductor $reproductor): void
{
echo " ⏸️ Pausando: {$reproductor->getCancion()}\n";
$reproductor->cambiarEstado(new EstadoListo());
}
public function stop(Reproductor $reproductor): void
{
echo " ⏹️ Parando reproductor.\n";
$reproductor->setCancion('Ninguna');
$reproductor->cambiarEstado(new EstadoListo());
}
public function bloquear(Reproductor $reproductor): void
{
echo " 🔒 Bloqueado mientras reproduce. La música sigue.\n";
$reproductor->cambiarEstado(new EstadoBloqueado());
}
}
class EstadoBloqueado implements EstadoAudio
{
public function play(Reproductor $reproductor): void
{
echo " 🚫 Bloqueado. Desbloquéalo primero.\n";
}
public function stop(Reproductor $reproductor): void
{
echo " 🚫 Bloqueado. No se puede parar.\n";
}
public function bloquear(Reproductor $reproductor): void
{
echo " 🔓 Desbloqueando...\n";
$reproductor->cambiarEstado(new EstadoListo());
}
}
// 4. Uso
$reproductor = new Reproductor(new EstadoListo());
echo "--- Play ---\n";
$reproductor->play();
echo "\n--- Play de nuevo (pausa) ---\n";
$reproductor->play();
echo "\n--- Bloqueamos ---\n";
$reproductor->bloquear();
echo "\n--- Intentamos play bloqueado ---\n";
$reproductor->play();
echo "\n--- Desbloqueamos ---\n";
$reproductor->bloquear();
// Salida:
// 🔄 Estado → EstadoListo
// --- Play ---
// ▶️ Reproduciendo: Estopa - Tu Calorro
// 🔄 Estado → EstadoReproduciendo
//
// --- Play de nuevo (pausa) ---
// ⏸️ Pausando: Estopa - Tu Calorro
// 🔄 Estado → EstadoListo
//
// --- Bloqueamos ---
// 🔒 Reproductor bloqueado.
// 🔄 Estado → EstadoBloqueado
//
// --- Intentamos play bloqueado ---
// 🚫 Bloqueado. Desbloquéalo primero.
//
// --- Desbloqueamos ---
// 🔓 Desbloqueando...
// 🔄 Estado → EstadoListo
Aquí hay varios detalles a tener en cuenta:
- El
Reproductorno tiene ni un soloif. Toda la lógica está en los estados. - Cada estado decide a qué estado transicionar.
EstadoListosabe que al hacerplay()pasa aEstadoReproduciendo. bloquear()enEstadoBloqueadofunciona como toggle: si ya estás bloqueado, desbloquea. Cada estado decide qué hace con cada acción.
En el Patrón State, ¿quién decide las transiciones entre estados?
🆚 4. El horror de no usar State
Para que entiendas lo que te ahorras, así quedaría el mismo reproductor con condicionales:
// ❌ El infierno de los if/elseif
class ReproductorSinState
{
private string $estado = 'listo';
public function play(): void
{
if ($this->estado === 'listo') {
echo "Reproduciendo...\n";
$this->estado = 'reproduciendo';
} elseif ($this->estado === 'reproduciendo') {
echo "Pausando...\n";
$this->estado = 'listo';
} elseif ($this->estado === 'bloqueado') {
echo "Bloqueado.\n";
}
}
public function stop(): void
{
if ($this->estado === 'listo') {
echo "Ya está parado.\n";
} elseif ($this->estado === 'reproduciendo') {
echo "Parando...\n";
$this->estado = 'listo';
} elseif ($this->estado === 'bloqueado') {
echo "Bloqueado.\n";
}
}
public function bloquear(): void
{
if ($this->estado === 'bloqueado') {
echo "Desbloqueando...\n";
$this->estado = 'listo';
} else {
echo "Bloqueando...\n";
$this->estado = 'bloqueado';
}
}
}
Solo con 3 estados y 3 métodos ya tienes 9 ramas condicionales. Imagina con 6 estados y 5 métodos: serían 30 ramas. Y cada vez que añades un estado nuevo, tienes que tocar todos los métodos. Con State, solo creas una clase nueva.
Si el Contexto usa instanceof para decidir transiciones, ¿qué indica?
🧑💻 5. Ejemplo avanzado: Sistema de pedidos
Un caso más real. Un pedido pasa por fases: Pendiente → Pagado → Enviado → Entregado. Cada estado tiene reglas sobre qué se puede hacer y qué no.
<?php
interface EstadoPedido
{
public function pagar(Pedido $pedido): void;
public function enviar(Pedido $pedido): void;
public function entregar(Pedido $pedido): void;
public function cancelar(Pedido $pedido): void;
public function getNombre(): string;
}
class Pedido
{
private EstadoPedido $estado;
private string $id;
public function __construct(string $id)
{
$this->id = $id;
$this->cambiarEstado(new EstadoPendiente());
}
public function cambiarEstado(EstadoPedido $estado): void
{
$this->estado = $estado;
echo " 📦 Pedido #{$this->id} → {$estado->getNombre()}\n";
}
public function getId(): string { return $this->id; }
public function getEstado(): EstadoPedido { return $this->estado; }
public function pagar(): void { $this->estado->pagar($this); }
public function enviar(): void { $this->estado->enviar($this); }
public function entregar(): void { $this->estado->entregar($this); }
public function cancelar(): void { $this->estado->cancelar($this); }
}
class EstadoPendiente implements EstadoPedido
{
public function getNombre(): string { return 'PENDIENTE'; }
public function pagar(Pedido $pedido): void
{
echo " 💳 Pago recibido.\n";
$pedido->cambiarEstado(new EstadoPagado());
}
public function enviar(Pedido $pedido): void
{
echo " ❌ No puedes enviar sin pagar.\n";
}
public function entregar(Pedido $pedido): void
{
echo " ❌ No puedes entregar sin enviar.\n";
}
public function cancelar(Pedido $pedido): void
{
echo " 🗑️ Pedido cancelado.\n";
$pedido->cambiarEstado(new EstadoCancelado());
}
}
class EstadoPagado implements EstadoPedido
{
public function getNombre(): string { return 'PAGADO'; }
public function pagar(Pedido $pedido): void
{
echo " ⚠️ Ya está pagado.\n";
}
public function enviar(Pedido $pedido): void
{
echo " 🚚 Pedido enviado a reparto.\n";
$pedido->cambiarEstado(new EstadoEnviado());
}
public function entregar(Pedido $pedido): void
{
echo " ❌ No puedes entregar sin enviar.\n";
}
public function cancelar(Pedido $pedido): void
{
echo " 🗑️ Cancelado. Se procede al reembolso.\n";
$pedido->cambiarEstado(new EstadoCancelado());
}
}
class EstadoEnviado implements EstadoPedido
{
public function getNombre(): string { return 'ENVIADO'; }
public function pagar(Pedido $pedido): void
{
echo " ⚠️ Ya está pagado.\n";
}
public function enviar(Pedido $pedido): void
{
echo " ⚠️ Ya está en camino.\n";
}
public function entregar(Pedido $pedido): void
{
echo " ✅ Pedido entregado al cliente.\n";
$pedido->cambiarEstado(new EstadoEntregado());
}
public function cancelar(Pedido $pedido): void
{
echo " ❌ No se puede cancelar: ya está en camino.\n";
}
}
class EstadoEntregado implements EstadoPedido
{
public function getNombre(): string { return 'ENTREGADO'; }
public function pagar(Pedido $pedido): void
{
echo " ⚠️ Pedido ya completado.\n";
}
public function enviar(Pedido $pedido): void
{
echo " ⚠️ Pedido ya completado.\n";
}
public function entregar(Pedido $pedido): void
{
echo " ⚠️ Ya está entregado.\n";
}
public function cancelar(Pedido $pedido): void
{
echo " ❌ No se puede cancelar un pedido entregado.\n";
}
}
class EstadoCancelado implements EstadoPedido
{
public function getNombre(): string { return 'CANCELADO'; }
public function pagar(Pedido $pedido): void
{
echo " ❌ Pedido cancelado, no se puede pagar.\n";
}
public function enviar(Pedido $pedido): void
{
echo " ❌ Pedido cancelado, no se puede enviar.\n";
}
public function entregar(Pedido $pedido): void
{
echo " ❌ Pedido cancelado, no se puede entregar.\n";
}
public function cancelar(Pedido $pedido): void
{
echo " ⚠️ Ya está cancelado.\n";
}
}
// Uso
$pedido = new Pedido('1042');
echo "--- Intentamos enviar sin pagar ---\n";
$pedido->enviar();
echo "\n--- Pagamos ---\n";
$pedido->pagar();
echo "\n--- Enviamos ---\n";
$pedido->enviar();
echo "\n--- Intentamos cancelar en camino ---\n";
$pedido->cancelar();
echo "\n--- Entregamos ---\n";
$pedido->entregar();
// Salida:
// 📦 Pedido #1042 → PENDIENTE
// --- Intentamos enviar sin pagar ---
// ❌ No puedes enviar sin pagar.
//
// --- Pagamos ---
// 💳 Pago recibido.
// 📦 Pedido #1042 → PAGADO
//
// --- Enviamos ---
// 🚚 Pedido enviado a reparto.
// 📦 Pedido #1042 → ENVIADO
//
// --- Intentamos cancelar en camino ---
// ❌ No se puede cancelar: ya está en camino.
//
// --- Entregamos ---
// ✅ Pedido entregado al cliente.
// 📦 Pedido #1042 → ENTREGADO
Esto es una máquina de estados en toda regla. Cada estado controla sus transiciones válidas: no puedes enviar sin
pagar, no puedes cancelar lo que ya está en camino. Y todo sin un solo if ($estado === ...).
🌍 6. Casos de uso en el mundo real
Pending → Paid → Shipped → Delivered → Returned. Cada fase tiene reglas propias sobre qué acciones están permitidas. State evita la montaña de if/else.
Un documento en Borrador solo lo ve el autor. En Revisión lo ven los editores. Publicado lo ve todo el mundo. Cada estado cambia permisos, visibilidad y acciones.
Idle → Running → Jumping → Attacking → Dead. Cada estado cambia la animación, las físicas y los controles. Unity y Unreal usan máquinas de estado para esto.
El protocolo TCP es literalmente una máquina de estados: LISTEN → SYN_SENT → ESTABLISHED → FIN_WAIT → CLOSED. Cada estado define qué paquetes se pueden enviar y recibir.
¿Cuál de estos es un caso de uso real del Patrón State?
🔄 7. Comparativa con otros patrones
| Patrón | Propósito | Diferencia clave |
|---|---|---|
| State | Cambiar comportamiento según estado interno | Los estados se conocen entre sí y gestionan las transiciones. El cambio es automático e interno |
| Strategy | Intercambiar algoritmos | Las estrategias no se conocen. El cliente elige cuál usar. No hay transiciones automáticas |
| Command | Encapsular una acción como objeto | Command encapsula una operación. State encapsula un conjunto de comportamientos que dependen del estado |
| Chain of Responsibility | Pasar una petición por una cadena | CoR pasa la petición secuencialmente por handlers. State cambia quién procesa según el estado actual |
| Observer | Notificar a suscriptores de cambios | Observer puede combinarse con State: cuando el estado cambia, notificar a observadores del cambio |
La confusión más frecuente: State vs Strategy. Estructuralmente son casi idénticos (ambos usan composición + interfaz). La diferencia es la intención:
- State: los estados se conocen entre sí y hacen transiciones automáticas. El objeto va cambiando de estado a lo largo de su vida. El cliente no elige el estado.
- Strategy: las estrategias son independientes. El cliente elige cuál usar y puede cambiarla en cualquier momento. No hay transiciones entre estrategias.
En resumen, si el cambio es interno y automático, es State. Si el cambio es externo y manual, es Strategy.
¿Cuál es la diferencia clave entre State y Strategy?
⚖️ 8. Relación con SOLID
Cada estado tiene una sola razón para cambiar: la lógica de ese estado. El Contexto solo se encarga de delegar. Nadie hace el trabajo de otro.
¿Nuevo estado? Creas una clase nueva que implementa la interfaz. No tocas ni el Contexto ni los estados existentes. Extensión pura.
Todos los estados implementan la misma interfaz. El Contexto puede usar cualquiera sin romperse. Da igual si es EstadoListo o EstadoBloqueado.
El Contexto depende de la abstracción EstadoAudio, no de ningún estado concreto. Inversión de dependencias al pie de la letra.
¿Qué principio SOLID cumple el Patrón State al permitir añadir estados sin modificar el Contexto?
✅ 9. Ventajas y desventajas
- Adiós a los if/elseif: Cada estado encapsula su lógica. Cero condicionales en el Contexto.
- OCP: Nuevo estado = nueva clase. Sin tocar nada existente.
- Transiciones explícitas: Las reglas de transición están en los propios estados, no dispersas por el código.
- Fácil de testear: Puedes testear cada estado por separado, sin montar el Contexto completo.
- Código legible: Abrir
EstadoPagado.phpy ver exactamente qué puede y qué no puede hacer un pedido pagado.
- Explosión de clases: 5 estados = 5 clases + interfaz + Contexto = 7 clases. Si el flujo es simple, puede ser overkill.
- Acoplamiento entre estados: Los estados se conocen entre sí (para hacer transiciones). Si añades uno nuevo, puede que tengas que modificar otros.
- Interfaz creciente: Si añades un método a la interfaz, tienes que implementarlo en TODOS los estados, incluso los que no lo necesitan.
- Visión global difícil: Con los if/else al menos ves todas las transiciones en un vistazo. Con State tienes que saltar entre clases.
⚠️ 10. Errores comunes
1. Meter lógica de transición en el Contexto
Si el Contexto decide cuándo cambiar de estado, pierdes todo el beneficio del patrón:
// ❌ MAL: el Contexto decide las transiciones
class Reproductor
{
public function play(): void
{
if ($this->estado instanceof EstadoListo) {
$this->cambiarEstado(new EstadoReproduciendo()); // 💀
} elseif ($this->estado instanceof EstadoReproduciendo) {
$this->cambiarEstado(new EstadoListo());
}
}
}
// ✅ BIEN: el estado decide su propia transición
class EstadoListo implements EstadoAudio
{
public function play(Reproductor $reproductor): void
{
echo "Reproduciendo...\n";
$reproductor->cambiarEstado(new EstadoReproduciendo());
}
}
Si el Contexto tiene instanceof, algo huele mal. El Contexto solo debe delegar.
2. Estados que crean nuevas instancias innecesariamente
Cada transición crea un new EstadoX(). Si los estados no tienen datos internos, es desperdicio. Puedes reutilizarlos:
// ❌ MAL: crea instancia nueva cada vez
public function play(Reproductor $reproductor): void
{
$reproductor->cambiarEstado(new EstadoReproduciendo()); // Nuevo objeto cada vez
}
// ✅ BIEN: singleton o estados prealojados
class EstadoReproduciendo implements EstadoAudio
{
private static ?self $instancia = null;
public static function getInstance(): self
{
return self::$instancia ??= new self();
}
// ...
}
// O más simple: el Contexto mantiene los estados
class Reproductor
{
private EstadoAudio $listo;
private EstadoAudio $reproduciendo;
private EstadoAudio $bloqueado;
public function __construct()
{
$this->listo = new EstadoListo();
$this->reproduciendo = new EstadoReproduciendo();
$this->bloqueado = new EstadoBloqueado();
$this->cambiarEstado($this->listo);
}
public function getEstadoListo(): EstadoAudio { return $this->listo; }
public function getEstadoReproduciendo(): EstadoAudio { return $this->reproduciendo; }
// ...
}
Esto solo merece la pena si hay muchas transiciones frecuentes. Si los estados son ligeros, new está bien.
3. Interfaz demasiado grande
Si tu interfaz tiene 10 métodos y la mayoría de estados solo necesitan 2, estás violando ISP. Los estados acaban con métodos vacíos o que lanzan excepciones:
// ❌ MAL: 10 métodos, la mayoría no hacen nada
class EstadoBloqueado implements EstadoAudio
{
public function play(Reproductor $r): void { /* no hace nada */ }
public function stop(Reproductor $r): void { /* no hace nada */ }
public function next(Reproductor $r): void { /* no hace nada */ }
public function prev(Reproductor $r): void { /* no hace nada */ }
public function shuffle(Reproductor $r): void { /* no hace nada */ }
public function repeat(Reproductor $r): void { /* no hace nada */ }
// ... los 10 hacen lo mismo: nada
}
// ✅ BIEN: clase base abstracta con implementación por defecto
abstract class EstadoBase implements EstadoAudio
{
public function play(Reproductor $r): void
{
echo "Acción no disponible en este estado.\n";
}
public function stop(Reproductor $r): void
{
echo "Acción no disponible en este estado.\n";
}
// ... cada estado solo sobreescribe lo que necesita
}
class EstadoBloqueado extends EstadoBase
{
// Solo sobreescribe bloquear() para desbloquear
public function bloquear(Reproductor $r): void
{
$r->cambiarEstado(new EstadoListo());
}
}
Si añades un nuevo método a la interfaz de Estado, ¿qué problema puede surgir?
4. Olvidar validar transiciones imposibles
Si cualquier estado puede transicionar a cualquier otro, tu máquina de estados no tiene sentido. Cada estado debe rechazar transiciones inválidas:
// ❌ MAL: el estado Entregado permite volver a Pendiente
class EstadoEntregado implements EstadoPedido
{
public function cancelar(Pedido $pedido): void
{
$pedido->cambiarEstado(new EstadoPendiente()); // 💀 ¡Un pedido entregado vuelve a pendiente!
}
}
// ✅ BIEN: rechaza la transición
class EstadoEntregado implements EstadoPedido
{
public function cancelar(Pedido $pedido): void
{
echo "No se puede cancelar un pedido ya entregado.\n";
// No cambia de estado
}
}
🤔 11. ¿Cuándo usarlo y cuándo no?
- El objeto tiene varios estados con comportamientos muy diferentes
- Tienes condicionales grandes basados en el estado del objeto
- Las transiciones entre estados tienen reglas claras
- Necesitas que sea fácil añadir estados nuevos sin tocar los existentes
- El flujo del objeto es una máquina de estados clara (workflows, procesos)
- Solo tienes 2 estados con poca lógica (un simple boolean basta)
- Los estados no cambian el comportamiento, solo un valor (no necesitas polimorfismo)
- El cliente elige el comportamiento (eso es Strategy, no State)
- La máquina de estados es tan simple que un switch/case es más claro
Si tienes más de 3 bloques
if ($estado === ...)en el mismo método, es hora de plantearte State. Si solo tienes 2, probablemente no merezca la pena.
¿Cuándo NO deberías usar el Patrón State?
💡 12. Conclusión
El Patrón State es perfecto para objetos que cambian de personalidad a lo largo de su vida. Un pedido que pasa por fases, un reproductor con diferentes modos, un personaje de juego con estados… todo eso son máquinas de estado, y este patrón las modela de forma limpia.
Lo que ganas: adiós a los if/elseif kilométricos, cada estado en su propia clase con su lógica, transiciones explícitas y la posibilidad de añadir estados nuevos sin tocar nada existente.
Lo que te cuesta son más clases y los estados se conocen entre sí (cierto acoplamiento). Si tu flujo es simple (encendido/apagado), un boolean hace el trabajo sin tanto ceremonia.
EA, ¡saluditos y nos vemos en los bares! 🍻