18. Strategy (Estrategia) - Patrón de Comportamiento

El patrón Strategy permite definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. El cliente elige la estrategia adecuada en tiempo de ejecución sin modificar el código que coordina la operación, lo que facilita extender o reemplazar comportamientos con bajo acoplamiento.

Strategy aplica polimorfismo para sustituir condicionales anidados y mantener abiertas las puertas a nuevas variantes de cálculo. El foco está en parametrizar el comportamiento; la clase que coordina delega el trabajo a un objeto estrategia que encapsula el algoritmo concreto.

18.1 Problema y contexto

En muchos sistemas existen operaciones que se calculan de distintos modos según reglas de negocio, configuraciones o condiciones de tiempo de ejecución. Sin Strategy se suele recurrir a estructuras condicionales complejas (if, switch) que crecen con cada variante agregada, dificultan las pruebas y provocan cambios recurrentes en las clases de alto nivel.

El patrón introduce un nivel de indirección: las clases que utilizan el algoritmo solo conocen la interfaz de la estrategia. Nuevas variantes se implementan como clases adicionales que cumplen el contrato sin alterar el código cliente.

18.2 Intención y motivación

La intención es definir una familia de algoritmos, encapsularlos y hacerlos intercambiables. Strategy es adecuado cuando:

  • Se desea aislar políticas de cálculo o reglas de negocio que cambian con frecuencia.
  • Existen múltiples algoritmos que comparten entrada y salida pero difieren en su implementación.
  • Se requiere seleccionar la variante en tiempo de ejecución, ya sea de forma explícita o por configuración.
  • Se busca reducir condicionales complejos y adherir al principio Abierto/Cerrado.

La motivación está en separar el qué (la operación que el cliente necesita) del cómo (el algoritmo que la realiza), habilitando la evolución independiente de ambas partes.

18.3 Participantes y estructura

Los elementos clásicos del patrón son:

  • Strategy: interfaz que declara la operación invocada por el cliente.
  • ConcreteStrategy: implementaciones específicas del algoritmo.
  • Context: clase que mantiene una referencia a una estrategia y la utiliza para ejecutar la operación.
  • Cliente: configura el contexto con la estrategia deseada.

El contexto puede exponer operaciones para cambiar la estrategia en tiempo de ejecución. Esto habilita modelos flexibles donde la lógica dominante se mantiene estable y solo se sustituyen las políticas de cálculo.

18.4 Estrategias puras vs. estrategias con estado

Una decisión de diseño importante es determinar si la estrategia mantiene estado interno:

  • Estrategias puras: no almacenan datos y dependen solo de los argumentos. Son fáciles de compartir y reutilizar.
  • Estrategias con estado: conservan información entre invocaciones, por ejemplo, estadísticas o configuraciones derivadas. Requieren especial atención al ciclo de vida y a la concurrencia.

Strategy no impone una opción; depende de los requisitos y de cómo se gestione la inmutabilidad o la compartición de instancias.

18.5 Escenario: motor de precios para un marketplace

Imaginemos un marketplace que calcula el precio final de un producto combinando descuentos, impuestos y promociones según el canal de venta. Durante un evento masivo se lanza una campaña con estrategias dinámicas que deben ajustarse a distintos segmentos de clientes sin detener el servicio.

El motor de precios necesita experimentar con devaluaciones progresivas, descuentos escalonados y tarifas fijas para vendedores premium. Strategy permite encapsular cada algoritmo en una estrategia distinta, alternarlas en ejecución y agregar nuevas variantes sin modificar el código central del marketplace.

18.6 Implementación en C#

El siguiente ejemplo modela la estrategia de fijación de precios con descuentos combinados y comisiones configurables:

using System;
using System.Collections.Generic;

public interface IEstrategiaPrecio
{
    decimal Calcular(PeticionPrecio peticion);
}

public class EstrategiaPrecioBase : IEstrategiaPrecio
{
    public decimal Calcular(PeticionPrecio peticion)
    {
        return peticion.MontoBase;
    }
}

public class EstrategiaComisionVendedor : IEstrategiaPrecio
{
    private readonly IEstrategiaPrecio _base;
    private readonly IDictionary<TipoVendedor, decimal> _comisionesPorTipo;

    public EstrategiaComisionVendedor(IEstrategiaPrecio baseEstrategia, IDictionary<TipoVendedor, decimal> comisionesPorTipo)
    {
        _base = baseEstrategia;
        _comisionesPorTipo = comisionesPorTipo;
    }

    public decimal Calcular(PeticionPrecio peticion)
    {
        var monto = _base.Calcular(peticion);
        if (_comisionesPorTipo.TryGetValue(peticion.TipoVendedor, out var comision))
        {
            return monto + monto * comision;
        }

        return monto;
    }
}

public class EstrategiaDescuentoTemporal : IEstrategiaPrecio
{
    private readonly IEstrategiaPrecio _base;
    private readonly DateTime _inicio;
    private readonly DateTime _fin;
    private readonly decimal _porcentajeDescuento;

    public EstrategiaDescuentoTemporal(IEstrategiaPrecio baseEstrategia, DateTime inicio, DateTime fin, decimal porcentajeDescuento)
    {
        _base = baseEstrategia;
        _inicio = inicio;
        _fin = fin;
        _porcentajeDescuento = porcentajeDescuento;
    }

    public decimal Calcular(PeticionPrecio peticion)
    {
        var monto = _base.Calcular(peticion);
        if (peticion.Fecha >= _inicio && peticion.Fecha <= _fin)
        {
            return monto * (1 - _porcentajeDescuento);
        }

        return monto;
    }
}

public class MotorPrecios
{
    private IEstrategiaPrecio _estrategiaActual;

    public MotorPrecios(IEstrategiaPrecio estrategiaActual)
    {
        _estrategiaActual = estrategiaActual;
    }

    public decimal Calcular(PeticionPrecio peticion)
    {
        return _estrategiaActual.Calcular(peticion);
    }

    public void ActualizarEstrategia(IEstrategiaPrecio estrategia)
    {
        _estrategiaActual = estrategia;
    }
}

public record PeticionPrecio(decimal MontoBase, TipoVendedor TipoVendedor, DateTime Fecha);

public enum TipoVendedor
{
    Individual,
    Premium
}

public static class DemoStrategy
{
    public static void Main()
    {
        var baseEstrategia = new EstrategiaPrecioBase();
        var comisiones = new Dictionary<TipoVendedor, decimal>
        {
            [TipoVendedor.Individual] = 0.03m,
            [TipoVendedor.Premium] = 0.01m
        };

        var estrategiaEvento = new EstrategiaDescuentoTemporal(
            new EstrategiaComisionVendedor(baseEstrategia, comisiones),
            DateTime.Today.AddDays(-1),
            DateTime.Today.AddDays(5),
            0.15m);

        var motor = new MotorPrecios(estrategiaEvento);
        var peticion = new PeticionPrecio(5000m, TipoVendedor.Premium, DateTime.Today);
        Console.WriteLine($"Precio evento: {motor.Calcular(peticion):C}");

        motor.ActualizarEstrategia(baseEstrategia);
        Console.WriteLine($"Precio regular: {motor.Calcular(peticion):C}");
    }
}

18.7 Explicación del flujo

El contexto MotorPrecios recibe cualquier implementación de EstrategiaPrecio. La estrategia evento encapsula descuentos y comisiones componiendo otras estrategias, lo que demuestra cómo se pueden encadenar. Cambiar la estrategia en caliente es tan simple como inyectar una nueva instancia, sin modificar el motor ni los clientes que lo consumen.

Las pruebas del motor se vuelven más simples porque es posible sustituir la estrategia real por dobles de prueba que implementen la misma interfaz.

18.8 Strategy en el ecosistema .NET

La documentación de IComparer<T> ejemplifica el patrón: la interfaz define la estrategia de comparación y estructuras como Array.Sort reciben el algoritmo por parámetro. Otros ejemplos incluyen IFormatProvider para políticas de formateo y los AuthenticationHandler de ASP.NET Core, que encapsulan flujos de autenticación intercambiables.

18.9 Variantes y extensiones

Strategy puede combinarse con expresiones lambda o delegados para simplificar implementaciones sin estado. En C# moderno, muchas bibliotecas admiten proveer estrategias mediante funciones lambda, lo que reduce la necesidad de clases adicionales.

Otra extensión habitual es registrar estrategias en un mapa y seleccionarlas por clave, lo que facilita habilitar o deshabilitar algoritmos mediante configuraciones externas o bases de datos.

18.10 Riesgos y malas prácticas

Un error frecuente es multiplicar el número de estrategias sin un criterio claro, generando una proliferación de clases pequeñas difíciles de mantener. También es riesgoso exponer demasiadas dependencias dentro de la estrategia, lo que incrementa el acoplamiento.

Si el cliente olvida configurar la estrategia adecuada, pueden ocurrir fallos en tiempo de ejecución. Es recomendable proveer valores por defecto seguros o validar la configuración durante la inicialización.

18.11 Buenas prácticas para aplicar Strategy

  • Definir una interfaz simple, orientada al dominio, para minimizar el código repetido.
  • Utilizar inmutabilidad o sincronicación adecuada cuando la estrategia mantiene estado.
  • Proveer factorías o registradores que simplifiquen la selección de estrategias según configuraciones.
  • Documentar los criterios que determinan la elección de cada estrategia para evitar decisiones arbitrarias.

18.12 Relación con otros patrones

Strategy se relaciona con State, dado que ambos encapsulan comportamientos intercambiables; la diferencia principal es que en State el cambio de estrategia se vincula con transiciones de estado. Se puede combinar con Template Method cuando parte del algoritmo es fija y otra parte se delega a la estrategia. También colabora con Factory Method para crear estrategias según la configuración y con Decorator para agregar responsabilidades transversales a una estrategia existente.