Intercambiar Algoritmos en Runtime con el Strategy Pattern
Cómo encapsular algoritmos y comportamientos intercambiables usando el strategy pattern con inyección de dependencias, function pointers y lambda strategies en Java, TypeScript y Python.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Visión general
Un sistema de checkout de e-commerce calcula costos de envío. Para órdenes domésticas, usa precio flat-rate. Para órdenes internacionales, usa precio basado en peso. Para entrega express, usa distancia más multiplicadores de urgencia. Una implementación ingenua pone los tres cálculos en un solo método calculateShipping() con un enorme switch. Agregar un nuevo método de envío significa editar ese método, violando el principio open-closed. Testear la lógica de envío requiere configurar todo el objeto checkout.
El strategy pattern separa los algoritmos del contexto que los usa. Cada algoritmo de envío se convierte en su propia clase implementando una interfaz ShippingStrategy compartida. El checkout mantiene una referencia a una estrategia y delega el cálculo a ella. En runtime, intercambias estrategias — flat rate para doméstico, weight-based para internacional — sin cambiar el código del checkout. Nuevas estrategias se agregan escribiendo nuevas clases, no editando existentes. La solucion a continuacion cubre estrategias basadas en clases, en funciones y selección por inyección de dependencias.
Cuándo usarlo
Usa esta receta cuando:
- Múltiples algoritmos o comportamientos existen para la misma tarea y solo uno se usa a la vez. Consulta Factory Pattern para crear algoritmos.
- El algoritmo debe seleccionarse en runtime basado en configuración o input del usuario. Consulta Input Validation para configuración segura.
- Quieres aislar la complejidad del algoritmo de la lógica de negocio principal
- Agregar nuevas variantes no debería requerir modificar código existente. Consulta Adapter Pattern para extender interfaces.
- El estado o configuración específico del algoritmo necesita encapsulación separada del contexto
Solución
Estrategia Basada en Clases (TypeScript)
interface ShippingStrategy {
calculate(order: Order): number;
}
class FlatRateStrategy implements ShippingStrategy {
constructor(private rate: number) {}
calculate(order: Order): number {
return this.rate;
}
}
class WeightBasedStrategy implements ShippingStrategy {
constructor(private ratePerKg: number) {}
calculate(order: Order): number {
return order.totalWeight * this.ratePerKg;
}
}
class DistanceBasedStrategy implements ShippingStrategy {
constructor(private baseRate: number, private perKm: number) {}
calculate(order: Order): number {
return this.baseRate + (order.distanceKm * this.perKm);
}
}
class CheckoutService {
private shippingStrategy: ShippingStrategy;
constructor(strategy: ShippingStrategy) {
this.shippingStrategy = strategy;
}
setStrategy(strategy: ShippingStrategy): void {
this.shippingStrategy = strategy;
}
getTotal(order: Order): number {
const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
const shipping = this.shippingStrategy.calculate(order);
return subtotal + shipping;
}
}
const strategy = order.destination === 'domestic'
? new FlatRateStrategy(10)
: new WeightBasedStrategy(2.5);
const checkout = new CheckoutService(strategy);
const total = checkout.getTotal(order);
Java con Enum Strategy
interface TaxStrategy {
BigDecimal calculateTax(BigDecimal amount);
}
enum TaxRegion implements TaxStrategy {
US {
public BigDecimal calculateTax(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.08"));
}
},
EU {
public BigDecimal calculateTax(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.20"));
}
};
}
class Invoice {
private final TaxStrategy taxStrategy;
Invoice(TaxStrategy taxStrategy) {
this.taxStrategy = taxStrategy;
}
BigDecimal getTotal(BigDecimal subtotal) {
return subtotal.add(taxStrategy.calculateTax(subtotal));
}
}
Invoice invoice = new Invoice(TaxRegion.EU);
Python con Estrategias como Funciones
from typing import Callable, List
from dataclasses import dataclass
@dataclass
class Order:
items: List[float]
total_weight: float
destination: str
Strategy = Callable[[Order], float]
def flat_rate_strategy(order: Order) -> float:
return 10.0
def weight_based_strategy(order: Order) -> float:
return order.total_weight * 2.5
def free_over_threshold(order: Order) -> float:
subtotal = sum(order.items)
return 0.0 if subtotal > 50 else 5.0
class CheckoutService:
def __init__(self, strategy: Strategy):
self.strategy = strategy
def set_strategy(self, strategy: Strategy):
self.strategy = strategy
def get_total(self, order: Order) -> float:
subtotal = sum(order.items)
shipping = self.strategy(order)
return subtotal + shipping
strategies = {
'domestic': flat_rate_strategy,
'international': weight_based_strategy,
'promo': free_over_threshold,
}
checkout = CheckoutService(strategies[order.destination])
total = checkout.get_total(order)
Explicación
- Encapsulación del algoritmo: cada estrategia es un objeto o función autocontenido con su propio estado y comportamiento. El contexto (checkout service) conoce solo la interfaz de la estrategia, no los detalles de implementación. Esto desacopla el contexto de la evolución del algoritmo.
- Selección en runtime: las estrategias se seleccionan en runtime basado en configuración, input del usuario o reglas de negocio. Una factory o registro puede mapear claves a instancias de estrategia. El contexto no hardcodea qué estrategia usar — recibe la estrategia como dependencia.
- Principio open-closed: El checkout service, las estrategias existentes y los tests permanecen intactos. Esta es la esencia del principio open-closed: abierto para extensión, cerrado para modificación.
- Strategy vs función simple: en lenguajes con funciones de primera clase (Python, JavaScript, Go), una estrategia puede ser una función en lugar de una clase. Esto reduce boilerplate para algoritmos stateless.
Variantes
| Variante | Estado | Lenguaje | Mejor para |
|---|---|---|---|
| Estrategia clase | Sí (campos) | Java, C# | Algoritmos complejos con config |
| Lambda/función | No | Python, JS, Go | Algoritmos simples, stateless |
| Enum strategy | Mínimo | Java | Conjunto fijo de estrategias conocidas |
| Registry + strategy | Sí | Cualquiera | Algoritmos configurables por usuario |
| Template method | Heredado | Cualquiera | Estrategias con esqueleto compartido |
Lo que funciona
- Usa inyección de dependencias para selección de estrategia: en lugar de que el contexto construya su propia estrategia, inyéctala vía constructor o setter. Esto hace el contexto testeable con mocks de estrategia y permite al llamador controlar la selección de algoritmo sin modificar el contexto.
- Mantén las interfaces de estrategia enfocadas: una interfaz de estrategia debería tener un método principal. Si te encuentras agregando
init(),validate()ycleanup()a la interfaz, la estrategia está haciendo demasiado. - Documenta precondiciones y efectos secundarios de estrategias: algunas estrategias mutan estado (ej. una estrategia de pago que cobra una tarjeta). Los consumidores deben entender el contrato.
- Considera la null strategy: si el contexto siempre espera una estrategia pero a veces no se necesita comportamiento, implementa un null object strategy que no hace nada.
- Compón estrategias con decorators: un decorator de caching envuelve una estrategia y memoiza resultados. Esto mantiene las estrategias individuales simples mientras agrega concerns transversales externamente.
Errores comunes
- Sobre-ingeniería condicionales simples: si tienes dos estrategias que son cada una una línea, un strategy pattern agrega más boilerplate que valor.
- Poner selección de estrategia dentro del contexto:
if (region === 'US') strategy = new UsTaxStrategy()dentro del contexto viola separación de concerns. El contexto debería recibir la estrategia. La lógica de selección pertenece a una factory, parser de configuración o controlador. - Estrategias accediendo a internals del contexto: una estrategia no debería alcanzar hacia atrás al objeto contexto. Pasa todos los datos necesarios como parámetros al método de estrategia. El acoplamiento bidireccional hace tanto al contexto como a la estrategia más difíciles de testear y razonar.
- Interfaces de estrategia inconsistentes: Define la interfaz precisamente — tipos de retorno, contratos de excepción y formas de parámetros deben ser uniformes entre todas las estrategias.
Preguntas frecuentes
Strategy Registry con Selección Dinámica (TypeScript)
class StrategyRegistry<TContext, TResult> {
private strategies: Map<string, (ctx: TContext) => TResult> = new Map();
register(key: string, strategy: (ctx: TContext) => TResult): void {
this.strategies.set(key, strategy);
}
select(context: TContext & { strategyKey?: string }): (ctx: TContext) => TResult {
const key = context.strategyKey;
if (!key || !this.strategies.has(key)) {
throw new Error(`No strategy registered for key: ${key}`);
}
return this.strategies.get(key)!;
}
}
// Registro al arranque
const shippingRegistry = new StrategyRegistry<Order, number>();
shippingRegistry.register('flat-rate', (order) => 10);
shippingRegistry.register('weight-based', (order) => order.totalWeight * 2.5);
shippingRegistry.register('distance', (order) => 5 + order.distanceKm * 0.5);
shippingRegistry.register('free-shipping', (order) => {
const subtotal = order.items.reduce((s, i) => s + i.price, 0);
return subtotal > 100 ? 0 : 10;
});
// Uso — selecciona estrategia por clave desde metadatos de la orden
const calculate = shippingRegistry.select(order as Order & { strategyKey: string });
const shipping = calculate(order);
Strategy con Decorator Composition (TypeScript)
interface PricingStrategy {
calculate(order: Order): Money;
}
class BasePricingStrategy implements PricingStrategy {
calculate(order: Order): Money {
return order.items.reduce(
(total, item) => total.add(item.price),
Money.zero('USD')
);
}
}
class DiscountDecorator implements PricingStrategy {
constructor(
private wrapped: PricingStrategy,
private discountPercentage: number
) {}
calculate(order: Order): Money {
const base = this.wrapped.calculate(order);
const discount = base.multiply(this.discountPercentage / 100);
return base.subtract(discount);
}
}
class TaxDecorator implements PricingStrategy {
constructor(
private wrapped: PricingStrategy,
private taxRate: number
) {}
calculate(order: Order): Money {
const base = this.wrapped.calculate(order);
const tax = base.multiply(this.taxRate / 100);
return base.add(tax);
}
}
class FreeShippingDecorator implements PricingStrategy {
constructor(
private wrapped: PricingStrategy,
private threshold: number
) {}
calculate(order: Order): Money {
const base = this.wrapped.calculate(order);
if (base.amount > this.threshold) {
return base;
}
return base.add(new Money(10, 'USD'));
}
}
// Composición — apila decorators para construir la estrategia final
const pricing = new FreeShippingDecorator(
new TaxDecorator(
new DiscountDecorator(
new BasePricingStrategy(),
10
),
8
),
100
);
const total = pricing.calculate(order);
Selección de Estrategia Basada en Contexto (Python)
from typing import Protocol
class PaymentStrategy(Protocol):
def pay(self, amount: float) -> str: ...
class CreditCardStrategy:
def __init__(self, card_number: str, cvv: str):
self._card = card_number
self._cvv = cvv
def pay(self, amount: float) -> str:
return f"Charged ${amount:.2f} to card ending in {self._card[-4:]}"
class PayPalStrategy:
def __init__(self, email: str):
self._email = email
def pay(self, amount: float) -> str:
return f"Charged ${amount:.2f} via PayPal ({self._email})"
class CryptoStrategy:
def __init__(self, wallet: str):
self._wallet = wallet
def pay(self, amount: float) -> str:
return f"Charged {amount / 50000:.8f} BTC from {self._wallet[:8]}..."
class PaymentContext:
def __init__(self):
self._strategies: dict[str, PaymentStrategy] = {}
def register(self, key: str, strategy: PaymentStrategy):
self._strategies[key] = strategy
def pay(self, method: str, amount: float) -> str:
strategy = self._strategies.get(method)
if strategy is None:
raise ValueError(f"Unknown payment method: {method}")
return strategy.pay(amount)
# Uso — registra estrategias, selecciona por clave de método
context = PaymentContext()
context.register('credit-card', CreditCardStrategy('4111111111111234', '123'))
context.register('paypal', PayPalStrategy('user@example.com'))
context.register('crypto', CryptoStrategy('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'))
result = context.pay('paypal', 99.99)
Recursos Relacionados
Crear Objetos Flexiblemente con el Factory Pattern
Cómo usar factory methods, abstract factories y containers de inyección de dependencias para desacoplar creación de objetos de su uso y mejorar testeabilidad.
RecipePuente entre Interfaces Incompatibles con el Adapter Pattern
Cómo integrar APIs legacy, librerías de terceros e interfaces incompatibles usando object adapters, class adapters y facade adapters en Java, TypeScript y Python.
RecipeConstruir Aplicaciones Mantenibles con Arquitectura
Cómo estructurar aplicaciones usando ports y adapters para aislar lógica de negocio de frameworks, bases de datos y servicios externos para testabilidad y flexibilidad.