🚨 ¡Nueva review! ✨ Mi ratón favorito para programar: el Logitech MX Master 3S . ¡Échale un ojo! 👀

El Patrón de diseño State

Cuando tu objeto tiene cambios de humor.

Escrito por domin el 25 de noviembre de 2025 · Actualizado el 15 de febrero de 2026

🚦 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.

Diagrama del Patrón State.

📱 1. Tu teléfono móvil

Imagina tu móvil y el botón de “Home” (el contexto):

  1. Estado desbloqueado: Si pulsas Home, vas a la pantalla de inicio.
  2. Estado bloqueado: Si pulsas Home, se enciende la pantalla para pedirte la clave.
  3. 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

Contexto

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.

Interfaz Estado

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.

Estados Concretos

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:

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: PendientePagadoEnviadoEntregado. 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

Workflows de pedidos (E-commerce)

Pending → Paid → Shipped → Delivered → Returned. Cada fase tiene reglas propias sobre qué acciones están permitidas. State evita la montaña de if/else.

Editores de texto (Draft/Published)

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.

Personajes de videojuegos

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.

Conexiones de red (TCP)

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ónPropósitoDiferencia clave
StateCambiar comportamiento según estado internoLos estados se conocen entre sí y gestionan las transiciones. El cambio es automático e interno
StrategyIntercambiar algoritmosLas estrategias no se conocen. El cliente elige cuál usar. No hay transiciones automáticas
CommandEncapsular una acción como objetoCommand encapsula una operación. State encapsula un conjunto de comportamientos que dependen del estado
Chain of ResponsibilityPasar una petición por una cadenaCoR pasa la petición secuencialmente por handlers. State cambia quién procesa según el estado actual
ObserverNotificar a suscriptores de cambiosObserver 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:

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

SRP (Single Responsibility)

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.

OCP (Open/Closed)

¿Nuevo estado? Creas una clase nueva que implementa la interfaz. No tocas ni el Contexto ni los estados existentes. Extensión pura.

LSP (Liskov Substitution)

Todos los estados implementan la misma interfaz. El Contexto puede usar cualquiera sin romperse. Da igual si es EstadoListo o EstadoBloqueado.

DIP (Dependency Inversion)

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

Ventajas
  • 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.php y ver exactamente qué puede y qué no puede hacer un pedido pagado.
Desventajas
  • 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?

Úsalo cuando...
  • 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)
No lo uses cuando...
  • 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! 🍻