804 lines
25 KiB
Markdown
804 lines
25 KiB
Markdown
|
|
# 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.**
|