930 lines
30 KiB
Markdown
930 lines
30 KiB
Markdown
|
|
# Diseño orientado a objetos (Object-Oriented Design)
|
||
|
|
|
||
|
|
El diseño orientado a objetos permite organizar el software mediante componentes que encapsulan estado, comportamiento y responsabilidades.
|
||
|
|
|
||
|
|
Su objetivo es facilitar la comprensión, evolución y mantenimiento del sistema mediante objetos cohesionados, contratos claros y dependencias controladas.
|
||
|
|
|
||
|
|
Este documento desarrolla los principios establecidos en el `SKILL.md` principal y debe interpretarse siguiendo sus criterios de aplicación.
|
||
|
|
|
||
|
|
**El objetivo no es maximizar el número de clases, interfaces o patrones, sino encontrar una organización sencilla y coherente con el dominio y los requisitos reales.**
|
||
|
|
|
||
|
|
## 1. Diseño guiado por responsabilidades (Responsibility-Driven Design, RDD)
|
||
|
|
|
||
|
|
La idea fundamental del diseño guiado por responsabilidades es:
|
||
|
|
|
||
|
|
> "Los objetos se definen principalmente por sus responsabilidades, no por los datos que almacenan."
|
||
|
|
|
||
|
|
Una responsabilidad describe aquello que un objeto conoce, hace o decide dentro del sistema.
|
||
|
|
|
||
|
|
### 1.1. Identificación de objetos
|
||
|
|
|
||
|
|
Al analizar requisitos, utiliza las siguientes pistas:
|
||
|
|
|
||
|
|
1. **Sustantivos:** pueden sugerir entidades, objetos de valor o conceptos del dominio.
|
||
|
|
2. **Verbos:** pueden sugerir operaciones, comportamientos o casos de uso.
|
||
|
|
3. **Conceptos del dominio:** pueden justificar objetos con identidad, objetos de valor o servicios especializados.
|
||
|
|
|
||
|
|
Por ejemplo, ante el requisito:
|
||
|
|
|
||
|
|
"Un cliente puede realizar un pedido que contiene productos y al que se pueden aplicar descuentos."
|
||
|
|
|
||
|
|
Podrían identificarse:
|
||
|
|
|
||
|
|
- `Customer`: cliente.
|
||
|
|
- `Order`: pedido.
|
||
|
|
- `OrderItem`: línea de pedido.
|
||
|
|
- `Product`: producto.
|
||
|
|
- `DiscountPolicy`: política de descuentos.
|
||
|
|
|
||
|
|
Sin embargo, **no conviertas automáticamente cada sustantivo en una clase ni cada verbo en un método**.
|
||
|
|
|
||
|
|
Comprueba que los elementos identificados representan conceptos reales y responsabilidades necesarias.
|
||
|
|
|
||
|
|
### 1.2. Identificación de responsabilidades
|
||
|
|
|
||
|
|
Para cada objeto, considera:
|
||
|
|
|
||
|
|
- **¿Qué conoce?** Información o estado del que es responsable.
|
||
|
|
- **¿Qué hace?** Operaciones o comportamientos que proporciona.
|
||
|
|
- **¿Qué decide?** Reglas o decisiones que le corresponden.
|
||
|
|
- **¿Qué invariantes protege?** Condiciones que deben mantenerse válidas.
|
||
|
|
- **¿De quién depende?** Colaboradores necesarios para cumplir su responsabilidad.
|
||
|
|
|
||
|
|
El objetivo es asignar cada comportamiento al componente que tenga la información y responsabilidad adecuadas.
|
||
|
|
|
||
|
|
### 1.3. Estereotipos de objetos
|
||
|
|
|
||
|
|
Los estereotipos permiten identificar responsabilidades habituales.
|
||
|
|
|
||
|
|
| Estereotipo | Propósito | Ejemplo |
|
||
|
|
|---|---|---|
|
||
|
|
| **Information Holder (Contenedor de información)** | Mantiene información o estado del dominio. | `User`, `Product`, `Address` |
|
||
|
|
| **Structurer (Estructurador)** | Gestiona relaciones entre objetos. | `OrderItems`, `UserGroup` |
|
||
|
|
| **Service Provider (Proveedor de servicios)** | Ejecuta operaciones o proporciona capacidades específicas. | `PaymentProcessor`, `EmailSender` |
|
||
|
|
| **Coordinator (Coordinador)** | Orquesta procesos y la colaboración entre componentes. | `OrderFulfillmentService` |
|
||
|
|
| **Controller (Controlador)** | Recibe solicitudes, gestiona decisiones de control y delega operaciones. | `CheckoutController` |
|
||
|
|
| **Interfacer (Adaptador de interfaces)** | Adapta información o contratos entre sistemas. | `UserApiAdapter`, `DatabaseMapper` |
|
||
|
|
|
||
|
|
Estos estereotipos son orientativos.
|
||
|
|
|
||
|
|
Una clase puede combinar responsabilidades compatibles sin que ello implique necesariamente un problema de diseño.
|
||
|
|
|
||
|
|
Tampoco es obligatorio que toda clase encaje perfectamente en una de estas categorías.
|
||
|
|
|
||
|
|
### 1.4. Preguntas fundamentales
|
||
|
|
|
||
|
|
Para cada clase, módulo o componente, pregúntate:
|
||
|
|
|
||
|
|
1. ¿Cuál es su responsabilidad principal?
|
||
|
|
2. ¿Qué motivos justificarían modificarlo?
|
||
|
|
3. ¿Está asumiendo responsabilidades independientes?
|
||
|
|
4. ¿Sus dependencias son necesarias?
|
||
|
|
5. ¿Existe una organización más sencilla?
|
||
|
|
6. ¿Representa correctamente un concepto o comportamiento del dominio?
|
||
|
|
|
||
|
|
Si las respuestas no resultan claras, revisa el diseño.
|
||
|
|
|
||
|
|
**No refactorices automáticamente una clase porque no corresponda a un patrón conocido o porque supere una métrica numérica.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Tell, Don't Ask (Ordena, no preguntes)
|
||
|
|
|
||
|
|
Este principio propone delegar el comportamiento en los objetos responsables, en lugar de consultar su estado interno y tomar decisiones desde el exterior.
|
||
|
|
|
||
|
|
### Ejemplo: retirada de dinero
|
||
|
|
|
||
|
|
**Menos adecuado: lógica de negocio fuera del objeto**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// MAL: El consumidor modifica directamente el saldo.
|
||
|
|
if (account.getBalance() >= amount) {
|
||
|
|
account.setBalance(account.getBalance() - amount);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Este enfoque permite que distintos consumidores implementen reglas diferentes y puede comprometer las invariantes de la cuenta.
|
||
|
|
|
||
|
|
**Más adecuado: comportamiento encapsulado**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const result = account.withdraw(amount);
|
||
|
|
|
||
|
|
if (result.isSuccess()) {
|
||
|
|
// Continuar con la operación.
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
La cuenta es responsable de comprobar si la retirada está permitida y actualizar su estado correctamente.
|
||
|
|
|
||
|
|
### Criterios de aplicación
|
||
|
|
|
||
|
|
- Encapsula las reglas de negocio junto al estado que deben proteger.
|
||
|
|
- Evita que los consumidores reproduzcan decisiones propias de un objeto.
|
||
|
|
- Expón operaciones que expresen intenciones del dominio.
|
||
|
|
- Reduce la necesidad de consultar y modificar repetidamente el estado interno.
|
||
|
|
|
||
|
|
### Excepciones legítimas
|
||
|
|
|
||
|
|
No apliques *Tell, Don't Ask* indiscriminadamente.
|
||
|
|
|
||
|
|
Consultar datos es adecuado cuando se trabaja con:
|
||
|
|
|
||
|
|
- DTO.
|
||
|
|
- Modelos de presentación.
|
||
|
|
- Respuestas de API.
|
||
|
|
- Objetos de configuración.
|
||
|
|
- Estructuras destinadas al transporte de datos.
|
||
|
|
- Consultas o proyecciones de solo lectura.
|
||
|
|
|
||
|
|
**El principio debe proteger el encapsulamiento y las invariantes, no impedir el acceso legítimo a la información.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Diseño por contrato (Design by Contract, DbC)
|
||
|
|
|
||
|
|
El diseño por contrato establece las condiciones que deben cumplirse antes, durante y después de ejecutar una operación.
|
||
|
|
|
||
|
|
### Elementos de un contrato
|
||
|
|
|
||
|
|
**Precondiciones:** condiciones que deben cumplirse antes de ejecutar una operación.
|
||
|
|
|
||
|
|
**Postcondiciones:** garantías que proporciona una operación al completarse correctamente.
|
||
|
|
|
||
|
|
**Invariantes:** condiciones que deben mantenerse válidas durante la vida observable del objeto.
|
||
|
|
|
||
|
|
### Ejemplo
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class BankAccount {
|
||
|
|
constructor(private balance: Money) {
|
||
|
|
if (balance.isNegative()) {
|
||
|
|
throw new InvalidInitialBalanceError();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
withdraw(amount: Money): WithdrawResult {
|
||
|
|
if (!amount.isPositive()) {
|
||
|
|
return WithdrawResult.invalidAmount();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.balance.isLessThan(amount)) {
|
||
|
|
return WithdrawResult.insufficientFunds();
|
||
|
|
}
|
||
|
|
|
||
|
|
this.balance = this.balance.subtract(amount);
|
||
|
|
|
||
|
|
return WithdrawResult.success(this.balance);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
En este ejemplo:
|
||
|
|
|
||
|
|
- **Invariante:** el saldo nunca puede ser negativo.
|
||
|
|
- **Precondición de éxito:** el importe debe ser positivo y no superar el saldo disponible.
|
||
|
|
- **Postcondición de éxito:** el saldo disminuye exactamente en el importe retirado.
|
||
|
|
- **Resultado de rechazo:** se devuelve un resultado que identifica el motivo sin modificar el saldo.
|
||
|
|
|
||
|
|
El ejemplo presupone que `Money` impide operaciones entre monedas incompatibles y utiliza una representación monetaria adecuada.
|
||
|
|
|
||
|
|
### Criterios de diseño por contrato
|
||
|
|
|
||
|
|
- Define las invariantes relevantes del dominio.
|
||
|
|
- Valida las precondiciones en los límites apropiados.
|
||
|
|
- Evita que las operaciones dejen objetos en estados inválidos.
|
||
|
|
- Expresa claramente los resultados esperados.
|
||
|
|
- Utiliza tipos para representar restricciones cuando sea apropiado.
|
||
|
|
- No dupliques validaciones innecesariamente en todas las capas.
|
||
|
|
- Distingue entre errores excepcionales y rechazos de negocio esperados.
|
||
|
|
|
||
|
|
No es obligatorio documentar expresamente cada precondición y postcondición cuando el contrato resulte evidente mediante los tipos, nombres y pruebas.
|
||
|
|
|
||
|
|
### Contratos y compatibilidad
|
||
|
|
|
||
|
|
Al modificar una operación existente:
|
||
|
|
|
||
|
|
- Identifica sus consumidores.
|
||
|
|
- Mantén las garantías previamente establecidas.
|
||
|
|
- No debilites invariantes sin una justificación funcional.
|
||
|
|
- Evita cambios silenciosos en el tratamiento de errores.
|
||
|
|
- Comprueba el principio de sustitución de Liskov cuando exista herencia o implementación de interfaces.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Composición frente a herencia (Composition Over Inheritance)
|
||
|
|
|
||
|
|
La composición permite construir comportamientos mediante la colaboración entre objetos.
|
||
|
|
|
||
|
|
La herencia permite especializar tipos y reutilizar contratos o comportamientos comunes.
|
||
|
|
|
||
|
|
**Prefiere composición cuando aporte mayor flexibilidad, menor acoplamiento o una representación más adecuada del dominio.**
|
||
|
|
|
||
|
|
### 4.1. Riesgos de la herencia
|
||
|
|
|
||
|
|
- Acoplamiento entre clases base y derivadas.
|
||
|
|
- Dependencia de detalles de implementación heredados.
|
||
|
|
- Dificultad para modificar clases base sin afectar a sus derivados.
|
||
|
|
- Jerarquías difíciles de comprender.
|
||
|
|
- Relaciones artificiales de tipo "es un".
|
||
|
|
- Problemas de sustitución cuando las clases derivadas alteran contratos.
|
||
|
|
|
||
|
|
Estos riesgos no implican que la herencia sea incorrecta por definición.
|
||
|
|
|
||
|
|
### 4.2. Cuándo utilizar herencia
|
||
|
|
|
||
|
|
Puede ser apropiada cuando:
|
||
|
|
|
||
|
|
- Existe una relación conceptual real de especialización.
|
||
|
|
- Las clases derivadas cumplen el contrato del tipo base.
|
||
|
|
- El framework requiere una jerarquía concreta.
|
||
|
|
- Se utiliza deliberadamente un patrón como *Template Method*.
|
||
|
|
- La reutilización y extensión del comportamiento justifican la estructura.
|
||
|
|
|
||
|
|
No utilices herencia únicamente para compartir unas pocas líneas de implementación.
|
||
|
|
|
||
|
|
### 4.3. Composición mediante políticas
|
||
|
|
|
||
|
|
**Ejemplo basado en herencia:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class PremiumUser extends User {
|
||
|
|
getDiscount(): number {
|
||
|
|
return 20;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Este diseño puede resultar válido, pero vincula el comportamiento del descuento con la jerarquía de usuarios.
|
||
|
|
|
||
|
|
**Alternativa mediante composición:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface DiscountPolicy {
|
||
|
|
calculate(): number;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class User {
|
||
|
|
constructor(
|
||
|
|
private readonly discountPolicy: DiscountPolicy,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
getDiscount(): number {
|
||
|
|
return this.discountPolicy.calculate();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class PremiumDiscount implements DiscountPolicy {
|
||
|
|
calculate(): number {
|
||
|
|
return 20;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class StandardDiscount implements DiscountPolicy {
|
||
|
|
calculate(): number {
|
||
|
|
return 5;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class NoDiscount implements DiscountPolicy {
|
||
|
|
calculate(): number {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const premiumUser = new User(new PremiumDiscount());
|
||
|
|
const standardUser = new User(new StandardDiscount());
|
||
|
|
const regularUser = new User(new NoDiscount());
|
||
|
|
```
|
||
|
|
|
||
|
|
La composición permite variar la política sin crear nuevas subclases de usuario.
|
||
|
|
|
||
|
|
### Criterios para elegir
|
||
|
|
|
||
|
|
- Utiliza composición cuando necesites combinar o sustituir comportamientos independientes.
|
||
|
|
- Utiliza herencia cuando exista una especialización legítima con sustitución segura.
|
||
|
|
- No introduzcas una interfaz y varias clases si un condicional sencillo resuelve adecuadamente un comportamiento estable.
|
||
|
|
- Evita crear estrategias intercambiables para variaciones hipotéticas.
|
||
|
|
- Considera funciones y objetos sencillos como alternativas a jerarquías complejas.
|
||
|
|
|
||
|
|
**La composición es una preferencia de diseño, no una prohibición de utilizar herencia.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Ley de Deméter (Law of Demeter)
|
||
|
|
|
||
|
|
La Ley de Deméter, también conocida como principio de mínimo conocimiento, propone limitar las dependencias de un componente respecto a las estructuras internas de otros.
|
||
|
|
|
||
|
|
De manera orientativa, un método debería colaborar principalmente con:
|
||
|
|
|
||
|
|
1. El propio objeto (`this`).
|
||
|
|
2. Los parámetros recibidos.
|
||
|
|
3. Los objetos que crea.
|
||
|
|
4. Sus colaboradores directos.
|
||
|
|
|
||
|
|
### Ejemplo
|
||
|
|
|
||
|
|
**Acoplamiento a estructuras internas:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const city = order.getCustomer().getAddress().getCity();
|
||
|
|
```
|
||
|
|
|
||
|
|
Este código conoce la organización interna del pedido, el cliente y su dirección.
|
||
|
|
|
||
|
|
**Alternativa con una operación de dominio:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const city = order.getShippingCity();
|
||
|
|
```
|
||
|
|
|
||
|
|
La segunda alternativa puede reducir el acoplamiento cuando la ciudad de envío sea una responsabilidad del pedido.
|
||
|
|
|
||
|
|
### Interpretación correcta
|
||
|
|
|
||
|
|
La Ley de Deméter no significa que deba existir un único punto por línea.
|
||
|
|
|
||
|
|
No prohíbe:
|
||
|
|
|
||
|
|
- APIs fluidas.
|
||
|
|
- Encadenamiento de operaciones sobre colecciones.
|
||
|
|
- Acceso a estructuras de datos públicas.
|
||
|
|
- Navegación legítima por DTO.
|
||
|
|
- Composición de operaciones sobre valores inmutables.
|
||
|
|
|
||
|
|
### Criterios de aplicación
|
||
|
|
|
||
|
|
- Evita que una clase dependa de detalles internos innecesarios de sus colaboradores.
|
||
|
|
- Prefiere operaciones que expresen comportamientos del dominio.
|
||
|
|
- No añadas métodos delegadores triviales únicamente para ocultar una cadena de acceso.
|
||
|
|
- Evalúa si la navegación introduce un acoplamiento real.
|
||
|
|
- Prioriza la claridad y la estabilidad de los contratos.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Encapsulamiento (Encapsulation)
|
||
|
|
|
||
|
|
El encapsulamiento consiste en controlar el acceso al estado y a los detalles internos de un componente, exponiendo únicamente los contratos necesarios.
|
||
|
|
|
||
|
|
### 6.1. Niveles de encapsulamiento
|
||
|
|
|
||
|
|
1. **Datos:** controlar el acceso y modificación del estado.
|
||
|
|
2. **Implementación:** ocultar los mecanismos internos.
|
||
|
|
3. **Tipos:** desacoplar consumidores de implementaciones concretas cuando sea necesario.
|
||
|
|
4. **Diseño:** ocultar decisiones arquitectónicas que no deban afectar a los consumidores.
|
||
|
|
|
||
|
|
No todos los componentes necesitan aplicar todos estos niveles.
|
||
|
|
|
||
|
|
### 6.2. Protección de invariantes
|
||
|
|
|
||
|
|
**Ejemplo problemático:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class Order {
|
||
|
|
public items: Item[] = [];
|
||
|
|
public total = 0;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Un consumidor puede modificar el estado sin respetar las reglas del pedido.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
order.items.push(item);
|
||
|
|
order.total = -999;
|
||
|
|
```
|
||
|
|
|
||
|
|
**Alternativa encapsulada:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class Order {
|
||
|
|
private readonly items: Item[] = [];
|
||
|
|
private total: Money;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.total = Money.zero('EUR');
|
||
|
|
}
|
||
|
|
|
||
|
|
addItem(item: Item): void {
|
||
|
|
const nextTotal = this.total.add(item.total);
|
||
|
|
|
||
|
|
this.validateTotal(nextTotal);
|
||
|
|
|
||
|
|
this.items.push(item);
|
||
|
|
this.total = nextTotal;
|
||
|
|
}
|
||
|
|
|
||
|
|
getTotal(): Money {
|
||
|
|
return this.total;
|
||
|
|
}
|
||
|
|
|
||
|
|
private validateTotal(total: Money): void {
|
||
|
|
if (total.isNegative()) {
|
||
|
|
throw new InvalidOrderTotalError();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Este ejemplo presupone que `Money` es inmutable y que las operaciones utilizadas preservan sus invariantes.
|
||
|
|
|
||
|
|
### Consideraciones
|
||
|
|
|
||
|
|
- Mantén privadas las estructuras internas cuyo acceso pueda comprometer invariantes.
|
||
|
|
- Expón operaciones de dominio para modificar estados protegidos.
|
||
|
|
- Utiliza propiedades de solo lectura cuando sea adecuado.
|
||
|
|
- Evita devolver referencias mutables que permitan modificar indirectamente el estado.
|
||
|
|
- No introduzcas getters o métodos delegadores innecesarios.
|
||
|
|
- No encapsules indiscriminadamente datos cuyo propósito legítimo sea ser públicos.
|
||
|
|
|
||
|
|
### Encapsulamiento e inmutabilidad
|
||
|
|
|
||
|
|
La inmutabilidad facilita razonar sobre el estado y evita modificaciones inesperadas.
|
||
|
|
|
||
|
|
Prefiere valores inmutables cuando resulte apropiado, especialmente para objetos de valor.
|
||
|
|
|
||
|
|
No obstante, las entidades y otros objetos pueden mantener estado mutable siempre que las modificaciones estén controladas.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. Polimorfismo (Polymorphism)
|
||
|
|
|
||
|
|
El polimorfismo permite utilizar distintas implementaciones mediante un contrato común.
|
||
|
|
|
||
|
|
Puede mejorar la extensibilidad cuando existen comportamientos alternativos bien definidos.
|
||
|
|
|
||
|
|
**No debe utilizarse indiscriminadamente para sustituir todos los condicionales.**
|
||
|
|
|
||
|
|
### 7.1. Implementación mediante condicionales
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
type ShippingType = 'standard' | 'express' | 'overnight';
|
||
|
|
|
||
|
|
function calculateShipping(
|
||
|
|
method: ShippingType,
|
||
|
|
orderValue: number,
|
||
|
|
): number {
|
||
|
|
if (method === 'standard') {
|
||
|
|
return orderValue < 50 ? 5 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (method === 'express') {
|
||
|
|
return 15;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 25;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Esta implementación es sencilla y puede ser perfectamente adecuada cuando los métodos de envío son pocos y estables.
|
||
|
|
|
||
|
|
### 7.2. Alternativa polimórfica
|
||
|
|
|
||
|
|
Cuando los métodos de envío incorporan reglas propias y evolucionan independientemente, puede resultar útil definir un contrato. Las implementaciones canónicas de `StandardShipping`, `ExpressShipping` y el consumidor `calculateShipping` se mantienen en [principios-solid.md](principios-solid.md), sección 2.3. Aquí se muestra únicamente una variante adicional para evitar mantener copias del mismo código.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface ShippingMethod {
|
||
|
|
calculateCost(orderValue: number): number;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class OvernightShipping implements ShippingMethod {
|
||
|
|
calculateCost(_orderValue: number): number {
|
||
|
|
return 25;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7.3. Cuándo utilizar polimorfismo
|
||
|
|
|
||
|
|
Considera este enfoque cuando:
|
||
|
|
|
||
|
|
- Existan varias implementaciones con comportamiento significativo.
|
||
|
|
- Cada alternativa tenga reglas propias.
|
||
|
|
- Sea necesario incorporar nuevos comportamientos sin modificar consumidores.
|
||
|
|
- Los condicionales relacionados aparezcan repetidamente.
|
||
|
|
- Las implementaciones deban probarse o evolucionar de forma independiente.
|
||
|
|
|
||
|
|
Evítalo cuando:
|
||
|
|
|
||
|
|
- El comportamiento sea muy sencillo.
|
||
|
|
- Las alternativas sean pocas y estables.
|
||
|
|
- La jerarquía incremente innecesariamente el número de componentes.
|
||
|
|
- El contrato común sea artificial.
|
||
|
|
- Una función o un mapa de operaciones proporcione una solución más clara.
|
||
|
|
|
||
|
|
**El polimorfismo debe reducir el acoplamiento o simplificar la evolución, no limitarse a eliminar estructuras `if` o `switch`.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. Objetos de valor y entidades (Value Objects vs Entities)
|
||
|
|
|
||
|
|
Los objetos de valor y las entidades representan conceptos diferentes dentro del modelo de dominio.
|
||
|
|
|
||
|
|
### 8.1. Objetos de valor (Value Objects)
|
||
|
|
|
||
|
|
Los objetos de valor:
|
||
|
|
|
||
|
|
- Se definen por sus atributos y significado.
|
||
|
|
- No poseen una identidad independiente relevante para el dominio.
|
||
|
|
- Deben ser inmutables desde la perspectiva de sus consumidores.
|
||
|
|
- Se comparan por sus valores.
|
||
|
|
- Pueden encapsular validaciones, invariantes y operaciones.
|
||
|
|
|
||
|
|
Ejemplos habituales:
|
||
|
|
|
||
|
|
- `Money`
|
||
|
|
- `Email`
|
||
|
|
- `Address`
|
||
|
|
- `DateRange`
|
||
|
|
|
||
|
|
### Ejemplo: Money
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class Money {
|
||
|
|
constructor(
|
||
|
|
private readonly amount: number,
|
||
|
|
private readonly currency: string,
|
||
|
|
) {
|
||
|
|
if (!Number.isFinite(amount)) {
|
||
|
|
throw new InvalidMoneyAmountError();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
equals(other: Money): boolean {
|
||
|
|
return (
|
||
|
|
this.amount === other.amount &&
|
||
|
|
this.currency === other.currency
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
add(other: Money): Money {
|
||
|
|
if (this.currency !== other.currency) {
|
||
|
|
throw new CurrencyMismatchError();
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Money(
|
||
|
|
this.amount + other.amount,
|
||
|
|
this.currency,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Este ejemplo ilustra la igualdad por valor y la inmutabilidad, pero no constituye una implementación monetaria completa.
|
||
|
|
|
||
|
|
En sistemas de producción, evita errores de precisión monetaria utilizando una representación adecuada, como unidades mínimas enteras o tipos decimales.
|
||
|
|
|
||
|
|
### Criterios para utilizar objetos de valor
|
||
|
|
|
||
|
|
- Introduce objetos de valor cuando exista semántica de dominio.
|
||
|
|
- Encapsula validaciones e invariantes relevantes.
|
||
|
|
- Evita utilizar tipos primitivos intercambiables cuando puedan provocar errores.
|
||
|
|
- No crees objetos de valor para cualquier cadena o número.
|
||
|
|
- Considera tipos nominales de TypeScript cuando únicamente necesites diferenciar identificadores.
|
||
|
|
- No introduzcas abstracciones que no aporten seguridad, comportamiento o expresividad.
|
||
|
|
|
||
|
|
### 8.2. Entidades (Entities)
|
||
|
|
|
||
|
|
Las entidades:
|
||
|
|
|
||
|
|
- Poseen una identidad que permanece aunque cambien sus atributos.
|
||
|
|
- Normalmente permiten modificar su estado mediante operaciones controladas.
|
||
|
|
- Se comparan por identidad cuando corresponde.
|
||
|
|
- Pueden encapsular reglas e invariantes del dominio.
|
||
|
|
|
||
|
|
Ejemplos habituales:
|
||
|
|
|
||
|
|
- `User`
|
||
|
|
- `Order`
|
||
|
|
- `Product`
|
||
|
|
|
||
|
|
### Ejemplo: User
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class User {
|
||
|
|
constructor(
|
||
|
|
private readonly id: UserId,
|
||
|
|
private email: Email,
|
||
|
|
private name: Name,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
equals(other: User): boolean {
|
||
|
|
return this.id.equals(other.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
changeEmail(newEmail: Email): void {
|
||
|
|
this.email = newEmail;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
El usuario conserva su identidad aunque cambie su dirección de correo electrónico.
|
||
|
|
|
||
|
|
La operación `changeEmail` podría incorporar reglas adicionales, como comprobar restricciones de negocio, según las responsabilidades del modelo.
|
||
|
|
|
||
|
|
### Diferencia fundamental
|
||
|
|
|
||
|
|
| Característica | Objeto de valor | Entidad |
|
||
|
|
|---|---|---|
|
||
|
|
| Identidad propia | No | Sí |
|
||
|
|
| Comparación | Por valor | Por identidad |
|
||
|
|
| Mutabilidad | Preferentemente inmutable | Puede modificar su estado |
|
||
|
|
| Responsabilidad | Representar un concepto mediante sus valores | Representar un concepto con continuidad e identidad |
|
||
|
|
| Ejemplos | `Money`, `Email` | `User`, `Order` |
|
||
|
|
|
||
|
|
No conviertas automáticamente todos los datos en entidades u objetos de valor.
|
||
|
|
|
||
|
|
Las estructuras de transporte de datos también tienen un papel legítimo dentro de una arquitectura.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. Agregados (Aggregates)
|
||
|
|
|
||
|
|
Un agregado es un conjunto de entidades y objetos de valor que se trata como una unidad de consistencia dentro de un dominio.
|
||
|
|
|
||
|
|
Este concepto procede del diseño guiado por el dominio (*Domain-Driven Design*, DDD).
|
||
|
|
|
||
|
|
### 9.1. Elementos de un agregado
|
||
|
|
|
||
|
|
**Raíz del agregado (Aggregate Root):**
|
||
|
|
|
||
|
|
- Constituye el punto principal de acceso al agregado.
|
||
|
|
- Protege sus invariantes.
|
||
|
|
- Controla las modificaciones de los elementos internos.
|
||
|
|
- Define los límites de consistencia.
|
||
|
|
|
||
|
|
**Elementos internos:**
|
||
|
|
|
||
|
|
- Pueden incluir entidades y objetos de valor.
|
||
|
|
- Participan en las reglas del agregado.
|
||
|
|
- No deberían modificarse desde el exterior eludiendo la raíz.
|
||
|
|
|
||
|
|
### 9.2. Ejemplo: pedido
|
||
|
|
|
||
|
|
Supongamos que un pedido tiene un importe máximo permitido.
|
||
|
|
|
||
|
|
La raíz del agregado debe impedir que una operación deje el pedido en un estado inválido.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class Order {
|
||
|
|
private items: OrderItem[] = [];
|
||
|
|
|
||
|
|
addItem(product: Product, quantity: number): void {
|
||
|
|
const item = new OrderItem(product, quantity);
|
||
|
|
|
||
|
|
const nextItems = [...this.items, item];
|
||
|
|
const nextTotal = this.calculateTotal(nextItems);
|
||
|
|
|
||
|
|
this.validateTotal(nextTotal);
|
||
|
|
|
||
|
|
this.items = nextItems;
|
||
|
|
}
|
||
|
|
|
||
|
|
removeItem(itemId: ItemId): void {
|
||
|
|
const nextItems = this.items.filter(
|
||
|
|
(item) => !item.id.equals(itemId),
|
||
|
|
);
|
||
|
|
|
||
|
|
const nextTotal = this.calculateTotal(nextItems);
|
||
|
|
|
||
|
|
this.validateTotal(nextTotal);
|
||
|
|
|
||
|
|
this.items = nextItems;
|
||
|
|
}
|
||
|
|
|
||
|
|
private calculateTotal(items: OrderItem[]): Money {
|
||
|
|
return items.reduce(
|
||
|
|
(total, item) => total.add(item.total),
|
||
|
|
Money.zero('EUR'),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private validateTotal(total: Money): void {
|
||
|
|
if (total.isGreaterThan(MAX_ORDER_VALUE)) {
|
||
|
|
throw new OrderTotalExceededError();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
El ejemplo presupone que los importes utilizan una moneda compatible y que `OrderItem` valida la cantidad y calcula correctamente su importe.
|
||
|
|
|
||
|
|
### 9.3. Preservación de invariantes
|
||
|
|
|
||
|
|
Observa que el ejemplo calcula el nuevo estado antes de modificar la colección.
|
||
|
|
|
||
|
|
El orden es importante:
|
||
|
|
|
||
|
|
1. Preparar el estado propuesto.
|
||
|
|
2. Calcular sus consecuencias.
|
||
|
|
3. Validar las invariantes.
|
||
|
|
4. Aplicar la modificación.
|
||
|
|
|
||
|
|
De este modo, si la validación falla, el estado del pedido permanece sin cambios.
|
||
|
|
|
||
|
|
**No modifiques primero el agregado para validar después, salvo que exista un mecanismo fiable que garantice la reversión del estado.**
|
||
|
|
|
||
|
|
### 9.4. Acceso a través de la raíz
|
||
|
|
|
||
|
|
**Incorrecto: modificación externa que evita las reglas**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
order.items.push(new OrderItem(product, 2));
|
||
|
|
```
|
||
|
|
|
||
|
|
Este acceso no debe estar disponible si permite incumplir las invariantes del agregado.
|
||
|
|
|
||
|
|
**Correcto: operación controlada**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
order.addItem(product, 2);
|
||
|
|
```
|
||
|
|
|
||
|
|
La raíz aplica las reglas necesarias antes de modificar el estado.
|
||
|
|
|
||
|
|
### 9.5. Criterios para diseñar agregados
|
||
|
|
|
||
|
|
- Define agregados alrededor de invariantes que deban mantenerse consistentes.
|
||
|
|
- Utiliza raíces de agregado cuando exista una necesidad real de controlar modificaciones conjuntas.
|
||
|
|
- Evita agregados excesivamente grandes.
|
||
|
|
- No incluyas entidades dentro de un mismo agregado únicamente porque estén relacionadas.
|
||
|
|
- Considera el coste transaccional y la concurrencia.
|
||
|
|
- Mantén referencias entre agregados mediante identificadores cuando sea apropiado.
|
||
|
|
- Distingue las operaciones que necesitan consistencia inmediata de las que admiten consistencia eventual.
|
||
|
|
- No introduzcas agregados DDD para modelos sencillos que no necesiten este nivel de organización.
|
||
|
|
|
||
|
|
### 9.6. Agregados y persistencia
|
||
|
|
|
||
|
|
La raíz del agregado controla las invariantes de dominio, pero no garantiza por sí sola la atomicidad de la persistencia.
|
||
|
|
|
||
|
|
Cuando sea necesario:
|
||
|
|
|
||
|
|
- Utiliza transacciones.
|
||
|
|
- Considera mecanismos de control de concurrencia.
|
||
|
|
- Evita modificaciones parciales de datos persistidos.
|
||
|
|
- Define explícitamente los límites de consistencia.
|
||
|
|
- Gestiona los conflictos entre operaciones concurrentes.
|
||
|
|
|
||
|
|
**La consistencia del agregado requiere tanto un modelo de dominio correcto como una estrategia de persistencia adecuada.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. Responsabilidades y servicios
|
||
|
|
|
||
|
|
No todas las operaciones pertenecen naturalmente a una entidad.
|
||
|
|
|
||
|
|
Algunas responsabilidades necesitan coordinar varios objetos o interactuar con servicios externos.
|
||
|
|
|
||
|
|
### Servicios de dominio
|
||
|
|
|
||
|
|
Representan operaciones del dominio que no pertenecen claramente a una única entidad u objeto de valor.
|
||
|
|
|
||
|
|
Deben contener comportamiento de negocio y evitar dependencias innecesarias de infraestructura.
|
||
|
|
|
||
|
|
### Servicios de aplicación
|
||
|
|
|
||
|
|
Orquestan casos de uso.
|
||
|
|
|
||
|
|
Pueden encargarse de:
|
||
|
|
|
||
|
|
- Coordinar entidades y servicios de dominio.
|
||
|
|
- Controlar transacciones.
|
||
|
|
- Invocar repositorios.
|
||
|
|
- Gestionar operaciones externas.
|
||
|
|
- Aplicar políticas de autorización cuando corresponda.
|
||
|
|
|
||
|
|
Evita trasladar todas las reglas de negocio a los servicios de aplicación si pertenecen naturalmente a las entidades u objetos de valor.
|
||
|
|
|
||
|
|
### Servicios de infraestructura
|
||
|
|
|
||
|
|
Implementan capacidades técnicas como:
|
||
|
|
|
||
|
|
- Persistencia.
|
||
|
|
- Correo electrónico.
|
||
|
|
- Acceso a archivos.
|
||
|
|
- Comunicación con APIs externas.
|
||
|
|
- Servicios de mensajería.
|
||
|
|
|
||
|
|
Cuando resulte apropiado, utiliza contratos que permitan mantener desacopladas las políticas de dominio de los detalles de infraestructura.
|
||
|
|
|
||
|
|
### Evitar servicios todopoderosos
|
||
|
|
|
||
|
|
Una clase denominada `ApplicationManager`, `BusinessService` o `DataProcessor` puede estar acumulando responsabilidades independientes.
|
||
|
|
|
||
|
|
Revisa:
|
||
|
|
|
||
|
|
- Si contiene operaciones sin relación clara.
|
||
|
|
- Si necesita demasiadas dependencias.
|
||
|
|
- Si cambia por motivos muy diferentes.
|
||
|
|
- Si concentra reglas que deberían pertenecer a otros objetos.
|
||
|
|
|
||
|
|
No dividas automáticamente los servicios grandes sin identificar previamente responsabilidades coherentes.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 11. Criterios para evitar la sobreingeniería
|
||
|
|
|
||
|
|
Un diseño orientado a objetos no es necesariamente mejor por incorporar más clases, interfaces o patrones.
|
||
|
|
|
||
|
|
Antes de introducir una abstracción, considera:
|
||
|
|
|
||
|
|
1. ¿Qué problema concreto resuelve?
|
||
|
|
2. ¿Qué responsabilidad representa?
|
||
|
|
3. ¿Reduce realmente el acoplamiento?
|
||
|
|
4. ¿Simplifica las pruebas o el mantenimiento?
|
||
|
|
5. ¿Existe una alternativa más directa?
|
||
|
|
6. ¿Su coste estructural está justificado?
|
||
|
|
|
||
|
|
### Evita especialmente
|
||
|
|
|
||
|
|
- Interfaces con una única implementación creadas sin necesidad de desacoplamiento.
|
||
|
|
- Jerarquías de herencia destinadas únicamente a compartir código trivial.
|
||
|
|
- Factorías que solo encapsulan constructores sencillos.
|
||
|
|
- Servicios que únicamente delegan llamadas.
|
||
|
|
- Objetos de valor sin semántica propia.
|
||
|
|
- Agregados sin invariantes relevantes.
|
||
|
|
- Patrones introducidos para necesidades hipotéticas.
|
||
|
|
- Refactorizaciones destinadas exclusivamente a cumplir límites numéricos.
|
||
|
|
|
||
|
|
### Favorece
|
||
|
|
|
||
|
|
- Responsabilidades claras.
|
||
|
|
- Contratos comprensibles.
|
||
|
|
- Dependencias controladas.
|
||
|
|
- Composición cuando aporte flexibilidad.
|
||
|
|
- Encapsulamiento de invariantes.
|
||
|
|
- Simplicidad estructural.
|
||
|
|
- Evolución incremental del diseño.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 12. Lista de comprobación de diseño orientado a objetos
|
||
|
|
|
||
|
|
### Responsabilidades
|
||
|
|
|
||
|
|
- [ ] ¿Cada objeto tiene una responsabilidad identificable?
|
||
|
|
- [ ] ¿Sus comportamientos pertenecen al concepto que representa?
|
||
|
|
- [ ] ¿He evitado clases que acumulen responsabilidades independientes?
|
||
|
|
- [ ] ¿Las dependencias son necesarias?
|
||
|
|
- [ ] ¿La organización facilita la evolución del sistema?
|
||
|
|
|
||
|
|
### Encapsulamiento
|
||
|
|
|
||
|
|
- [ ] ¿Las invariantes están protegidas?
|
||
|
|
- [ ] ¿El estado mutable se modifica mediante operaciones controladas?
|
||
|
|
- [ ] ¿He evitado exponer referencias mutables que comprometan el estado?
|
||
|
|
- [ ] ¿Los contratos públicos son claros?
|
||
|
|
- [ ] ¿Los datos se exponen cuando su finalidad lo justifica?
|
||
|
|
|
||
|
|
### Composición y herencia
|
||
|
|
|
||
|
|
- [ ] ¿La composición mejora el diseño en este caso?
|
||
|
|
- [ ] ¿Las relaciones de herencia representan especializaciones legítimas?
|
||
|
|
- [ ] ¿Los subtipos respetan los contratos del tipo base?
|
||
|
|
- [ ] ¿He evitado jerarquías artificiales?
|
||
|
|
- [ ] ¿Las abstracciones responden a necesidades reales?
|
||
|
|
|
||
|
|
### Objetos de valor y entidades
|
||
|
|
|
||
|
|
- [ ] ¿He distinguido correctamente identidad y valor?
|
||
|
|
- [ ] ¿Los objetos de valor protegen sus invariantes?
|
||
|
|
- [ ] ¿Las entidades conservan su identidad al modificar sus atributos?
|
||
|
|
- [ ] ¿He evitado encapsular primitivas innecesariamente?
|
||
|
|
- [ ] ¿Las operaciones de dominio están correctamente ubicadas?
|
||
|
|
|
||
|
|
### Agregados
|
||
|
|
|
||
|
|
- [ ] ¿Existe una necesidad real de utilizar un agregado?
|
||
|
|
- [ ] ¿La raíz protege las invariantes?
|
||
|
|
- [ ] ¿Las modificaciones se realizan mediante operaciones controladas?
|
||
|
|
- [ ] ¿Los límites de consistencia están correctamente definidos?
|
||
|
|
- [ ] ¿He considerado transacciones y concurrencia cuando corresponde?
|
||
|
|
- [ ] ¿He evitado agregados excesivamente grandes?
|
||
|
|
|
||
|
|
### Complejidad
|
||
|
|
|
||
|
|
- [ ] ¿La solución es suficientemente sencilla?
|
||
|
|
- [ ] ¿He evitado patrones innecesarios?
|
||
|
|
- [ ] ¿Existe una alternativa más directa?
|
||
|
|
- [ ] ¿La estructura facilita las pruebas?
|
||
|
|
- [ ] ¿He respetado la arquitectura existente?
|
||
|
|
- [ ] ¿La modificación mantiene el comportamiento y los contratos requeridos?
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Principio final
|
||
|
|
|
||
|
|
> "Los objetos deben organizarse alrededor de responsabilidades reales y proteger las reglas del dominio, no existir únicamente para satisfacer patrones de diseño."
|
||
|
|
|
||
|
|
El diseño orientado a objetos debe facilitar la comprensión, la evolución y la verificación del software.
|
||
|
|
|
||
|
|
Utiliza entidades, objetos de valor, servicios, composición, herencia y agregados cuando aporten ventajas concretas.
|
||
|
|
|
||
|
|
**Una arquitectura sencilla, cohesionada y con contratos bien definidos es preferible a una estructura artificialmente compleja que únicamente aparenta seguir principios de diseño orientado a objetos.**
|