♟️ El patrón de diseño Strategy
El patrón Strategy es uno de los patrones de comportamiento más elegantes y útiles. Su misión principal es manejar la variación de algoritmos de forma limpia.

🤯 1. ¿Para qué queremos a Strategy?
Imagina que tienes una clase encargada de una tarea, pero la forma de hacer esa tarea cambia según una condición externa. La clásica cagada es crear un método gigante lleno de if/else o switch para elegir qué camino tomar.
// ❌ Código difícil de mantener y acoplado
class CarritoCompras
{
public function checkout(string $metodoPago): void
{
if ($metodoPago === 'paypal') {
// Lógica de PayPal
} elseif ($metodoPago === 'tarjeta') {
// Lógica de Tarjeta de Crédito
} elseif ($metodoPago === 'bizum') {
// Lógica de Bizum
}
// Si mañana quieres añadir "Apple Pay", tienes que modificar esta clase.
}
}
Esto se peta el principio Open/Close de SOLID:
una clase debería estar abierta a extensión (poder añadir nuevos pagos) pero cerrada a modificación (no tener
que tocar el método checkout cada vez).
El patrón Strategy arregla esto delegando la lógica a una clase separada.
Lo ideal sería definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. El objeto principal ya no sabe cómo hacer la tarea, simplemente se la pasa al objeto estrategia que tenga asignado.
¿Qué problema resuelve el Patrón Strategy?
🌎 2. Métodos de pago
Vamos a ver cómo funciona el proceso de pago en una tienda online:
- El sujeto (el contexto): Es el Carrito de Compras. Su trabajo es saber qué productos llevas y el total. No le importa cómo pagas, solo que pagues.
- La interfaz (la estrategia): Es el Contrato de Pago. Define una norma:
ejecutarPago(monto). - Las implementaciones (estrategias concretas): Son PayPal, Tarjeta o Bizum. Cada uno cumple el contrato a su manera.
La magia está en que el Carrito no conoce a PayPal ni a Bizum, solo conoce la interfaz genérica. Esto desacopla totalmente el código.
🛠️ 3. Los roles del patrón
Define el contrato común que todas las estrategias concretas deben cumplir. Un solo método como pagar(), ordenar() o calcular().
Implementan la interfaz con la lógica real del algoritmo. Cada una a su manera: PayPal conecta con su API, Tarjeta con la pasarela bancaria, Bizum con su servicio.
Mantiene una referencia a una estrategia. Delega la ejecución sin saber qué estrategia tiene. Permite cambiarla en runtime.
Imagina un coche (el contexto) con un método acelerar(). El coche puede tener varios motores intercambiables (estrategias): eléctrico, diésel o de reacción. El coche simplemente llama a motor.acelerar(), pero el comportamiento real depende del motor que le hayas instalado.
¿Cuál es el rol principal del Contexto en el patrón Strategy?
🧑💻 4. Ejemplo práctico en PHP: Carrito de compras
Vamos a implementar el ejemplo del Carrito de Compras con tres métodos de pago intercambiables.
<?php
// 1. La Interfaz (Estrategia)
interface EstrategiaPago
{
public function pagar(float $monto): bool;
public function getNombre(): string;
}
// 2. Estrategias Concretas
class PagoPayPal implements EstrategiaPago
{
private string $email;
public function __construct(string $email)
{
$this->email = $email;
}
public function pagar(float $monto): bool
{
echo " 💜 PayPal: Cobrando {$monto}€ a {$this->email}\n";
// Aquí iría la conexión real con la API de PayPal
return true;
}
public function getNombre(): string { return 'PayPal'; }
}
class PagoTarjeta implements EstrategiaPago
{
private string $numero;
private string $cvv;
public function __construct(string $numero, string $cvv)
{
$this->numero = $numero;
$this->cvv = $cvv;
}
public function pagar(float $monto): bool
{
$ultimos4 = substr($this->numero, -4);
echo " 💳 Tarjeta: Cobrando {$monto}€ a ****{$ultimos4}\n";
// Conexión con pasarela bancaria
return true;
}
public function getNombre(): string { return 'Tarjeta'; }
}
class PagoBizum implements EstrategiaPago
{
private string $telefono;
public function __construct(string $telefono)
{
$this->telefono = $telefono;
}
public function pagar(float $monto): bool
{
echo " 📱 Bizum: Enviando cobro de {$monto}€ al {$this->telefono}\n";
return true;
}
public function getNombre(): string { return 'Bizum'; }
}
// 3. El Contexto
class CarritoCompras
{
private ?EstrategiaPago $estrategiaPago = null;
private array $productos = [];
public function agregarProducto(string $nombre, float $precio): void
{
$this->productos[] = ['nombre' => $nombre, 'precio' => $precio];
}
public function getTotal(): float
{
return array_sum(array_column($this->productos, 'precio'));
}
public function setEstrategiaPago(EstrategiaPago $estrategia): void
{
$this->estrategiaPago = $estrategia;
echo " 🔄 Método de pago: {$estrategia->getNombre()}\n";
}
public function checkout(): bool
{
if ($this->estrategiaPago === null) {
echo " ❌ Elige un método de pago primero.\n";
return false;
}
$total = $this->getTotal();
echo " 🛒 Procesando pedido ({$total}€)...\n";
// El contexto delega al strategy. No sabe NI QUIERE saber
// si es PayPal, tarjeta o señales de humo.
return $this->estrategiaPago->pagar($total);
}
}
// 4. Uso
$carrito = new CarritoCompras();
$carrito->agregarProducto('Teclado mecánico', 89.99);
$carrito->agregarProducto('Ratón gaming', 49.99);
echo "--- Pago con PayPal ---\n";
$carrito->setEstrategiaPago(new PagoPayPal('domin@mail.com'));
$carrito->checkout();
echo "\n--- El cliente cambia de opinión: Bizum ---\n";
$carrito->setEstrategiaPago(new PagoBizum('612345678'));
$carrito->checkout();
echo "\n--- Ahora con tarjeta ---\n";
$carrito->setEstrategiaPago(new PagoTarjeta('4111111111111234', '123'));
$carrito->checkout();
// Salida:
// --- Pago con PayPal ---
// 🔄 Método de pago: PayPal
// 🛒 Procesando pedido (139.98€)...
// 💜 PayPal: Cobrando 139.98€ a domin@mail.com
//
// --- El cliente cambia de opinión: Bizum ---
// 🔄 Método de pago: Bizum
// 🛒 Procesando pedido (139.98€)...
// 📱 Bizum: Enviando cobro de 139.98€ al 612345678
//
// --- Ahora con tarjeta ---
// 🔄 Método de pago: Tarjeta
// 🛒 Procesando pedido (139.98€)...
// 💳 Tarjeta: Cobrando 139.98€ a ****1234
El CarritoCompras no tiene ni un solo if para decidir cómo pagar. No conoce PayPal, ni Bizum, ni Tarjeta. Solo
conoce la interfaz EstrategiaPago. Si mañana quieres añadir Apple Pay, creas una clase nueva y listo. Cero cambios
en el carrito.
🧑💻 5. Ejemplo avanzado: Sistema de envíos con cálculo de coste
Otro caso muy habitual, el coste de envío depende del método elegido. Cada estrategia calcula el precio de forma diferente.
<?php
interface EstrategiaEnvio
{
public function calcularCoste(float $peso, float $distancia): float;
public function getNombre(): string;
public function getTiempoEstimado(): string;
}
class EnvioEstandar implements EstrategiaEnvio
{
public function calcularCoste(float $peso, float $distancia): float
{
// Tarifa base + coste por km + coste por kg
return 2.50 + ($distancia * 0.01) + ($peso * 0.50);
}
public function getNombre(): string { return 'Estándar (3-5 días)'; }
public function getTiempoEstimado(): string { return '3-5 días laborables'; }
}
class EnvioExpress implements EstrategiaEnvio
{
public function calcularCoste(float $peso, float $distancia): float
{
// El doble que el estándar + recargo fijo
return 5.00 + ($distancia * 0.02) + ($peso * 1.00);
}
public function getNombre(): string { return 'Express (24h)'; }
public function getTiempoEstimado(): string { return '24 horas'; }
}
class EnvioGratuito implements EstrategiaEnvio
{
public function calcularCoste(float $peso, float $distancia): float
{
return 0.00; // Gratis, pero lento
}
public function getNombre(): string { return 'Gratuito (7-10 días)'; }
public function getTiempoEstimado(): string { return '7-10 días laborables'; }
}
class RecogidaTienda implements EstrategiaEnvio
{
public function calcularCoste(float $peso, float $distancia): float
{
return 0.00; // Sin envío
}
public function getNombre(): string { return 'Recogida en tienda'; }
public function getTiempoEstimado(): string { return 'Disponible en 2h'; }
}
// Contexto
class CalculadoraEnvio
{
private EstrategiaEnvio $estrategia;
public function __construct(EstrategiaEnvio $estrategia)
{
$this->estrategia = $estrategia;
}
public function setEstrategia(EstrategiaEnvio $estrategia): void
{
$this->estrategia = $estrategia;
}
public function mostrarResumen(float $peso, float $distancia): void
{
$coste = $this->estrategia->calcularCoste($peso, $distancia);
echo " 📦 Método: {$this->estrategia->getNombre()}\n";
echo " 💰 Coste: {$coste}€\n";
echo " 🕐 Entrega: {$this->estrategia->getTiempoEstimado()}\n";
}
}
// Uso: comparar opciones de envío
$peso = 2.5; // kg
$distancia = 300; // km
$calc = new CalculadoraEnvio(new EnvioEstandar());
echo "--- Opción 1: Estándar ---\n";
$calc->mostrarResumen($peso, $distancia);
echo "\n--- Opción 2: Express ---\n";
$calc->setEstrategia(new EnvioExpress());
$calc->mostrarResumen($peso, $distancia);
echo "\n--- Opción 3: Gratuito ---\n";
$calc->setEstrategia(new EnvioGratuito());
$calc->mostrarResumen($peso, $distancia);
// Salida:
// --- Opción 1: Estándar ---
// 📦 Método: Estándar (3-5 días)
// 💰 Coste: 6.75€
// 🕐 Entrega: 3-5 días laborables
//
// --- Opción 2: Express ---
// 📦 Método: Express (24h)
// 💰 Coste: 13.5€
// 🕐 Entrega: 24 horas
//
// --- Opción 3: Gratuito ---
// 📦 Método: Gratuito (7-10 días)
// 💰 Coste: 0€
// 🕐 Entrega: 7-10 días laborables
Aquí cada estrategia tiene su propia fórmula de cálculo. El contexto (CalculadoraEnvio) le da igual cómo se calcula,
simplemente pide el coste y lo muestra. Si mañana añades envío con drones, creas EnvioDron y enchufas. Sin tocar nada.
🌍 6. Casos de uso en el mundo real
Cualquier e-commerce que soporte múltiples métodos de pago usa Strategy. El checkout delega a la pasarela elegida sin conocerla directamente.
usort($array, $comparator) en PHP o Array.sort(compareFn) en JS. El array no sabe cómo ordenar, tú le pasas la estrategia de comparación.
Exportar datos en JSON, XML, CSV o YAML. El sistema de exportación usa Strategy para delegar el formato al serializador elegido.
Validar un email, un teléfono, un IBAN... cada tipo de campo tiene su estrategia de validación. Laravel Validation Rules funcionan así internamente.
¿Cuál es un ejemplo real del patrón Strategy?
🔄 7. Comparativa con otros patrones
| Patrón | Propósito | Diferencia clave |
|---|---|---|
| Strategy | Intercambiar algoritmos en runtime | El cliente elige la estrategia. Las estrategias no se conocen entre sí |
| State | Cambiar comportamiento según estado interno | Los estados se conocen y hacen transiciones automáticas. El cliente no elige el estado |
| Template Method | Definir el esqueleto de un algoritmo | Template Method usa herencia para variar pasos. Strategy usa composición para intercambiar el algoritmo completo |
| Command | Encapsular una acción como objeto | Command encapsula una operación (con undo, queue). Strategy encapsula un algoritmo intercambiable |
| Decorator | Añadir funcionalidad envolviendo | Decorator apila capas de funcionalidad. Strategy reemplaza el algoritmo entero |
La confusión más frecuente: Strategy vs State. Estructuralmente son casi idénticos (composición + interfaz). La diferencia es la intención:
- Strategy: el cliente elige la estrategia externamente. Las estrategias son independientes, no saben que existen las demás. No hay transiciones.
- State: el cambio es interno y automático. Los estados se conocen entre sí y gestionan sus propias transiciones.
Si tú eliges el algoritmo (pago con PayPal o Bizum), es Strategy. Si el objeto cambia solo según su estado (pedido pendiente → pagado → enviado), es State.
¿Cuál es la diferencia principal entre Strategy y State?
⚖️ 8. Relación con SOLID
Cada estrategia tiene una sola responsabilidad: su algoritmo. El contexto solo coordina. Nada de clases con 5 algoritmos metidos dentro.
¿Nuevo método de pago? Creas PagoApplePay y lo inyectas. Cero cambios en el CarritoCompras. Abierto a extensión, cerrado a modificación.
El contexto depende de la abstracción EstrategiaPago, no de PagoPayPal ni de PagoBizum. Inversión de dependencias perfecta.
Todas las estrategias implementan la misma interfaz. Puedes sustituir una por otra sin romper nada. PayPal y Bizum son intercambiables para el contexto.
¿Qué principio SOLID cumple Strategy al añadir algoritmos sin modificar el contexto?
✅ 9. Ventajas y desventajas
- Adiós a los if/elseif: Cada algoritmo en su clase. Cero condicionales en el contexto.
- OCP: Nueva estrategia = nueva clase. Sin tocar nada existente.
- Intercambio en runtime: Puedes cambiar el comportamiento mientras el programa corre.
- Testeable: Cada estrategia se testea por separado, independiente del contexto.
- Composición sobre herencia: Usas inyección de dependencias en vez de crear subclases para cada variante.
- Más clases: 4 métodos de pago = 4 clases + interfaz + contexto. Si solo tienes 2 variantes simples, puede ser overkill.
- El cliente debe conocer las estrategias: Alguien tiene que decidir cuál usar. La complejidad de selección se mueve al cliente.
- Overhead en estrategias simples: Si el "algoritmo" es una línea de código, crear una clase entera para eso es matar moscas a cañonazos.
- Comunicación contexto-estrategia: Si la estrategia necesita muchos datos del contexto, acabas pasando demasiados parámetros o exponiendo internos.
⚠️ 10. Errores comunes
1. Seleccionar la estrategia con if/else (anula el patrón)
Si creas las estrategias pero luego usas un if para decidir cuál instanciar, has movido el problema en vez de resolverlo:
// ❌ MAL: el if sigue ahí, solo cambió de sitio
function getEstrategia(string $tipo): EstrategiaPago
{
if ($tipo === 'paypal') return new PagoPayPal('...');
if ($tipo === 'tarjeta') return new PagoTarjeta('...', '...');
if ($tipo === 'bizum') return new PagoBizum('...');
throw new \Exception("Tipo desconocido");
}
// ✅ BIEN: usa un registry/map o inyección de dependencias
class RegistroPagos
{
/** @var array<string, EstrategiaPago> */
private array $estrategias = [];
public function registrar(string $nombre, EstrategiaPago $estrategia): void
{
$this->estrategias[$nombre] = $estrategia;
}
public function obtener(string $nombre): EstrategiaPago
{
return $this->estrategias[$nombre]
?? throw new \InvalidArgumentException("Pago '$nombre' no registrado");
}
}
// Registro al arrancar la app (o en el container de DI)
$registro = new RegistroPagos();
$registro->registrar('paypal', new PagoPayPal('domin@mail.com'));
$registro->registrar('bizum', new PagoBizum('612345678'));
// Uso: sin if/else
$carrito->setEstrategiaPago($registro->obtener('paypal'));
¿Qué error se comete si seleccionas la estrategia con un if/else?
2. Estrategia que necesita demasiado contexto
Si tu estrategia necesita 10 datos del contexto, algo falla. O la interfaz es demasiado genérica o la estrategia tiene demasiada responsabilidad:
// ❌ MAL: la estrategia necesita todo
interface EstrategiaPago
{
public function pagar(
float $monto,
string $moneda,
string $usuario,
array $productos,
string $direccion,
string $codigoDescuento
): bool;
}
// ✅ BIEN: pasa un DTO con lo necesario
class DatosPago
{
public function __construct(
public readonly float $monto,
public readonly string $moneda,
public readonly string $referencia,
) {}
}
interface EstrategiaPago
{
public function pagar(DatosPago $datos): bool;
}
3. Estrategias con estado compartido
Si varias invocaciones de la misma estrategia comparten estado interno, puedes tener bugs sutiles:
// ❌ MAL: la estrategia acumula estado
class PagoPayPal implements EstrategiaPago
{
private int $intentos = 0;
public function pagar(float $monto): bool
{
$this->intentos++; // Se acumula entre llamadas 💀
if ($this->intentos > 3) {
echo "Demasiados intentos.\n";
return false;
}
// ...
}
}
// ✅ BIEN: sin estado o con estado local al método
class PagoPayPal implements EstrategiaPago
{
public function pagar(float $monto): bool
{
// Reintentos se gestionan fuera o con un wrapper
echo "Procesando pago PayPal...\n";
return true;
}
}
4. Sobreingeniería: Strategy para una sola variante
Si solo tienes un algoritmo y no prevés que cambie, no necesitas Strategy. Un método directo es más claro:
// ❌ MAL: Strategy para un solo caso
interface EstrategiaSaludo
{
public function saludar(): string;
}
class SaludoHola implements EstrategiaSaludo
{
public function saludar(): string { return "Hola"; }
}
// Una interfaz, una clase, un contexto... para decir "Hola". Overkill.
// ✅ BIEN: método directo
class Saludador
{
public function saludar(): string { return "Hola"; }
}
¿Qué ventaja tiene Strategy sobre usar herencia (subclases) para cada variante?
🤔 11. ¿Cuándo usarlo y cuándo no?
- Tienes varias formas de hacer lo mismo y necesitas intercambiarlas
- Quieres limpiar condicionales gigantes que seleccionan comportamientos
- Necesitas cambiar el algoritmo en runtime (el usuario elige)
- Quieres aislar lógica de negocio compleja en clases independientes
- Necesitas que sea fácil testear cada algoritmo por separado
- Solo tienes 1-2 variantes que nunca van a cambiar
- Las estrategias son tan simples (una línea) que crear clases ensucia más que limpia
- El cambio de algoritmo es interno y automático (eso es State, no Strategy)
- Puedes resolverlo con un callback o closure sin necesidad de clases
Si tienes un
switch/caseoif/elseifque elige entre algoritmos, y encima piensas que tiene previsto crecer, es Strategy picando fuerte a la puerta.
¿Cuándo NO deberías usar el patrón Strategy?
💡 12. Conclusión
El Patrón Strategy es como una herramienta eléctrica con cabezales intercambiables: el motor (contexto) es el mismo, pero según el cabezal (estrategia) que le pongas, puedes taladrar, lijar o pulir.
Lo ves por todas partes, cada vez que pasas un callback a usort() estás usando Strategy. Cada vez que configuras un método de pago en un checkout, Strategy. Cada vez que eliges entre JSON y XML para exportar, Strategy.
Su fuerza está en la composición, en vez de crear subclases para cada variante, inyectas el comportamiento desde fuera. Esto te da flexibilidad total, código limpio y la posibilidad de intercambiar algoritmos en runtime sin tocar nada existente.
Eso sí, si solo tienes una forma de hacer las cosas y no prevés que cambie, no montes toda la estructura. Un método directo es más claro que una interfaz, una clase y un contexto para una sola variante.
EA, ¡saluditos y nos vemos en los bares! 🍻