tutoriales.com

Patrones de Diseño Modernos en C#: Implementando Strategy y Chain of Responsibility para un Código Limpio

En este tutorial avanzado de C#, exploraremos cómo implementar los patrones de diseño Strategy y Chain of Responsibility. Descubrirás cómo transformar lógicas complejas llenas de condicionales en arquitecturas flexibles y modulares.

Intermedio8 min de lectura4 views
Reportar error

🚀 Introducción a los Patrones de Diseño Modernos

Cuando desarrollamos aplicaciones empresariales en C#, es común enfrentarnos a bases de código que crecen de manera desmedida. Las sentencias condicionales masivas (if-else anidados o switch gigantescos) son el talón de Aquiles de la mantenibilidad. En este tutorial, profundizaremos en dos de los patrones de diseño de comportamiento más potentes: Strategy y Chain of Responsibility (Cadena de Responsabilidad).

Con estos patrones, lograrás aplicar los principios SOLID, especialmente el Principio de Responsabilidad Única (SRP) y el Principio de Abierto/Cerrado (OCP). A lo largo de esta guía, implementaremos escenarios reales utilizando características modernas de C# como inyección de dependencias nativa, expresiones lambda y registros.

💡 Consejo: Asegúrate de tener instalado el SDK de .NET 8 o superior para aprovechar al máximo las características modernas de C# utilizadas en este tutorial.

🎯 El Patrón Strategy: Desacoplando Algoritmos

El patrón Strategy permite definir una familia de algoritmos, encapsular cada uno en una clase separada y hacer que sus objetos sean intercambiables en tiempo de ejecución. Esto evita el uso masivo de declaraciones switch cuando necesitamos aplicar diferentes reglas de negocio según el contexto.

Escenario Práctico: Motor de Descuentos para E-commerce

Imaginemos que estamos construyendo una plataforma de comercio electrónico donde los clientes pueden tener diferentes tipos de membresía (Regular, VIP, Empleado). Cada membresía aplica un cálculo de descuento distinto sobre el total de la compra.

CheckoutContext - strategy: IDiscount + calculateTotal(): void «interface» IDiscountStrategy + applyDiscount(amt: double) RegularDiscountStrategy + applyDiscount(amt) VipDiscountStrategy + applyDiscount(amt) EmployeeDiscountStrategy + applyDiscount(amt)

Implementación en C#

Primero, definimos la interfaz común para todas las estrategias de descuento:

public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal totalAmount);
}

A continuación, implementamos las estrategias concretas para cada tipo de cliente:

public class RegularDiscountStrategy : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal totalAmount)
    {
        return totalAmount * 0.05m; // 5% de descuento
    }
}

public class VipDiscountStrategy : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal totalAmount)
    {
        return totalAmount * 0.20m; // 20% de descuento
    }
}

public class EmployeeDiscountStrategy : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal totalAmount)
    {
        return totalAmount * 0.50m; // 50% de descuento
    }
}

Integración con el Contenedor de Inyección de Dependencias

Una de las ventajas de ASP.NET Core es su contenedor DI nativo. Podemos registrar todas las estrategias y resolverlas mediante un diccionario o mediante inyección directa en el consumidor.

public class CheckoutService
{
    private readonly Dictionary<string, IDiscountStrategy> _strategies;

    public CheckoutService(IEnumerable<IDiscountStrategy> strategies)
    {
        // Mapeamos las estrategias basándonos en el nombre de la clase o un identificador
        _strategies = strategies.ToDictionary(
            s => s.GetType().Name.Replace("DiscountStrategy", "").ToLower(),
            s => s
        );
    }

    public decimal ApplyDiscount(string customerType, decimal amount)
    {
        if (_strategies.TryGetValue(customerType.ToLower(), out var strategy))
        {
            return strategy.CalculateDiscount(amount);
        }
        
        throw new ArgumentException($"Estrategia de descuento no encontrada para: {customerType}");
    }
}
📌 Nota: Este enfoque nos permite agregar nuevas estrategias (por ejemplo, `FlashSaleDiscountStrategy`) simplemente creando una nueva clase que implemente IDiscountStrategy, sin modificar ni una sola línea de código existente en el servicio de checkout.

⛓️ El Patrón Chain of Responsibility: Procesamiento Secuencial

El patrón Chain of Responsibility pasa solicitudes a lo largo de una cadena de manejadores (handlers). Cada manejador decide, al recibir la solicitud, si la procesa o si la pasa al siguiente manejador de la cadena.

Escenario Práctico: Pipeline de Validación de Órdenes

Supongamos que estamos procesando una orden de compra en nuestra tienda online. Antes de aceptar la orden, debemos pasar por múltiples validaciones:

  1. Verificar si el stock de productos es suficiente.
  2. Validar que el cupón de descuento (si existe) sea válido.
  3. Ejecutar una verificación de fraude con una pasarela externa.
Orden Recibida StockHandler CouponHandler FraudHandler Pedido Completado

Implementación Base del Manejador

Creamos una clase abstracta base que gestione la referencia al siguiente eslabón de la cadena:

public record Order(int Id, string CustomerEmail, decimal TotalAmount, string? CouponCode, bool HasStock);

public interface IOrderHandler
{
    IOrderHandler SetNext(IOrderHandler handler);
    Task<bool> HandleAsync(Order order);
}

public abstract class BaseOrderHandler : IOrderHandler
{
    private IOrderHandler? _nextHandler;

    public IOrderHandler SetNext(IOrderHandler handler)
    {
        _nextHandler = handler;
        return handler; // Permite encadenamiento fluido
    }

    public virtual async Task<bool> HandleAsync(Order order)
    {
        if (_nextHandler != null)
        {
            return await _nextHandler.HandleAsync(order);
        }
        return true;
    }
}

Implementación de los Eslabones Concretos

Ahora creamos los manejadores específicos para el stock, el cupón y el fraude:

public class StockValidationHandler : BaseOrderHandler
{
    public override async Task<bool> HandleAsync(Order order)
    {
        if (!order.HasStock)
        {
            Console.WriteLine($"[Fallo] La orden #{order.Id} no tiene stock disponible.");
            return false;
        }

        Console.WriteLine($"[Éxito] Stock validado para la orden #{order.Id}.");
        return await base.HandleAsync(order);
    }
}

public class FraudCheckHandler : BaseOrderHandler
{
    public override async Task<bool> HandleAsync(Order order)
    {
        // Simulación de verificación de fraude
        if (order.TotalAmount > 5000m)
        {
            Console.WriteLine($"[Fallo] La orden #{order.Id} requiere revisión manual por alto monto.");
            return false;
        }

        Console.WriteLine($"[Éxito] Verificación de fraude superada para la orden #{order.Id}.");
        return await base.HandleAsync(order);
    }
}

Ensamblando la Cadena

Podemos construir y ejecutar la cadena en nuestro flujo de trabajo de la siguiente manera:

var stockHandler = new StockValidationHandler();
var fraudHandler = new FraudCheckHandler();

// Construimos la cadena
stockHandler.SetNext(fraudHandler);

var sampleOrder = new Order(101, "cliente@email.com", 250.00m, "VERANO2024", true);

bool isValid = await stockHandler.HandleAsync(sampleOrder);
Console.WriteLine($"Resultado final del procesamiento de orden: {isValid}");
⚠️ Advertencia: Si olvidas llamar a `base.HandleAsync(order)` en alguno de tus manejadores concretos, romperás la cadena y los siguientes eslabones nunca se ejecutarán.

🛠️ Combinando Strategy y Chain of Responsibility

Una práctica avanzada en arquitecturas empresariales consiste en combinar ambos patrones. Por ejemplo, podemos usar el patrón Chain of Responsibility para determinar el flujo de validaciones y, dentro de uno de los eslabones, utilizar el patrón Strategy para calcular el costo de envío o los impuestos basándonos en la ubicación geográfica del usuario.

Resumen de Patrones

CaracterísticaStrategyChain of Responsibility
---------
Propósito PrincipalSeleccionar un algoritmo intercambiablePasar solicitudes a lo largo de una secuencia de objetos
Toma de decisionesEl cliente elige la estrategia explícitamenteCada manejador decide si procesa o pasa la solicitud
---------
AcoplamientoBajo (reemplazable en tiempo de ejecución)Bajo (desacopla emisor y receptores múltiples)
Caso de uso típicoDescuentos, métodos de pago, compresiónPipelines de validación, manejo de excepciones, logging

📋 Conclusión

El dominio de los patrones Strategy y Chain of Responsibility en C# te empodera para escribir código altamente modular, fácil de testear mediante pruebas unitarias y preparado para crecer sin fricción. Al delegar la complejidad lógica en clases especializadas y desacopladas, mantienes tus controladores y servicios limpios y enfocados en su propósito principal.

Paso 1: Identificar condicionales complejos en tu código.
Paso 2: Diseñar interfaces claras para extraer comportamientos (Strategy).
Paso 3: Encadenar reglas secuenciales independientes (Chain of Responsibility).
Paso 4: Integrar todo con el contenedor de dependencias de .NET.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!