StackPractices
beginner Por Mathias Paulenko

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.

Temas: design

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

VarianteEstadoLenguajeMejor para
Estrategia claseSí (campos)Java, C#Algoritmos complejos con config
Lambda/funciónNoPython, JS, GoAlgoritmos simples, stateless
Enum strategyMínimoJavaConjunto fijo de estrategias conocidas
Registry + strategyCualquieraAlgoritmos configurables por usuario
Template methodHeredadoCualquieraEstrategias 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() y cleanup() 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)