Introducción: cuando SOLID deja de ser teoría
La mayoría de los desarrolladores conocen el acrónimo SOLID a nivel de "una clase debe responder por una sola cosa". Sin embargo, en proyectos reales en C# (.NET) con Dependency Injection, microservicios y patrones asíncronos, seguir los principios de forma ingenua produce el efecto contrario: sobrecomplejidad, interfaces infladas y violación de la encapsulación.
En este artículo veremos SOLID no como un conjunto de dogmas, sino como una herramienta para tomar decisiones arquitectónicas. Aprenderás cómo aplicar correctamente los principios en escenarios avanzados: trabajo con contenedores IoC, composición en lugar de herencia, manejo de errores mediante interfaces y pruebas de sagas.
1. Single Responsibility: límites de responsabilidad en DDD
El principio de responsabilidad única (SRP) a menudo se confunde con "una clase, una función". En realidad, se trata de la razón de cambio (reason to change). En el contexto de C# y Domain-Driven Design, esto significa que una clase debe cambiar solo por una razón, relacionada con un actor o regla de negocio.
Antipatrón avanzado: "Entity divina"
Consideremos un ejemplo típico de violación de SRP en .NET: la clase OrderEntity, que simultáneamente gestiona el estado del pedido, calcula impuestos, envía correos electrónicos y guarda datos en la BD mediante Entity Framework.
// ❌ Violación de SRPpublic class OrderEntity{ public int Id { get; set; } public decimal Total { get; set; } public string Email { get; set; }
public void CalculateTax() { /* lógica de impuestos */ } public void SendConfirmation() { /* envío de email */ } public void SaveToDatabase() { /* EF Core SaveChanges */ }}Problema: un cambio en la regla de negocio de impuestos (por ejemplo, una nueva ley) provocará un cambio en la clase que también se encarga de enviar notificaciones. Esto aumenta el riesgo de regresión.
Solución: separación de servicios mediante DI
// ✅ SRP mediante serviciospublic class Order{ public int Id { get; set; } public decimal Total { get; set; } // Solo estado, sin lógica}
public class TaxCalculator : ITaxCalculator{ public decimal Calculate(Order order) { /* ... */ }}
public class EmailService : INotificationService{ public Task SendConfirmation(Order order) { /* ... */ }}
public class OrderRepository : IRepository<Order>{ public Task SaveAsync(Order order) { /* EF Core */ }}Ahora cada clase tiene exactamente una razón para cambiar. El contenedor DI (Microsoft.Extensions.DependencyInjection) las vincula a través de los constructores.
2. Open/Closed: extensión mediante estrategia y decorador
El principio de abierto/cerrado (OCP) establece: una clase debe estar abierta para extensión, pero cerrada para modificación. En .NET esto se logra no solo mediante herencia, sino también mediante composición con los patrones Strategy y Decorator.
Caso avanzado: validación con crecimiento de reglas
Imagina que tienes un sistema de validación de pedidos. La versión inicial solo verificaba el total y la dirección. Un mes después se agregó la regla "no se pueden pedir productos por un monto superior al límite". En lugar de modificar la clase OrderValidator, usamos composición.
// ❌ Violación de OCP — modificación de la clase existentepublic class OrderValidator{ public bool Validate(Order order) { if (order.Total < 0) return false; if (string.IsNullOrEmpty(order.Address)) return false; // Nueva regla — modificamos la clase if (order.Total > 10000) return false; return true; }}// ✅ OCP mediante composición de validadorespublic interface IValidator<T>{ bool Validate(T entity);}
public class TotalValidator : IValidator<Order>{ public bool Validate(Order order) => order.Total > 0;}
pu