Revisión SOLID
Autor: Guerth Castro (github.com/GuerthCastro). Licenciado bajo MIT.
Proceso de revisión
Cuando se proporciona código para revisión:
- Identificar el lenguaje y la capa (entidad de dominio C#, manejador de aplicación, servicio Angular, componente React, etc.).
- Evaluar cada uno de los cinco principios de forma independiente.
- Producir un informe estructurado usando el formato a continuación.
- Clasificar las violaciones por severidad: Crítico (rompe la arquitectura) sobre Mayor (degrada la mantenibilidad) sobre Menor (preferencia de estilo).
- Proporcionar una versión refactorizada concreta para cada violación Crítica o Mayor.
No añadir relleno al informe. Si un principio se cumple genuinamente, indicar PASS y continuar.
Los cinco principios
S: Principio de Responsabilidad Única
Una clase, una razón para cambiar.
Violaciones a buscar:
- Una clase realiza acceso a datos y lógica de negocio.
- Un servicio envía correos, valida datos y escribe en la base de datos.
- Un componente obtiene datos, los formatea y gestiona el estado de la UI.
- Un manejador orquesta varias operaciones no relacionadas.
- Clases "dios" con diez o más métodos que abarcan múltiples dominios.
VIOLACIÓN. OrderService valida, persiste y notifica. La llamada de notificación es la que no pertenece.
public class OrderService(IOrderRepository repository, IEmailService email)
{
public async Task<bool> AddItem(long orderId, long productId)
{
Order order = await repository.GetById(orderId);
if (order.ItemCount >= order.MaxItems)
{
return false;
}
await repository.AddItem(orderId, productId);
await email.SendOrderUpdated(orderId);
return true;
}
}
Solución: extraer un servicio de notificación y dejar que el manejador coordine las dos llamadas.
O: Principio Abierto/Cerrado
Abierto para extensión, cerrado para modificación.
Violaciones a buscar:
- Cadenas de
if/else ifo bloquesswitchsobre cadenas de tipo o enumeraciones que crecen con cada funcionalidad. - Agregar un nuevo tipo de informe requiere editar el servicio de informes existente.
- Agregar un nuevo método de pago requiere editar el manejador de pago.
- Dependencias concretas donde corresponde una abstracción.
VIOLACIÓN. Un nuevo formato de exportación significa editar este método.
public byte[] ExportInvoice(string format, IEnumerable<InvoiceLine> lines)
{
if (format == "pdf")
{
return GeneratePdf(lines);
}
if (format == "excel")
{
return GenerateExcel(lines);
}
throw new NotSupportedException(format);
}
Solución: una abstracción IInvoiceExporter con implementaciones PdfInvoiceExporter y ExcelInvoiceExporter, resueltas por clave desde el contenedor.
L: Principio de Sustitución de Liskov
Los subtipos deben ser sustituibles por sus tipos base.
Violaciones a buscar:
- Una clase derivada lanza
NotImplementedExceptionpara un miembro heredado. - Una anulación cambia el comportamiento esperado: un no-op silencioso, una forma de retorno diferente, un nuevo tipo de excepción.
- Una interfaz declara miembros que algunas implementaciones no pueden implementar de manera significativa.
- Conversión a un tipo concreto para alcanzar comportamiento:
if (repository is SqlOrderRepository sql).
I: Principio de Segregación de Interfaces
Ninguna clase debe ser forzada a implementar miembros que no usa.
Violaciones a buscar:
- Interfaces gruesas con quince o más miembros donde las implementaciones usan tres.
- Un
IRepository<T>genérico que fuerzaGetAll,GetById,Create,Update,Delete,Paginate,SearchyBulkInserten cada entidad. - Una interfaz de servicio que mezcla lecturas y escrituras cuando los consumidores solo necesitan un lado.
VIOLACIÓN. Un consumidor de solo lectura de informes se ve forzado a ver Crear, Actualizar y Eliminar.
public interface IOrderRepository
{
Task<Order> GetById(long id);
Task<IEnumerable<Order>> GetAll();
Task Create(Order order);
Task Update(Order order);
Task Delete(long id);
}
Solución: dividir en IOrderReader e IOrderWriter. Componerlos donde ambos sean necesarios.
D: Principio de Inversión de Dependencias
Depender de abstracciones, no de concreciones.
Violaciones a buscar:
newinstanciando un servicio o repositorio dentro de la lógica de negocio.- Una dependencia directa en
SqlConnectionoNpgsqlConnectiondentro de un servicio. - La capa de Aplicación importando tipos de Infraestructura.
- Clases de servicio estáticas que mantienen estado mutable.
VIOLACIÓN. El manejador construye su propio repositorio concreto y conexión.
public class GetOrderHandler
{
public async Task<OrderDto> Handle(long id)
{
OrderRepository repository = new(new SqlConnection("..."));
Order order = await repository.GetById(id);
return order.MapToDto();
}
}
Solución: inyectar IOrderRepository a través del constructor primario y registrarlo en el contenedor en la raíz de composición.
Formato del informe
Producir esta estructura para cada revisión:
## Revisión SOLID: [NombreClase o nombre de archivo]
**Idioma/Capa:** Manejador de Aplicación C#, Servicio Angular, etc.
### S: Responsabilidad Única
Estado: PASS / VIOLACIÓN (Crítico, Mayor, Menor)
Hallazgo: [lo que la clase hace más allá de su responsabilidad única]
Solución: [acción concreta]
### O: Abierto/Cerrado
Estado: PASS / VIOLACIÓN
Hallazgo: ...
Solución: ...
### L: Sustitución de Liskov
Estado: PASS / VIOLACIÓN / N/A (sin herencia)
Hallazgo: ...
### I: Segregación de Interfaces
Estado: PASS / VIOLACIÓN / N/A (sin interfaces)
Hallazgo: ...
### D: Inversión de Dependencias
Estado: PASS / VIOLACIÓN
Hallazgo: ...
Solución: ...
### Violaciones prioritarias
1. [Crítico] DIP: el manejador instancia un repositorio concreto
2. [Mayor] SRP: el servicio también envía el correo de notificación
### Versión refactorizada
[bloque de código completo corregido]
Violaciones de capas de Clean Architecture
Más allá de SOLID, marcar estos problemas transversales:
- Una entidad de Dominio importa tipos de Aplicación o Infraestructura (violación de dirección de dependencia).
- Un controlador contiene lógica de negocio que pertenece a un manejador.
- Un repositorio devuelve DTOs en lugar de entidades de dominio. El mapeo pertenece a Aplicación.
- Infraestructura llama a una API externa directamente en lugar de implementar una interfaz declarada en Aplicación o Dominio.
- Un manejador toca
HttpContext, filtrando una preocupación de infraestructura en la capa de aplicación. - Un registro de DI en el ensamblado incorrecto, por lo que una capa tiene que hacer referencia hacia abajo para compilar.