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.
🚀 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.
🎯 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.
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}");
}
}
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:
- Verificar si el stock de productos es suficiente.
- Validar que el cupón de descuento (si existe) sea válido.
- Ejecutar una verificación de fraude con una pasarela externa.
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}");
🛠️ 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ística | Strategy | Chain of Responsibility |
|---|---|---|
| --- | --- | --- |
| Propósito Principal | Seleccionar un algoritmo intercambiable | Pasar solicitudes a lo largo de una secuencia de objetos |
| Toma de decisiones | El cliente elige la estrategia explícitamente | Cada manejador decide si procesa o pasa la solicitud |
| --- | --- | --- |
| Acoplamiento | Bajo (reemplazable en tiempo de ejecución) | Bajo (desacopla emisor y receptores múltiples) |
| Caso de uso típico | Descuentos, métodos de pago, compresión | Pipelines 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.
Tutoriales relacionados
- Desarrollo de Microservicios con gRPC en C# y ASP.NET Core: Comunicación de Alto Rendimientointermediate30 min
- C#: Desbloqueando la Programación Funcional con Tuplas, Funciones Lambda y Extension Methodsintermediate18 min
- C#: Desbloqueando la Inmutabilidad con Registros (Records) y Tuplas para un Código Más Robustointermediate20 min
- Programación Asíncrona en C#: Desmitificando async/await para un Rendimiento Óptimointermediate20 min
- C#: Desbloqueando la Reflexión y los Atributos para Metaprogramación Avanzadaadvanced18 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!