metis-desk/.agents/skills/rdx-solid/referencias/principios-solid.md

804 lines
25 KiB
Markdown
Raw Normal View History

# Principios SOLID
## Introducción
Los principios SOLID constituyen un conjunto de criterios de diseño orientados a desarrollar software flexible, mantenible, comprensible y verificable.
Su aplicación adecuada permite:
- Reducir el acoplamiento innecesario.
- Mejorar la cohesión de clases y módulos.
- Facilitar la evolución de funcionalidades.
- Proteger contratos y comportamientos existentes.
- Simplificar las pruebas y el mantenimiento.
- Controlar el impacto de los cambios.
Estos principios deben aplicarse con criterio profesional y teniendo en cuenta las necesidades reales del sistema.
**SOLID no exige utilizar siempre interfaces, herencia, patrones de diseño ni arquitecturas complejas.**
El objetivo es mejorar la estructura del software, no maximizar el número de abstracciones.
Este documento desarrolla los criterios establecidos en el `SKILL.md` principal.
---
## 1. S — Principio de responsabilidad única (Single Responsibility Principle, SRP)
> "Una clase debería tener una, y solo una, razón para cambiar."
### 1.1. Qué problema resuelve
SRP ayuda a evitar clases o módulos que acumulan responsabilidades independientes.
Estas estructuras suelen presentar:
- Dificultad para comprender su comportamiento.
- Cambios frecuentes por motivos diferentes.
- Pruebas complejas.
- Dependencias excesivas.
- Mayor riesgo de regresiones.
- Acoplamiento entre funcionalidades que deberían evolucionar por separado.
### 1.2. Cómo aplicarlo
Cada clase o módulo debe representar una responsabilidad coherente.
Una responsabilidad no equivale necesariamente a un único método, operación o dato.
Una clase puede realizar varias operaciones relacionadas y seguir cumpliendo SRP.
**Ejemplo: responsabilidades mezcladas**
```typescript id="lzj3ab"
// MAL: Mezcla reglas de negocio, persistencia y generación de documentos.
class Order {
calculateTotal(): Money {
// Calcular el importe del pedido.
}
async saveToDatabase(): Promise<void> {
// Persistir el pedido.
}
generateInvoice(): Invoice {
// Generar la factura.
}
}
```
En este caso, la clase puede cambiar por motivos independientes:
- Modificación de reglas de cálculo.
- Cambios en la persistencia.
- Cambios en la generación de facturas.
**Alternativa: responsabilidades diferenciadas**
```typescript id="u0b51p"
class Order {
private readonly items: OrderItem[] = [];
addItem(item: OrderItem): void {
this.items.push(item);
}
calculateTotal(): Money {
return calculateOrderTotal(this.items);
}
}
```
```typescript id="9drm12"
class OrderRepository {
async save(order: Order): Promise<void> {
// Persistir el pedido.
}
}
```
```typescript id="e1iz3a"
class InvoiceGenerator {
generate(order: Order): Invoice {
// Generar una factura a partir del pedido.
}
}
```
La entidad `Order` mantiene las operaciones relacionadas con sus reglas de negocio, mientras que la persistencia y la generación de documentos pertenecen a responsabilidades diferenciadas.
Los ejemplos son ilustrativos y presuponen los tipos y colaboradores necesarios.
### 1.3. Preguntas para detectar problemas
- ¿Tiene esta clase varios motivos independientes para cambiar?
- ¿Sus operaciones responden a una responsabilidad coherente?
- ¿Distintos requisitos obligan a modificar partes no relacionadas?
- ¿La clase depende de demasiados componentes externos?
- ¿Sería más sencillo probar y mantener determinadas responsabilidades por separado?
### 1.4. Evitar una interpretación excesivamente estricta
No dividas una clase únicamente porque su descripción incluya la palabra «y».
Por ejemplo, una entidad `Order` puede:
- Añadir artículos.
- Eliminar artículos.
- Calcular importes.
- Comprobar restricciones.
- Cambiar de estado.
Todas estas operaciones pueden pertenecer a una única responsabilidad: gestionar el comportamiento del pedido.
**SRP busca separar motivos de cambio independientes, no reducir todas las clases a una única operación.**
---
## 2. O — Principio abierto/cerrado (Open/Closed Principle, OCP)
> "Las entidades de software deberían estar abiertas a la extensión, pero cerradas a la modificación."
### 2.1. Qué problema resuelve
OCP busca reducir la necesidad de modificar código estable cada vez que se incorpora una variante de comportamiento.
Esto resulta especialmente útil cuando:
- Existen múltiples implementaciones de una capacidad.
- Se incorporan nuevas variantes con frecuencia.
- Los componentes tienen contratos estables.
- Modificar lógica existente supone un riesgo elevado.
### 2.2. Cómo aplicarlo
Cuando exista una necesidad real de extensión, diseña contratos que permitan incorporar nuevos comportamientos sin modificar innecesariamente los consumidores.
**Ejemplo: cálculo de gastos de envío**
```typescript id="e33kiw"
// Implementación sencilla basada en condicionales.
type ShippingType = 'standard' | 'express';
class ShippingCalculator {
calculate(type: ShippingType, orderValue: number): number {
if (type === 'standard') {
return orderValue < 50 ? 5 : 0;
}
return 15;
}
}
```
Esta implementación no es necesariamente incorrecta.
Si los tipos de envío son pocos y estables, puede ser la solución más sencilla y mantenible.
Sin embargo, cuando las políticas de envío evolucionan independientemente, puede resultar conveniente introducir abstracciones.
### 2.3. Alternativa extensible
```typescript id="0kb5qt"
interface ShippingMethod {
calculateCost(orderValue: number): number;
}
```
```typescript id="mky8hb"
class StandardShipping implements ShippingMethod {
calculateCost(orderValue: number): number {
return orderValue < 50 ? 5 : 0;
}
}
```
```typescript id="p9o7s1"
class ExpressShipping implements ShippingMethod {
calculateCost(_orderValue: number): number {
return 15;
}
}
```
Para incorporar un nuevo método:
```typescript id="bw5t7r"
class SameDayShipping implements ShippingMethod {
calculateCost(_orderValue: number): number {
return 25;
}
}
```
Un consumidor puede utilizar el contrato común:
```typescript id="d8t4vk"
function calculateShipping(
method: ShippingMethod,
orderValue: number,
): number {
return method.calculateCost(orderValue);
}
```
Este diseño permite incorporar nuevas políticas sin modificar el cálculo del consumidor.
No obstante, el mecanismo que selecciona o registra las implementaciones puede necesitar cambios según la arquitectura utilizada.
### 2.4. Aplicación pragmática
- Introduce puntos de extensión cuando exista una necesidad identificada.
- Mantén estables los contratos que tengan consumidores.
- Favorece composición y polimorfismo cuando reduzcan el acoplamiento.
- Evita crear interfaces para comportamientos que probablemente no variarán.
- No introduzcas extensibilidad hipotética.
- Acepta modificaciones de código existente cuando sean la solución más clara.
### 2.5. OCP a nivel arquitectónico
A nivel de arquitectura, OCP favorece la existencia de módulos con responsabilidades claras y contratos estables.
Esto permite incorporar funcionalidades nuevas con un impacto controlado.
Sin embargo, **no significa que toda funcionalidad nueva deba implementarse exclusivamente añadiendo archivos o clases**.
Modificar código existente es legítimo cuando cambian sus requisitos o responsabilidades.
El objetivo es minimizar modificaciones innecesarias en componentes estables, no prohibirlas.
---
## 3. L — Principio de sustitución de Liskov (Liskov Substitution Principle, LSP)
> "Los subtipos deben poder sustituir a sus tipos base sin alterar la corrección del programa."
### 3.1. Qué problema resuelve
LSP evita jerarquías o implementaciones que incumplen las expectativas establecidas por un contrato común.
Cuando se vulnera este principio, suelen aparecer:
- Comprobaciones del tipo concreto en los consumidores.
- Condicionales especiales para determinadas implementaciones.
- Errores inesperados al sustituir componentes.
- Contratos inconsistentes.
- Dependencias del comportamiento interno de cada implementación.
### 3.2. El contrato es más que una firma
Dos implementaciones no son necesariamente sustituibles por tener los mismos métodos.
La sustitución requiere respetar el comportamiento esperado, incluyendo:
- Precondiciones.
- Postcondiciones.
- Invariantes.
- Semántica de los resultados.
- Comportamiento ante errores.
- Garantías relevantes para los consumidores.
### 3.3. Ejemplo de incumplimiento
Supongamos que el contrato establece que un descuento nunca puede ser negativo.
```typescript id="j3zfk5"
interface DiscountPolicy {
getDiscount(orderValue: number): number;
}
```
Una implementación correcta:
```typescript id="tr7txy"
class StandardDiscount implements DiscountPolicy {
getDiscount(_orderValue: number): number {
return 5;
}
}
```
Una implementación incorrecta:
```typescript id="q0laxd"
class InvalidDiscount implements DiscountPolicy {
getDiscount(_orderValue: number): number {
return -5;
}
}
```
Aunque ambas cumplen la interfaz TypeScript, la segunda incumple el contrato semántico si este exige descuentos no negativos.
### 3.4. Protección del contrato
El contrato puede expresarse mediante documentación, tipos, validaciones y pruebas.
```typescript id="e1iu3o"
class FixedDiscount implements DiscountPolicy {
constructor(private readonly amount: number) {
if (!Number.isFinite(amount) || amount < 0) {
throw new InvalidDiscountError();
}
}
getDiscount(_orderValue: number): number {
return this.amount;
}
}
```
La validación impide crear una política de descuento fijo con un importe negativo.
No obstante, validar únicamente esta clase no garantiza que todas las implementaciones de `DiscountPolicy` cumplan el contrato.
Para ello pueden utilizarse pruebas de contrato compartidas y validaciones en los límites apropiados.
### 3.5. Reglas de sustitución
Una implementación sustitutiva debe:
- Respetar las invariantes del contrato.
- No exigir precondiciones más estrictas que las comprometidas por el tipo base.
- No proporcionar postcondiciones más débiles.
- Mantener el significado de los resultados.
- Respetar las garantías de error relevantes.
- No introducir efectos secundarios incompatibles con el contrato.
### 3.6. Ejemplo con repositorios
```typescript id="60hovx"
interface UserRepository {
findById(id: UserId): Promise<User | null>;
save(user: User): Promise<void>;
}
```
Las implementaciones:
```typescript id="kp45w8"
class InMemoryUserRepository implements UserRepository {
// Implementación en memoria.
}
```
```typescript id="q2v45i"
class PostgresUserRepository implements UserRepository {
// Implementación sobre PostgreSQL.
}
```
Pueden sustituirse cuando respetan el mismo contrato funcional.
Esto incluye:
- Significado de `null` cuando no existe el usuario.
- Garantías de persistencia.
- Restricciones de integridad.
- Tratamiento de errores.
- Comportamientos relevantes para los consumidores.
**Compartir una interfaz no garantiza automáticamente la sustituibilidad.**
---
## 4. I — Principio de segregación de interfaces (Interface Segregation Principle, ISP)
> "Los clientes no deberían verse obligados a depender de métodos que no utilizan."
### 4.1. Qué problema resuelve
ISP evita interfaces excesivamente amplias que obligan a los consumidores o implementaciones a conocer capacidades innecesarias.
Los síntomas habituales incluyen:
- Interfaces con numerosas operaciones independientes.
- Implementaciones parciales.
- Métodos vacíos.
- Excepciones de funcionalidad no soportada.
- Dependencias innecesarias.
- Cambios en interfaces que afectan a consumidores no relacionados.
### 4.2. Ejemplo problemático
```typescript id="d9qvm1"
interface WarehouseDevice {
printLabel(orderId: string): void;
scanBarcode(): string;
packageItem(orderId: string): void;
}
```
Una impresora sencilla no debería verse obligada a implementar operaciones que no admite.
```typescript id="z83zrv"
class BasicPrinter implements WarehouseDevice {
printLabel(_orderId: string): void {
// Imprimir etiqueta.
}
scanBarcode(): string {
throw new Error('Operación no soportada');
}
packageItem(_orderId: string): void {
throw new Error('Operación no soportada');
}
}
```
### 4.3. Alternativa con interfaces cohesionadas
```typescript id="80gu9m"
interface LabelPrinter {
printLabel(orderId: string): void;
}
```
```typescript id="e4lqq1"
interface BarcodeScanner {
scanBarcode(): string;
}
```
```typescript id="a113zj"
interface ItemPackager {
packageItem(orderId: string): void;
}
```
Una impresora implementa únicamente su capacidad real:
```typescript id="b9ksk8"
class BasicPrinter implements LabelPrinter {
printLabel(_orderId: string): void {
// Imprimir etiqueta.
}
}
```
Los consumidores pueden depender de las capacidades que necesitan.
### 4.4. Aplicación pragmática
- Diseña interfaces alrededor de necesidades reales de los consumidores.
- Agrupa operaciones que representen una responsabilidad coherente.
- Divide interfaces cuando obliguen a depender de capacidades no utilizadas.
- Evita crear una interfaz independiente para cada método sin una razón clara.
- No fragmentes contratos cohesionados únicamente para reducir su tamaño.
### 4.5. Señales de revisión
Revisa una interfaz cuando:
- Varias implementaciones contienen métodos vacíos.
- Algunas operaciones lanzan excepciones de no implementación de forma sistemática.
- Los consumidores utilizan subconjuntos completamente independientes.
- Los cambios afectan a componentes que no necesitan la funcionalidad modificada.
Sin embargo, un método opcional o una operación no soportada no demuestra por sí solo una vulneración de ISP.
Evalúa el contrato y el contexto antes de modificarlo.
**ISP trata de reducir dependencias innecesarias, no de maximizar el número de interfaces.**
---
## 5. D — Principio de inversión de dependencias (Dependency Inversion Principle, DIP)
> "Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones."
### 5.1. Qué problema resuelve
DIP busca evitar que las reglas de negocio queden acopladas innecesariamente a detalles técnicos.
Ejemplos:
- Bases de datos concretas.
- Proveedores de correo electrónico.
- APIs externas.
- Sistemas de almacenamiento.
- Frameworks.
- Implementaciones de infraestructura.
Este acoplamiento puede dificultar las pruebas, la evolución o la sustitución de componentes.
### 5.2. Ejemplo de dependencia concreta
```typescript id="r5k4a2"
class OrderService {
private readonly emailService = new SendGridEmailService();
async confirmOrder(email: string): Promise<void> {
await this.emailService.send(email, 'Pedido confirmado');
}
}
```
La clase conoce directamente al proveedor de correo electrónico.
Si cambia el proveedor, puede ser necesario modificar el servicio de aplicación.
### 5.3. Dependencia de una abstracción
Define un contrato que represente la capacidad necesaria.
```typescript id="k8p2ag"
interface EmailService {
send(to: string, message: string): Promise<void>;
}
```
El servicio depende del contrato:
```typescript id="5g7pvd"
class OrderService {
constructor(
private readonly emailService: EmailService,
) {}
async confirmOrder(email: string): Promise<void> {
await this.emailService.send(email, 'Pedido confirmado');
}
}
```
Las implementaciones concretas pueden variar:
```typescript id="gcl3yt"
const sendGridOrderService = new OrderService(
new SendGridEmailService(),
);
```
```typescript id="k85qhq"
const sesOrderService = new OrderService(
new SesEmailService(),
);
```
En una prueba puede utilizarse una implementación controlada:
```typescript id="f4yb6z"
const testOrderService = new OrderService(
new FakeEmailService(),
);
```
El servicio de aplicación no necesita conocer los detalles internos de cada proveedor.
### 5.4. Inversión de dependencias e inyección de dependencias
Son conceptos relacionados, pero diferentes.
**Inversión de dependencias (DIP):**
Principio de diseño que establece cómo deben organizarse las dependencias entre políticas y detalles.
**Inyección de dependencias (DI):**
Técnica mediante la cual un componente recibe sus colaboradores desde el exterior.
Puede realizarse mediante:
- Constructores.
- Parámetros.
- Factorías.
- Composición explícita.
- Contenedores de dependencias.
**Aplicar DIP no exige utilizar un contenedor de inyección de dependencias.**
### 5.5. Evitar abstracciones innecesarias
No es obligatorio crear una interfaz para cada clase.
Una dependencia concreta puede resultar apropiada cuando:
- El componente es sencillo y estable.
- No existe una necesidad real de sustitución.
- No introduce acoplamiento arquitectónico problemático.
- La abstracción adicional no aporta claridad ni capacidad de evolución.
- Se trata de un objeto de valor, una utilidad pura u otro componente concreto cuyo contrato es suficientemente estable.
La dependencia de abstracciones debe justificarse por su utilidad, no por el cumplimiento mecánico de DIP.
### 5.6. Regla de dependencias arquitectónicas
En arquitecturas limpias, hexagonales o equivalentes, las dependencias del código fuente deben orientarse hacia las políticas de alto nivel.
Representación simplificada:
```text id="y99n58"
Infraestructura → Aplicación → Dominio
exterior medio interior
```
Esta representación expresa la dirección deseada de las dependencias en una arquitectura organizada de esa forma.
En la práctica, la infraestructura puede implementar contratos definidos en la aplicación o el dominio.
Lo importante es que las reglas de negocio no dependan innecesariamente de los detalles de infraestructura.
### 5.7. Aplicación pragmática
- Mantén independientes las reglas de negocio respecto a detalles técnicos cuando resulte necesario.
- Define contratos en los límites donde aporten desacoplamiento.
- Utiliza inyección de dependencias cuando facilite la composición o las pruebas.
- No introduzcas interfaces y adaptadores para todas las operaciones.
- Evita contenedores complejos en aplicaciones que puedan resolverse mediante composición directa.
- Respeta la arquitectura existente salvo que exista una razón técnica o funcional para modificarla.
---
## 6. Aplicación de SOLID a nivel arquitectónico
Los principios SOLID pueden orientar el diseño de módulos, subsistemas y límites arquitectónicos.
No obstante, su aplicación a nivel de arquitectura requiere interpretar correctamente cada principio.
| Principio | Aplicación arquitectónica |
|---|---|
| **SRP** | Módulos y subsistemas con responsabilidades cohesionadas y motivos de cambio relacionados. |
| **OCP** | Contratos y puntos de extensión que permiten incorporar capacidades sin modificar innecesariamente componentes estables. |
| **LSP** | Implementaciones sustituibles que respetan los contratos funcionales y no funcionales relevantes. |
| **ISP** | Interfaces y contratos entre módulos adaptados a las necesidades reales de sus consumidores. |
| **DIP** | Separación entre políticas de negocio y detalles técnicos mediante dependencias orientadas adecuadamente. |
### 6.1. SRP en módulos y subsistemas
Un módulo puede contener numerosas clases y operaciones siempre que representen una responsabilidad coherente.
No debe interpretarse que cada módulo o contexto delimitado (*bounded context*) tenga que realizar una única operación.
### 6.2. OCP en arquitectura
Favorece módulos estables que puedan ampliarse mediante nuevos componentes cuando exista una necesidad real de extensión.
No significa que las funcionalidades nuevas deban implementarse exclusivamente añadiendo módulos.
### 6.3. LSP en contratos distribuidos
En sistemas distribuidos, compartir un contrato técnico no garantiza sustituibilidad.
Deben considerarse también:
- Semántica de las operaciones.
- Garantías de consistencia.
- Tratamiento de errores.
- Idempotencia cuando corresponda.
- Compatibilidad de versiones.
- Restricciones operativas relevantes.
Dos microservicios que exponen la misma API no son necesariamente intercambiables.
### 6.4. ISP entre módulos
Los contratos deben exponer las capacidades necesarias sin obligar a los consumidores a depender de funcionalidades ajenas.
Esto no exige crear un servicio o API independiente para cada operación.
### 6.5. DIP en arquitectura
Las reglas de negocio deben mantenerse desacopladas de detalles de infraestructura cuando esa separación facilite la evolución del sistema.
La arquitectura concreta debe adaptarse a los requisitos y complejidad reales.
**SOLID no obliga a utilizar Clean Architecture, arquitectura hexagonal o microservicios en cualquier proyecto.**
---
## 7. Errores habituales al aplicar SOLID
### Aplicar SRP como "una clase, un método"
Una responsabilidad puede incluir numerosas operaciones relacionadas.
Dividirlas indiscriminadamente puede reducir la cohesión.
### Aplicar OCP mediante abstracciones especulativas
No crees extensibilidad para requisitos hipotéticos.
Introduce puntos de extensión cuando exista una necesidad identificada.
### Considerar LSP únicamente como compatibilidad de tipos
Compartir una interfaz no garantiza cumplir el mismo contrato semántico.
Comprueba invariantes, resultados y expectativas.
### Aplicar ISP creando demasiadas interfaces
Las interfaces deben representar capacidades coherentes.
No dividas contratos únicamente para reducir el número de métodos.
### Aplicar DIP mediante interfaces para todas las clases
DIP no consiste en prohibir dependencias concretas.
Consiste en evitar que las políticas importantes dependan innecesariamente de detalles técnicos.
### Introducir patrones sin necesidad
SOLID no exige utilizar automáticamente:
- Repositorios.
- Factorías.
- Estrategias.
- Adaptadores.
- Contenedores de dependencias.
- Jerarquías de herencia.
Utiliza estos mecanismos cuando resuelvan un problema concreto.
### Refactorizar código estable sin justificación
No modifiques una implementación que funciona correctamente únicamente para ajustarla a una interpretación más rígida de SOLID.
Evalúa el beneficio esperado, el riesgo y el alcance de la tarea.
---
## 8. Referencia rápida
| Principio | Idea fundamental | Señal de revisión |
|---|---|---|
| **SRP** | Una responsabilidad coherente y motivos de cambio relacionados. | Clase que acumula responsabilidades independientes. |
| **OCP** | Permitir extensiones justificadas sin modificar innecesariamente código estable. | Variantes frecuentes que obligan a cambiar múltiples componentes. |
| **LSP** | Las implementaciones deben respetar el contrato común. | Comportamientos incompatibles o comprobaciones especiales por tipo. |
| **ISP** | Los consumidores dependen únicamente de las capacidades necesarias. | Interfaces amplias con operaciones irrelevantes o implementaciones parciales. |
| **DIP** | Evitar dependencias innecesarias entre políticas de alto nivel y detalles técnicos. | Reglas de negocio fuertemente acopladas a infraestructura concreta. |
---
## 9. Lista de comprobación SOLID
### Responsabilidad única (SRP)
- [ ] ¿Cada clase o módulo tiene una responsabilidad coherente?
- [ ] ¿Existen motivos de cambio independientes que justifiquen una separación?
- [ ] ¿He evitado fragmentar responsabilidades relacionadas?
- [ ] ¿La organización facilita comprender y mantener el código?
### Abierto/cerrado (OCP)
- [ ] ¿Existe una necesidad real de extensión?
- [ ] ¿Puedo incorporar variantes sin modificar innecesariamente código estable?
- [ ] ¿Las abstracciones introducidas están justificadas?
- [ ] ¿He evitado diseñar para requisitos hipotéticos?
### Sustitución de Liskov (LSP)
- [ ] ¿Las implementaciones respetan el contrato común?
- [ ] ¿Se mantienen las invariantes?
- [ ] ¿Las precondiciones y postcondiciones son compatibles?
- [ ] ¿Los consumidores pueden utilizar otra implementación sin tratamientos especiales?
- [ ] ¿Las diferencias de comportamiento están correctamente definidas?
### Segregación de interfaces (ISP)
- [ ] ¿Cada interfaz representa capacidades coherentes?
- [ ] ¿Los consumidores dependen de operaciones que realmente necesitan?
- [ ] ¿Existen implementaciones forzadas a soportar operaciones irrelevantes?
- [ ] ¿He evitado fragmentar interfaces artificialmente?
### Inversión de dependencias (DIP)
- [ ] ¿Las reglas de negocio dependen innecesariamente de detalles técnicos?
- [ ] ¿Las abstracciones están situadas en límites apropiados?
- [ ] ¿La inyección de dependencias aporta ventajas concretas?
- [ ] ¿He evitado interfaces y adaptadores innecesarios?
- [ ] ¿Se respeta la arquitectura existente y sus contratos?
---
## Principio final
> "Los principios SOLID deben facilitar el cambio, reducir el acoplamiento y mejorar la comprensión del software, no convertirse en una obligación de introducir más clases e interfaces."
Aplica SOLID con criterio profesional, considerando las necesidades del dominio, el tamaño del sistema, sus requisitos de evolución y el coste de mantenimiento.
**Una solución sencilla, cohesionada y correctamente desacoplada es preferible a una arquitectura artificialmente compleja que únicamente aparenta cumplir SOLID.**