Saltar al contenidoJMRG
Todas las entradas

Patrón Strategy: Algoritmos Intercambiables en Runtime

design-patternsbehavioralstrategy

¿Qué es el Patrón Strategy?

El patrón Strategy define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables. Permite que el algoritmo varíe independientemente de los clientes que lo utilizan.

classDiagram
    class Context {
        -strategy: Strategy
        +setStrategy(strategy)
        +executeStrategy()
    }

    class Strategy {
        <<interface>>
        +execute(data)
    }

    class ConcreteStrategyA {
        +execute(data)
    }

    class ConcreteStrategyB {
        +execute(data)
    }

    Context o--> Strategy
    Strategy <|.. ConcreteStrategyA
    Strategy <|.. ConcreteStrategyB

En términos simples: en lugar de tener un if-else gigante para decidir qué algoritmo usar, extraes cada algoritmo a su propia clase y los intercambias según necesites.

El Problema que Resuelve

class PaymentProcessor {
  process(method: string, amount: number): void {
    if (method === 'credit_card') {
      this.processCreditCard(amount);
    } else if (method === 'paypal') {
      this.processPayPal(amount);
    } else if (method === 'crypto') {
      this.processCrypto(amount);
    }
  }
}

Problemas: La clase crece sin control, difícil de testear, viola Open/Closed.

La Solución: Strategy

1. Definir la Interfaz Strategy

interface PaymentData {
  amount: number;
  currency: string;
  customerId: string;
}
interface PaymentResult {
  success: boolean;
  transactionId: string;
  fee: number;
  netAmount: number;
}
/** Contrato que toda estrategia de pago debe implementar */
interface PaymentStrategy {
  readonly name: string;
  process(data: PaymentData): Promise<PaymentResult>;
  calculateFee(amount: number): number;
  validate(data: PaymentData): boolean;
}

2. Implementar Estrategias Concretas

/** Estrategia para pagos con tarjeta de crédito */
class CreditCardStrategy implements PaymentStrategy {
  readonly name = 'credit_card';
  async process(data: PaymentData): Promise<PaymentResult> {
    const fee = this.calculateFee(data.amount);
    return {
      success: true,
      transactionId: `cc_${Date.now()}`,
      fee,
      netAmount: data.amount - fee,
    };
  }
  calculateFee(amount: number): number {
    return amount * 0.029 + 0.30;
  }
  validate(data: PaymentData): boolean {
    return data.amount <= 10000;
  }
}
/** Estrategia para pagos con criptomonedas */
class CryptoStrategy implements PaymentStrategy {
  readonly name = 'crypto';
  async process(data: PaymentData): Promise<PaymentResult> {
    const fee = this.calculateFee(data.amount);
    return {
      success: true,
      transactionId: `crypto_${Date.now()}`,
      fee,
      netAmount: data.amount - fee,
    };
  }
  calculateFee(amount: number): number {
    return amount * 0.01;
  }
  validate(data: PaymentData): boolean {
    return data.amount >= 10;
  }
}

3. Crear el Contexto

/** Procesador que acepta múltiples estrategias de pago */
class PaymentProcessor {
  private strategies = new Map<string, PaymentStrategy>();
  private currentStrategy: PaymentStrategy | null = null;
  registerStrategy(strategy: PaymentStrategy): this {
    this.strategies.set(strategy.name, strategy);
    return this;
  }
  setStrategy(name: string): this {
    const strategy = this.strategies.get(name);
    if (!strategy) throw new Error(`Unknown strategy: ${name}`);
    this.currentStrategy = strategy;
    return this;
  }
  async process(data: PaymentData): Promise<PaymentResult> {
    if (!this.currentStrategy) throw new Error('No strategy selected');
    if (!this.currentStrategy.validate(data)) throw new Error('Validation failed');
    return this.currentStrategy.process(data);
  }
  /** Selecciona automáticamente la estrategia con menor comisión */
  selectBestStrategy(amount: number): string {
    let bestStrategy: PaymentStrategy | null = null;
    let lowestFee = Infinity;
    for (const strategy of this.strategies.values()) {
      if (strategy.validate({ amount, currency: 'USD', customerId: '' })) {
        const fee = strategy.calculateFee(amount);
        if (fee < lowestFee) {
          lowestFee = fee;
          bestStrategy = strategy;
        }
      }
    }
    if (!bestStrategy) throw new Error('No valid strategy');
    this.currentStrategy = bestStrategy;
    return bestStrategy.name;
  }
}

Ejemplo de Uso

const processor = new PaymentProcessor()
  .registerStrategy(new CreditCardStrategy())
  .registerStrategy(new CryptoStrategy());
processor.setStrategy('crypto');
const result = await processor.process({
  amount: 99.99,
  currency: 'USD',
  customerId: 'cust_123',
});
const bestMethod = processor.selectBestStrategy(500);
console.log(`Best method for $500: ${bestMethod}`);

Beneficios del Patrón Strategy

Aspecto Beneficio
Open/Closed Agregar estrategias sin modificar código
Single Responsibility Cada estrategia tiene su propia clase
Testabilidad Cada estrategia se testea aisladamente
Flexibilidad Cambiar algoritmo en runtime

Cuándo Usar Strategy

  • Cuando necesitas diferentes variantes de un algoritmo
  • Cuando quieres evitar condicionales múltiples
  • Cuando tienes clases que solo difieren en su comportamiento

Cuándo NO Usar Strategy

  • Si solo hay 2-3 algoritmos simples que nunca cambiarán
  • Si la selección del algoritmo no cambia en runtime

Conclusión

El patrón Strategy es una herramienta poderosa para manejar múltiples algoritmos de forma limpia y extensible.

En el próximo artículo exploraremos el Patrón Adapter, que nos permite integrar interfaces incompatibles.


Basado en "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).