1284 lines
36 KiB
Markdown
1284 lines
36 KiB
Markdown
|
|
# Patrones de diseño (Design Patterns)
|
||
|
|
|
||
|
|
Los patrones de diseño son soluciones reutilizables a problemas recurrentes de diseño de software.
|
||
|
|
|
||
|
|
Proporcionan un vocabulario común que facilita comunicar decisiones arquitectónicas, reconocer estructuras y organizar responsabilidades.
|
||
|
|
|
||
|
|
Este documento desarrolla los criterios definidos en el `SKILL.md` principal.
|
||
|
|
|
||
|
|
**Los patrones deben utilizarse cuando resuelvan un problema real, no como objetivos de implementación ni como demostración de sofisticación técnica.**
|
||
|
|
|
||
|
|
## 1. Principios fundamentales
|
||
|
|
|
||
|
|
### No fuerces la utilización de patrones
|
||
|
|
|
||
|
|
> "Permite que los patrones surjan de las necesidades del diseño y de la refactorización, en lugar de imponerlos anticipadamente."
|
||
|
|
|
||
|
|
Un patrón debe resolver un problema existente o responder a un requisito de evolución suficientemente definido.
|
||
|
|
|
||
|
|
No introduzcas abstracciones destinadas exclusivamente a necesidades hipotéticas.
|
||
|
|
|
||
|
|
### Cuándo utilizar un patrón
|
||
|
|
|
||
|
|
Antes de aplicarlo, comprueba:
|
||
|
|
|
||
|
|
1. **Problema identificado:** ¿Existe un problema concreto que el patrón resuelve?
|
||
|
|
2. **Adecuación:** ¿El patrón representa correctamente las responsabilidades y relaciones?
|
||
|
|
3. **Simplicidad:** ¿Mejora el diseño sin introducir complejidad innecesaria?
|
||
|
|
4. **Mantenibilidad:** ¿Facilita comprender, modificar o probar el código?
|
||
|
|
5. **Conocimiento del equipo:** ¿La estructura resultará comprensible para otros desarrolladores?
|
||
|
|
6. **Alternativas:** ¿Existe una función, composición u objeto más sencillo que resuelva adecuadamente el problema?
|
||
|
|
|
||
|
|
### Principios de aplicación
|
||
|
|
|
||
|
|
- Prioriza soluciones directas cuando sean suficientes.
|
||
|
|
- Utiliza patrones conocidos cuando reduzcan complejidad o acoplamiento.
|
||
|
|
- No introduzcas jerarquías de clases para comportamientos triviales.
|
||
|
|
- No crees interfaces sin una responsabilidad o contrato identificable.
|
||
|
|
- Prefiere composición cuando aporte flexibilidad.
|
||
|
|
- Respeta los patrones y convenciones ya establecidos en el proyecto.
|
||
|
|
- No refactorices código estable únicamente para incorporar un patrón diferente.
|
||
|
|
- Valora el coste de mantenimiento de cada abstracción.
|
||
|
|
|
||
|
|
**Un patrón correcto debe simplificar el razonamiento sobre el sistema, no limitarse a reorganizar código en más archivos.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Patrones creacionales (Creational Patterns)
|
||
|
|
|
||
|
|
Los patrones creacionales proporcionan mecanismos para construir objetos y controlar su creación.
|
||
|
|
|
||
|
|
### 2.1. Singleton
|
||
|
|
|
||
|
|
**Propósito:** garantizar que exista una única instancia de una clase dentro de un ámbito determinado.
|
||
|
|
|
||
|
|
**Posibles aplicaciones:**
|
||
|
|
|
||
|
|
- Recursos compartidos que requieren una única instancia por proceso.
|
||
|
|
- Coordinadores cuyo ciclo de vida debe estar controlado.
|
||
|
|
- Componentes con restricciones explícitas de unicidad.
|
||
|
|
|
||
|
|
**Advertencia:** Singleton suele utilizarse excesivamente.
|
||
|
|
|
||
|
|
No debe ser la solución predeterminada para:
|
||
|
|
|
||
|
|
- Configuración.
|
||
|
|
- Logging.
|
||
|
|
- Acceso a bases de datos.
|
||
|
|
- Clientes HTTP.
|
||
|
|
- Servicios de aplicación.
|
||
|
|
|
||
|
|
En muchos casos, resulta preferible crear una instancia compartida durante la inicialización de la aplicación e inyectarla donde sea necesaria.
|
||
|
|
|
||
|
|
#### Ejemplo clásico
|
||
|
|
|
||
|
|
```typescript id="n0dk11"
|
||
|
|
class Logger {
|
||
|
|
private static instance: Logger | undefined;
|
||
|
|
|
||
|
|
private constructor() {}
|
||
|
|
|
||
|
|
static getInstance(): Logger {
|
||
|
|
if (!Logger.instance) {
|
||
|
|
Logger.instance = new Logger();
|
||
|
|
}
|
||
|
|
|
||
|
|
return Logger.instance;
|
||
|
|
}
|
||
|
|
|
||
|
|
log(message: string): void {
|
||
|
|
console.log(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Alternativa mediante composición
|
||
|
|
|
||
|
|
```typescript id="ptj27s"
|
||
|
|
class Logger {
|
||
|
|
log(message: string): void {
|
||
|
|
console.log(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const logger = new Logger();
|
||
|
|
|
||
|
|
const orderService = new OrderService(logger);
|
||
|
|
const paymentService = new PaymentService(logger);
|
||
|
|
```
|
||
|
|
|
||
|
|
Este enfoque permite compartir una instancia sin ocultar las dependencias de los consumidores.
|
||
|
|
|
||
|
|
#### Criterios de utilización
|
||
|
|
|
||
|
|
- Utiliza Singleton únicamente cuando la unicidad forme parte del requisito.
|
||
|
|
- Evita convertir dependencias compartidas en estado global accesible desde cualquier lugar.
|
||
|
|
- Considera el impacto sobre las pruebas y el aislamiento.
|
||
|
|
- Distingue entre una instancia única por proceso y una instancia única en todo un sistema distribuido.
|
||
|
|
- No utilices Singleton para resolver problemas que pertenecen a la gestión del ciclo de vida de dependencias.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2.2. Factory (Factoría)
|
||
|
|
|
||
|
|
**Propósito:** encapsular la creación de objetos cuando esta requiera decisiones, validaciones o configuraciones específicas.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- La creación depende de parámetros o configuraciones.
|
||
|
|
- Existen varias implementaciones de un contrato.
|
||
|
|
- La construcción requiere lógica significativa.
|
||
|
|
- Se necesita centralizar una política de creación.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="0n7hgs"
|
||
|
|
interface Notification {
|
||
|
|
send(message: string): Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
class EmailNotification implements Notification {
|
||
|
|
async send(message: string): Promise<void> {
|
||
|
|
await sendEmail(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class SmsNotification implements Notification {
|
||
|
|
async send(message: string): Promise<void> {
|
||
|
|
await sendSms(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class PushNotification implements Notification {
|
||
|
|
async send(message: string): Promise<void> {
|
||
|
|
await sendPush(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="4f1mr6"
|
||
|
|
type NotificationType = 'email' | 'sms' | 'push';
|
||
|
|
|
||
|
|
function createNotification(type: NotificationType): Notification {
|
||
|
|
switch (type) {
|
||
|
|
case 'email':
|
||
|
|
return new EmailNotification();
|
||
|
|
|
||
|
|
case 'sms':
|
||
|
|
return new SmsNotification();
|
||
|
|
|
||
|
|
case 'push':
|
||
|
|
return new PushNotification();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Consideraciones
|
||
|
|
|
||
|
|
En TypeScript, una función factoría suele ser suficiente.
|
||
|
|
|
||
|
|
No es necesario crear una clase `NotificationFactory` si únicamente encapsula una selección sencilla.
|
||
|
|
|
||
|
|
El uso de `switch` dentro de una factoría es legítimo. No contradice los principios SOLID cuando centraliza adecuadamente una decisión de construcción.
|
||
|
|
|
||
|
|
Si las implementaciones deben incorporarse dinámicamente, puede ser preferible un registro explícito.
|
||
|
|
|
||
|
|
#### Evita
|
||
|
|
|
||
|
|
- Factorías que únicamente llaman a un constructor sin añadir valor.
|
||
|
|
- Jerarquías de factorías innecesarias.
|
||
|
|
- Registros dinámicos para un conjunto pequeño y estable de alternativas.
|
||
|
|
- Ocultar dependencias importantes detrás de mecanismos de creación difíciles de rastrear.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2.3. Builder (Constructor por etapas)
|
||
|
|
|
||
|
|
**Propósito:** construir objetos complejos mediante una secuencia controlada de operaciones.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Existen numerosos parámetros opcionales.
|
||
|
|
- La construcción requiere varias etapas.
|
||
|
|
- Deben validarse combinaciones de parámetros.
|
||
|
|
- La preparación de datos de prueba es compleja.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="z082g9"
|
||
|
|
interface UserOptions {
|
||
|
|
name: string;
|
||
|
|
email: string;
|
||
|
|
age?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
class UserBuilder {
|
||
|
|
private name: string | undefined;
|
||
|
|
private email: string | undefined;
|
||
|
|
private age: number | undefined;
|
||
|
|
|
||
|
|
withName(name: string): this {
|
||
|
|
this.name = name;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
withEmail(email: string): this {
|
||
|
|
this.email = email;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
withAge(age: number): this {
|
||
|
|
this.age = age;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
build(): UserOptions {
|
||
|
|
if (!this.name || !this.email) {
|
||
|
|
throw new Error('Faltan los datos obligatorios');
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
name: this.name,
|
||
|
|
email: this.email,
|
||
|
|
...(this.age === undefined ? {} : { age: this.age }),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="t31gdk"
|
||
|
|
const userOptions = new UserBuilder()
|
||
|
|
.withName('Alice')
|
||
|
|
.withEmail('alice@example.com')
|
||
|
|
.build();
|
||
|
|
```
|
||
|
|
|
||
|
|
La validación impide generar un resultado sin los datos obligatorios.
|
||
|
|
|
||
|
|
#### Alternativa más sencilla
|
||
|
|
|
||
|
|
Para muchos casos, un objeto de opciones resulta suficiente:
|
||
|
|
|
||
|
|
```typescript id="l2dwgv"
|
||
|
|
const userOptions: UserOptions = {
|
||
|
|
name: 'Alice',
|
||
|
|
email: 'alice@example.com',
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Criterios de utilización
|
||
|
|
|
||
|
|
- Utiliza Builder cuando aporte claridad a un proceso de construcción complejo.
|
||
|
|
- Prefiere objetos de parámetros para construcciones sencillas.
|
||
|
|
- Evita utilizar aserciones de no nulidad (`!`) para ocultar datos obligatorios ausentes.
|
||
|
|
- Define correctamente las validaciones de construcción.
|
||
|
|
- No dupliques innecesariamente las reglas del constructor o del dominio.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2.4. Prototype (Prototipo)
|
||
|
|
|
||
|
|
**Propósito:** crear nuevos objetos a partir de otros existentes mediante clonación.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- La construcción original es costosa.
|
||
|
|
- Se necesitan copias con pequeñas variaciones.
|
||
|
|
- El objeto dispone de una semántica de clonación definida.
|
||
|
|
- Se deben reproducir configuraciones complejas.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="sc008k"
|
||
|
|
interface DocumentMetadata {
|
||
|
|
tags: string[];
|
||
|
|
author: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
class Document {
|
||
|
|
constructor(
|
||
|
|
readonly title: string,
|
||
|
|
readonly content: string,
|
||
|
|
readonly metadata: DocumentMetadata,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
clone(): Document {
|
||
|
|
return new Document(
|
||
|
|
this.title,
|
||
|
|
this.content,
|
||
|
|
{
|
||
|
|
author: this.metadata.author,
|
||
|
|
tags: [...this.metadata.tags],
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Este ejemplo copia explícitamente los datos mutables de `metadata`.
|
||
|
|
|
||
|
|
#### Precauciones
|
||
|
|
|
||
|
|
Una copia superficial puede conservar referencias compartidas a objetos anidados.
|
||
|
|
|
||
|
|
```typescript id="kz4l4u"
|
||
|
|
// Copia superficial: los objetos anidados siguen compartidos.
|
||
|
|
const copy = { ...original };
|
||
|
|
```
|
||
|
|
|
||
|
|
Antes de clonar, determina:
|
||
|
|
|
||
|
|
- Qué elementos deben copiarse.
|
||
|
|
- Qué referencias pueden compartirse.
|
||
|
|
- Si existen recursos externos.
|
||
|
|
- Si deben preservarse las invariantes.
|
||
|
|
- Qué significa exactamente la identidad de la copia.
|
||
|
|
|
||
|
|
`structuredClone()` puede ser útil para determinados datos compatibles, pero no sustituye universalmente a una operación de clonación de dominio.
|
||
|
|
|
||
|
|
No introduzcas Prototype cuando construir directamente un objeto resulte más sencillo.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Patrones estructurales (Structural Patterns)
|
||
|
|
|
||
|
|
Los patrones estructurales organizan relaciones entre objetos y permiten combinar capacidades.
|
||
|
|
|
||
|
|
### 3.1. Adapter (Adaptador)
|
||
|
|
|
||
|
|
**Propósito:** permitir que componentes con interfaces incompatibles colaboren mediante un contrato común.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Integración con bibliotecas de terceros.
|
||
|
|
- Migración de código legado.
|
||
|
|
- Adaptación de APIs externas.
|
||
|
|
- Sustitución de proveedores.
|
||
|
|
- Separación entre dominio e infraestructura.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
Una biblioteca heredada proporciona este contrato:
|
||
|
|
|
||
|
|
```typescript id="4bdqn8"
|
||
|
|
class OldPaymentApi {
|
||
|
|
makePayment(cents: number): boolean {
|
||
|
|
return legacyPaymentProvider.charge(cents);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
La aplicación utiliza otro contrato:
|
||
|
|
|
||
|
|
```typescript id="29npml"
|
||
|
|
interface PaymentGateway {
|
||
|
|
charge(amount: Money): ChargeResult;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
El adaptador transforma ambas representaciones:
|
||
|
|
|
||
|
|
```typescript id="nv310r"
|
||
|
|
class OldPaymentAdapter implements PaymentGateway {
|
||
|
|
constructor(
|
||
|
|
private readonly oldApi: OldPaymentApi,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
charge(amount: Money): ChargeResult {
|
||
|
|
const cents = amount.toCents();
|
||
|
|
const success = this.oldApi.makePayment(cents);
|
||
|
|
|
||
|
|
return success
|
||
|
|
? ChargeResult.success()
|
||
|
|
: ChargeResult.failed();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Criterios de utilización
|
||
|
|
|
||
|
|
- Mantén el contrato de dominio independiente del proveedor externo.
|
||
|
|
- Centraliza las conversiones necesarias.
|
||
|
|
- Comprueba diferencias de unidades, tipos y semántica.
|
||
|
|
- Traduce correctamente los errores y resultados.
|
||
|
|
- Evita filtrar detalles del proveedor hacia las capas consumidoras.
|
||
|
|
|
||
|
|
En el ejemplo, debe verificarse que el importe esté expresado en una moneda y una unidad admitidas por el proveedor.
|
||
|
|
|
||
|
|
**Un adaptador no debe limitarse a cambiar nombres si existen diferencias semánticas relevantes entre los contratos.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.2. Bridge (Puente)
|
||
|
|
|
||
|
|
**Propósito:** separar dos dimensiones de variación para que puedan evolucionar de manera independiente.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Existen dos jerarquías conceptuales que pueden combinarse.
|
||
|
|
- La herencia produciría numerosas subclases.
|
||
|
|
- Se necesita sustituir implementaciones independientemente de sus consumidores.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
Un sistema genera informes que pueden utilizar distintos formatos de salida.
|
||
|
|
|
||
|
|
```typescript id="30ev2g"
|
||
|
|
interface ReportRenderer {
|
||
|
|
render(content: string): Promise<void>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="0fxjr3"
|
||
|
|
class PdfRenderer implements ReportRenderer {
|
||
|
|
async render(content: string): Promise<void> {
|
||
|
|
await generatePdf(content);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class HtmlRenderer implements ReportRenderer {
|
||
|
|
async render(content: string): Promise<void> {
|
||
|
|
await generateHtml(content);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="12xjux"
|
||
|
|
class Report {
|
||
|
|
constructor(
|
||
|
|
private readonly renderer: ReportRenderer,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async generate(content: string): Promise<void> {
|
||
|
|
await this.renderer.render(content);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
El informe no depende directamente de un formato concreto.
|
||
|
|
|
||
|
|
En un sistema con varios tipos de informes y varios formatos, esta separación puede evitar múltiples combinaciones mediante herencia.
|
||
|
|
|
||
|
|
#### Criterios de utilización
|
||
|
|
|
||
|
|
- Comprueba que existan realmente dos dimensiones independientes de variación.
|
||
|
|
- Evita utilizar Bridge para una única implementación estable.
|
||
|
|
- No crees jerarquías adicionales si una composición sencilla ya resuelve el problema.
|
||
|
|
- Favorece contratos pequeños y explícitos.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.3. Decorator (Decorador)
|
||
|
|
|
||
|
|
**Propósito:** añadir comportamiento a un componente mediante composición, preservando su contrato.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Se necesitan capacidades combinables.
|
||
|
|
- Existen operaciones transversales.
|
||
|
|
- Se desea ampliar un comportamiento sin modificar su implementación.
|
||
|
|
- Debe evitarse una jerarquía extensa de subclases.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="6r5r7y"
|
||
|
|
interface Notifier {
|
||
|
|
send(message: string): Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
class EmailNotifier implements Notifier {
|
||
|
|
async send(message: string): Promise<void> {
|
||
|
|
await sendEmail(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Decorador que añade SMS:
|
||
|
|
|
||
|
|
```typescript id="awibpt"
|
||
|
|
class SmsDecorator implements Notifier {
|
||
|
|
constructor(
|
||
|
|
private readonly wrapped: Notifier,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async send(message: string): Promise<void> {
|
||
|
|
await this.wrapped.send(message);
|
||
|
|
await sendSms(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Decorador que añade Slack:
|
||
|
|
|
||
|
|
```typescript id="dn84cw"
|
||
|
|
class SlackDecorator implements Notifier {
|
||
|
|
constructor(
|
||
|
|
private readonly wrapped: Notifier,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async send(message: string): Promise<void> {
|
||
|
|
await this.wrapped.send(message);
|
||
|
|
await sendSlackMessage(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Composición:
|
||
|
|
|
||
|
|
```typescript id="v5qc8v"
|
||
|
|
const notifier = new SlackDecorator(
|
||
|
|
new SmsDecorator(
|
||
|
|
new EmailNotifier(),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
await notifier.send('Alerta del sistema');
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Consideraciones importantes
|
||
|
|
|
||
|
|
En este ejemplo, las notificaciones se ejecutan secuencialmente.
|
||
|
|
|
||
|
|
Si falla una operación, las siguientes pueden no ejecutarse.
|
||
|
|
|
||
|
|
En un sistema real debe definirse:
|
||
|
|
|
||
|
|
- Si se permite el fallo parcial.
|
||
|
|
- Si deben efectuarse reintentos.
|
||
|
|
- Si las notificaciones son independientes.
|
||
|
|
- Si debe conservarse el orden.
|
||
|
|
- Cómo se registran los errores.
|
||
|
|
|
||
|
|
No utilices Decorator automáticamente cuando una simple función coordinadora resulte más clara.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.4. Proxy
|
||
|
|
|
||
|
|
**Propósito:** controlar el acceso a un objeto manteniendo un contrato compatible.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Carga diferida (*lazy loading*).
|
||
|
|
- Control de acceso.
|
||
|
|
- Caché.
|
||
|
|
- Registro de operaciones.
|
||
|
|
- Acceso remoto.
|
||
|
|
- Control de recursos costosos.
|
||
|
|
|
||
|
|
#### Ejemplo: carga diferida
|
||
|
|
|
||
|
|
```typescript id="6w85mj"
|
||
|
|
interface Image {
|
||
|
|
display(): void;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="737xpj"
|
||
|
|
class RealImage implements Image {
|
||
|
|
constructor(private readonly filename: string) {
|
||
|
|
this.loadFromDisk();
|
||
|
|
}
|
||
|
|
|
||
|
|
private loadFromDisk(): void {
|
||
|
|
// Cargar la imagen.
|
||
|
|
}
|
||
|
|
|
||
|
|
display(): void {
|
||
|
|
// Mostrar la imagen.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="9xypzj"
|
||
|
|
class ImageProxy implements Image {
|
||
|
|
private realImage: RealImage | null = null;
|
||
|
|
|
||
|
|
constructor(private readonly filename: string) {}
|
||
|
|
|
||
|
|
display(): void {
|
||
|
|
if (!this.realImage) {
|
||
|
|
this.realImage = new RealImage(this.filename);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.realImage.display();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
La imagen real únicamente se crea cuando se necesita.
|
||
|
|
|
||
|
|
#### Criterios de utilización
|
||
|
|
|
||
|
|
- Define claramente qué comportamiento controla el proxy.
|
||
|
|
- Mantén los contratos relevantes del objeto real.
|
||
|
|
- Considera los efectos de la caché y la concurrencia.
|
||
|
|
- No ocultes fallos del objeto real.
|
||
|
|
- Evita proxies que únicamente deleguen operaciones sin aportar comportamiento.
|
||
|
|
|
||
|
|
En aplicaciones modernas, ciertas bibliotecas y frameworks ya proporcionan mecanismos de carga diferida, caché o control de acceso. Reutilízalos cuando resulten apropiados.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3.5. Composite (Compuesto)
|
||
|
|
|
||
|
|
**Propósito:** tratar objetos individuales y agrupaciones de objetos mediante un contrato común.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Estructuras jerárquicas.
|
||
|
|
- Árboles de componentes.
|
||
|
|
- Directorios y archivos.
|
||
|
|
- Grupos de elementos.
|
||
|
|
- Expresiones compuestas.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="b9ae4m"
|
||
|
|
interface PriceComponent {
|
||
|
|
getPrice(): number;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="69wct2"
|
||
|
|
class Product implements PriceComponent {
|
||
|
|
constructor(private readonly price: number) {}
|
||
|
|
|
||
|
|
getPrice(): number {
|
||
|
|
return this.price;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="41ojbs"
|
||
|
|
class Box implements PriceComponent {
|
||
|
|
private readonly children: PriceComponent[] = [];
|
||
|
|
|
||
|
|
add(component: PriceComponent): void {
|
||
|
|
this.children.push(component);
|
||
|
|
}
|
||
|
|
|
||
|
|
getPrice(): number {
|
||
|
|
return this.children.reduce(
|
||
|
|
(total, child) => total + child.getPrice(),
|
||
|
|
0,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Uso:
|
||
|
|
|
||
|
|
```typescript id="3v8qdz"
|
||
|
|
const smallBox = new Box();
|
||
|
|
|
||
|
|
smallBox.add(new Product(10));
|
||
|
|
smallBox.add(new Product(20));
|
||
|
|
|
||
|
|
const bigBox = new Box();
|
||
|
|
|
||
|
|
bigBox.add(smallBox);
|
||
|
|
bigBox.add(new Product(50));
|
||
|
|
|
||
|
|
console.log(bigBox.getPrice()); // 80
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Consideraciones
|
||
|
|
|
||
|
|
- Evita referencias circulares cuando la estructura sea un árbol.
|
||
|
|
- Define quién controla la modificación de los elementos.
|
||
|
|
- No expongas colecciones mutables que comprometan invariantes.
|
||
|
|
- Considera la profundidad y el coste de recorrer la jerarquía.
|
||
|
|
- Utiliza objetos de valor apropiados cuando existan importes monetarios reales.
|
||
|
|
|
||
|
|
Composite es útil cuando la estructura jerárquica forma parte del problema. No es necesario para colecciones planas sencillas.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Patrones de comportamiento (Behavioral Patterns)
|
||
|
|
|
||
|
|
Los patrones de comportamiento organizan las interacciones, decisiones y responsabilidades entre componentes.
|
||
|
|
|
||
|
|
### 4.1. Strategy (Estrategia)
|
||
|
|
|
||
|
|
**Propósito:** definir diferentes implementaciones de un comportamiento mediante un contrato común.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Existen varios algoritmos alternativos.
|
||
|
|
- Las reglas pueden seleccionarse dinámicamente.
|
||
|
|
- Los algoritmos evolucionan independientemente.
|
||
|
|
- Se necesita probar cada política por separado.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="u10rrb"
|
||
|
|
interface PricingStrategy {
|
||
|
|
calculate(basePrice: number): number;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="0ivv9t"
|
||
|
|
class RegularPricing implements PricingStrategy {
|
||
|
|
calculate(basePrice: number): number {
|
||
|
|
return basePrice;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="fpmp3v"
|
||
|
|
class PremiumDiscount implements PricingStrategy {
|
||
|
|
calculate(basePrice: number): number {
|
||
|
|
return basePrice * 0.8;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="cvl9d1"
|
||
|
|
class BlackFridayPricing implements PricingStrategy {
|
||
|
|
calculate(basePrice: number): number {
|
||
|
|
return basePrice * 0.5;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
El carrito utiliza la estrategia seleccionada:
|
||
|
|
|
||
|
|
```typescript id="q0uv1j"
|
||
|
|
class ShoppingCart {
|
||
|
|
constructor(
|
||
|
|
private readonly pricing: PricingStrategy,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
calculateTotal(items: Item[]): number {
|
||
|
|
const basePrice = items.reduce(
|
||
|
|
(total, item) => total + item.price,
|
||
|
|
0,
|
||
|
|
);
|
||
|
|
|
||
|
|
return this.pricing.calculate(basePrice);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Los ejemplos numéricos ilustran el patrón. Para importes monetarios reales deben utilizarse representaciones y reglas de redondeo adecuadas.
|
||
|
|
|
||
|
|
#### Alternativa mediante funciones
|
||
|
|
|
||
|
|
En TypeScript, una función puede ser suficiente:
|
||
|
|
|
||
|
|
```typescript id="xkmb4o"
|
||
|
|
type PricingStrategy = (basePrice: number) => number;
|
||
|
|
|
||
|
|
const regularPricing: PricingStrategy = (price) => price;
|
||
|
|
const premiumPricing: PricingStrategy = (price) => price * 0.8;
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Criterios de utilización
|
||
|
|
|
||
|
|
- Prefiere funciones cuando únicamente necesites intercambiar algoritmos sencillos.
|
||
|
|
- Utiliza objetos cuando las estrategias requieran dependencias, estado o comportamiento adicional.
|
||
|
|
- No crees una jerarquía de estrategias para uno o dos condicionales triviales.
|
||
|
|
- Mantén coherentes las precondiciones y resultados de las implementaciones.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 4.2. Observer (Observador)
|
||
|
|
|
||
|
|
**Propósito:** notificar a varios consumidores cuando se produce un evento o cambio de estado.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Sistemas de eventos.
|
||
|
|
- Actualizaciones reactivas.
|
||
|
|
- Publicación y suscripción.
|
||
|
|
- Comunicación desacoplada entre componentes.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="nt8slp"
|
||
|
|
interface OrderPlacedEvent {
|
||
|
|
type: 'ORDER_PLACED';
|
||
|
|
orderId: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
type OrderObserver = (event: OrderPlacedEvent) => void;
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="q2mtwi"
|
||
|
|
class OrderEventEmitter {
|
||
|
|
private readonly observers = new Set<OrderObserver>();
|
||
|
|
|
||
|
|
subscribe(observer: OrderObserver): () => void {
|
||
|
|
this.observers.add(observer);
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
this.observers.delete(observer);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
notify(event: OrderPlacedEvent): void {
|
||
|
|
for (const observer of this.observers) {
|
||
|
|
observer(event);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Uso:
|
||
|
|
|
||
|
|
```typescript id="e0uhri"
|
||
|
|
const emitter = new OrderEventEmitter();
|
||
|
|
|
||
|
|
const unsubscribe = emitter.subscribe((event) => {
|
||
|
|
console.log(`Pedido realizado: ${event.orderId}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
emitter.notify({
|
||
|
|
type: 'ORDER_PLACED',
|
||
|
|
orderId: 'ORDER-001',
|
||
|
|
});
|
||
|
|
|
||
|
|
unsubscribe();
|
||
|
|
```
|
||
|
|
|
||
|
|
El ejemplo utiliza notificaciones síncronas. Si un observador lanza una excepción, puede interrumpir la entrega a los siguientes.
|
||
|
|
|
||
|
|
#### Consideraciones de diseño
|
||
|
|
|
||
|
|
- Define cuándo se emiten los eventos.
|
||
|
|
- Evita emitir eventos de negocio confirmados antes de completar la operación correspondiente.
|
||
|
|
- Establece cómo se gestionan los errores de los observadores.
|
||
|
|
- Permite cancelar suscripciones para evitar retención innecesaria de recursos.
|
||
|
|
- Diferencia los eventos internos de los eventos distribuidos.
|
||
|
|
- Determina si las notificaciones deben ser síncronas o asíncronas.
|
||
|
|
- Considera idempotencia, reintentos y entrega duplicada cuando exista mensajería distribuida.
|
||
|
|
|
||
|
|
Observer no sustituye automáticamente a una arquitectura de eventos.
|
||
|
|
|
||
|
|
Para comunicación entre procesos o servicios pueden ser necesarios mecanismos de persistencia de eventos, colas o patrones como *Transactional Outbox*.
|
||
|
|
|
||
|
|
#### Evita
|
||
|
|
|
||
|
|
- Utilizar eventos para ocultar dependencias que deberían ser explícitas.
|
||
|
|
- Crear flujos difíciles de rastrear.
|
||
|
|
- Encadenar numerosos observadores con efectos secundarios implícitos.
|
||
|
|
- Introducir un bus de eventos global para operaciones sencillas.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 4.3. Template Method (Método plantilla)
|
||
|
|
|
||
|
|
**Propósito:** definir la estructura general de un algoritmo y permitir que determinadas etapas varíen mediante especialización.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Existe un proceso estable con pasos claramente definidos.
|
||
|
|
- Varias implementaciones comparten la misma secuencia.
|
||
|
|
- Las diferencias pueden representarse mediante operaciones especializadas.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="0qnk1j"
|
||
|
|
abstract class DataExporter {
|
||
|
|
export(data: ExportRecord[]): void {
|
||
|
|
this.validate(data);
|
||
|
|
|
||
|
|
const content = this.format(data);
|
||
|
|
|
||
|
|
this.write(content);
|
||
|
|
}
|
||
|
|
|
||
|
|
private validate(data: ExportRecord[]): void {
|
||
|
|
if (!Array.isArray(data)) {
|
||
|
|
throw new Error('Datos de exportación no válidos');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected abstract format(data: ExportRecord[]): string;
|
||
|
|
|
||
|
|
protected abstract write(content: string): void;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="jcwu1g"
|
||
|
|
class CsvExporter extends DataExporter {
|
||
|
|
protected format(data: ExportRecord[]): string {
|
||
|
|
return data.map((record) => record.toCsv()).join('\n');
|
||
|
|
}
|
||
|
|
|
||
|
|
protected write(content: string): void {
|
||
|
|
writeFile('export.csv', content);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="jzeyc0"
|
||
|
|
class JsonExporter extends DataExporter {
|
||
|
|
protected format(data: ExportRecord[]): string {
|
||
|
|
return JSON.stringify(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected write(content: string): void {
|
||
|
|
writeFile('export.json', content);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Alternativa mediante composición
|
||
|
|
|
||
|
|
Cuando las variaciones pueden expresarse mediante colaboradores independientes:
|
||
|
|
|
||
|
|
```typescript id="x7v9ni"
|
||
|
|
interface DataFormatter {
|
||
|
|
format(data: ExportRecord[]): string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface DataWriter {
|
||
|
|
write(content: string): void;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="9rqgh3"
|
||
|
|
class DataExporter {
|
||
|
|
constructor(
|
||
|
|
private readonly formatter: DataFormatter,
|
||
|
|
private readonly writer: DataWriter,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
export(data: ExportRecord[]): void {
|
||
|
|
const content = this.formatter.format(data);
|
||
|
|
|
||
|
|
this.writer.write(content);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
La composición permite sustituir el formateador y el mecanismo de escritura por separado.
|
||
|
|
|
||
|
|
#### Criterios de utilización
|
||
|
|
|
||
|
|
- Utiliza Template Method cuando la herencia represente correctamente un algoritmo estable.
|
||
|
|
- Prefiere composición cuando las etapas deban combinarse independientemente.
|
||
|
|
- Evita clases base con numerosos métodos protegidos.
|
||
|
|
- No introduzcas herencia únicamente para compartir unas pocas instrucciones.
|
||
|
|
- Comprueba que las especializaciones respetan los contratos de la clase base.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 4.4. Command (Comando)
|
||
|
|
|
||
|
|
**Propósito:** encapsular una solicitud u operación como una unidad independiente.
|
||
|
|
|
||
|
|
**Cuándo utilizarlo:**
|
||
|
|
|
||
|
|
- Historial de operaciones.
|
||
|
|
- Deshacer y rehacer.
|
||
|
|
- Colas de trabajo.
|
||
|
|
- Ejecución diferida.
|
||
|
|
- Registro de acciones.
|
||
|
|
- Operaciones que requieren auditoría.
|
||
|
|
|
||
|
|
#### Ejemplo
|
||
|
|
|
||
|
|
```typescript id="ld2of3"
|
||
|
|
interface Command {
|
||
|
|
execute(): void;
|
||
|
|
undo(): void;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="57hwjz"
|
||
|
|
class AddItemCommand implements Command {
|
||
|
|
private executed = false;
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
private readonly cart: Cart,
|
||
|
|
private readonly item: Item,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
execute(): void {
|
||
|
|
this.cart.add(this.item);
|
||
|
|
this.executed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
undo(): void {
|
||
|
|
if (!this.executed) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.cart.remove(this.item);
|
||
|
|
this.executed = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript id="77kn41"
|
||
|
|
class CommandHistory {
|
||
|
|
private readonly history: Command[] = [];
|
||
|
|
|
||
|
|
execute(command: Command): void {
|
||
|
|
command.execute();
|
||
|
|
this.history.push(command);
|
||
|
|
}
|
||
|
|
|
||
|
|
undo(): void {
|
||
|
|
const command = this.history.at(-1);
|
||
|
|
|
||
|
|
if (!command) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
command.undo();
|
||
|
|
this.history.pop();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Precauciones importantes
|
||
|
|
|
||
|
|
El ejemplo presupone que:
|
||
|
|
|
||
|
|
- La operación `add` se completa correctamente o lanza una excepción.
|
||
|
|
- El elemento puede identificarse y eliminarse de forma inequívoca.
|
||
|
|
- No se modifican los mismos datos concurrentemente.
|
||
|
|
- Cada comando se ejecuta y deshace según el ciclo previsto.
|
||
|
|
|
||
|
|
En aplicaciones reales, estas garantías deben analizarse expresamente.
|
||
|
|
|
||
|
|
**Deshacer una operación no equivale necesariamente a ejecutar la operación contraria.**
|
||
|
|
|
||
|
|
Por ejemplo:
|
||
|
|
|
||
|
|
- Deshacer un envío de correo electrónico puede ser imposible.
|
||
|
|
- Revertir un pago requiere una nueva operación de negocio.
|
||
|
|
- Eliminar un elemento puede requerir conservar su posición original.
|
||
|
|
- Una operación distribuida puede exigir acciones compensatorias.
|
||
|
|
|
||
|
|
### Criterios de utilización
|
||
|
|
|
||
|
|
- Encapsula comandos cuando necesites representar operaciones independientes.
|
||
|
|
- Utiliza funciones cuando una simple llamada sea suficiente.
|
||
|
|
- Define los errores y las garantías de ejecución.
|
||
|
|
- Distingue comandos reversibles de acciones compensables.
|
||
|
|
- Evita implementar sistemas de historial o colas sin una necesidad identificada.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Reconocimiento de patrones
|
||
|
|
|
||
|
|
### Análisis mediante cuatro dimensiones
|
||
|
|
|
||
|
|
Cuando analices código nuevo o bibliotecas desconocidas, considera:
|
||
|
|
|
||
|
|
1. **Problema que resuelve:** creación, estructura o comportamiento.
|
||
|
|
2. **Ámbito:** objeto, clase, módulo o sistema.
|
||
|
|
3. **Momento de aplicación:** configuración, inicialización, compilación o ejecución.
|
||
|
|
4. **Acoplamiento:** qué dependencias introduce y cuáles elimina.
|
||
|
|
|
||
|
|
Estas dimensiones ayudan a reconocer patrones incluso cuando su implementación no coincide exactamente con la descripción clásica.
|
||
|
|
|
||
|
|
### Preguntas adicionales
|
||
|
|
|
||
|
|
- ¿Qué responsabilidades separa?
|
||
|
|
- ¿Qué contrato proporciona?
|
||
|
|
- ¿Qué variaciones permite?
|
||
|
|
- ¿Qué coste introduce?
|
||
|
|
- ¿Qué alternativa sería más sencilla?
|
||
|
|
- ¿Qué ocurriría si elimináramos el patrón?
|
||
|
|
|
||
|
|
**Reconocer un patrón no significa que sea necesario introducirlo.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Patrones en TypeScript y arquitecturas modernas
|
||
|
|
|
||
|
|
Los lenguajes y frameworks modernos proporcionan mecanismos que pueden sustituir implementaciones clásicas basadas en clases.
|
||
|
|
|
||
|
|
### 6.1. Funciones como estrategias
|
||
|
|
|
||
|
|
Las funciones de primera clase permiten intercambiar algoritmos sin crear jerarquías.
|
||
|
|
|
||
|
|
```typescript id="sqn7la"
|
||
|
|
type DiscountCalculator = (price: number) => number;
|
||
|
|
|
||
|
|
const noDiscount: DiscountCalculator = (price) => price;
|
||
|
|
|
||
|
|
const premiumDiscount: DiscountCalculator = (price) => price * 0.8;
|
||
|
|
|
||
|
|
function calculatePrice(
|
||
|
|
price: number,
|
||
|
|
discount: DiscountCalculator,
|
||
|
|
): number {
|
||
|
|
return discount(price);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Este enfoque puede resultar suficiente cuando las estrategias carecen de estado o dependencias complejas.
|
||
|
|
|
||
|
|
### 6.2. Funciones factoría
|
||
|
|
|
||
|
|
Una función puede encapsular la creación de objetos sin necesidad de una clase específica.
|
||
|
|
|
||
|
|
```typescript id="8fujam"
|
||
|
|
function createLogger(environment: 'development' | 'production'): Logger {
|
||
|
|
if (environment === 'development') {
|
||
|
|
return new ConsoleLogger();
|
||
|
|
}
|
||
|
|
|
||
|
|
return new StructuredLogger();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.3. Composición explícita
|
||
|
|
|
||
|
|
La composición puede realizarse directamente durante la inicialización de una aplicación.
|
||
|
|
|
||
|
|
```typescript id="w528ki"
|
||
|
|
const logger = new ConsoleLogger();
|
||
|
|
const repository = new PostgresOrderRepository();
|
||
|
|
|
||
|
|
const orderService = new OrderService(
|
||
|
|
repository,
|
||
|
|
logger,
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
No es obligatorio utilizar un contenedor de dependencias cuando la composición manual sea suficiente.
|
||
|
|
|
||
|
|
### 6.4. Patrones propios de frameworks
|
||
|
|
|
||
|
|
Cuando el proyecto utilice React, Express, NestJS u otros frameworks:
|
||
|
|
|
||
|
|
- Respeta los mecanismos idiomáticos del framework.
|
||
|
|
- No reproduzcas patrones clásicos que el framework ya resuelve.
|
||
|
|
- Evita introducir capas adicionales sin responsabilidades concretas.
|
||
|
|
- Mantén la separación entre lógica de negocio y detalles técnicos cuando corresponda.
|
||
|
|
- No fuerces el uso de clases en escenarios donde funciones, componentes o hooks sean más apropiados.
|
||
|
|
|
||
|
|
Por ejemplo, en React puede resultar más adecuado utilizar composición de componentes y hooks que aplicar literalmente jerarquías de herencia, Decorator u Observer.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. Antipatrones que deben evitarse
|
||
|
|
|
||
|
|
| Antipatrón | Problema | Enfoque recomendado |
|
||
|
|
|---|---|---|
|
||
|
|
| **God Object (Objeto todopoderoso)** | Una clase concentra responsabilidades independientes. | Separar responsabilidades coherentes. |
|
||
|
|
| **Spaghetti Code (Código espagueti)** | Flujo y dependencias difíciles de comprender. | Simplificar estructuras y reorganizar responsabilidades. |
|
||
|
|
| **Golden Hammer (Martillo de oro)** | Utilizar siempre el mismo patrón. | Elegir soluciones según el problema concreto. |
|
||
|
|
| **Premature Optimization (Optimización prematura)** | Optimizar sin necesidad ni evidencia. | Medir y priorizar problemas reales. |
|
||
|
|
| **Copy-Paste Programming (Programación por copia y pega)** | Duplicación de conocimiento o reglas. | Identificar abstracciones justificadas. |
|
||
|
|
| **Overengineering (Sobreingeniería)** | Introducir complejidad para requisitos hipotéticos. | Aplicar YAGNI y KISS. |
|
||
|
|
| **Speculative Generality (Generalización especulativa)** | Crear extensibilidad sin necesidad real. | Diseñar para requisitos existentes o definidos. |
|
||
|
|
| **Service Locator indiscriminado** | Ocultar dependencias mediante búsquedas globales. | Preferir dependencias explícitas cuando corresponda. |
|
||
|
|
| **Abstraction Explosion (Explosión de abstracciones)** | Demasiadas interfaces, clases y capas triviales. | Eliminar intermediarios sin responsabilidad real. |
|
||
|
|
|
||
|
|
### La regla de tres
|
||
|
|
|
||
|
|
La regla de tres puede ayudar a detectar oportunidades de reutilización, pero no debe interpretarse como un requisito obligatorio.
|
||
|
|
|
||
|
|
- No extraigas automáticamente una abstracción por encontrar tres fragmentos parecidos.
|
||
|
|
- No esperes a una tercera duplicación cuando ya exista una regla de negocio común que deba mantenerse coherente.
|
||
|
|
- Comprueba que el código duplicado represente el mismo conocimiento.
|
||
|
|
- Prefiere una pequeña duplicación comprensible antes que una abstracción incorrecta.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. Cómo decidir si utilizar un patrón
|
||
|
|
|
||
|
|
Antes de introducir un patrón nuevo:
|
||
|
|
|
||
|
|
1. Identifica el problema concreto.
|
||
|
|
2. Comprueba las soluciones existentes en el proyecto.
|
||
|
|
3. Define qué responsabilidad debe quedar aislada.
|
||
|
|
4. Evalúa una solución directa sin patrón.
|
||
|
|
5. Compara complejidad, acoplamiento y mantenibilidad.
|
||
|
|
6. Introduce el patrón únicamente si aporta una ventaja clara.
|
||
|
|
7. Verifica que no modifica involuntariamente el comportamiento existente.
|
||
|
|
|
||
|
|
### Señales de que un patrón puede ser útil
|
||
|
|
|
||
|
|
- Existen variaciones de comportamiento que evolucionan independientemente.
|
||
|
|
- Se repiten decisiones de creación complejas.
|
||
|
|
- Las dependencias impiden probar componentes.
|
||
|
|
- Una integración externa contamina los contratos de dominio.
|
||
|
|
- Una estructura jerárquica necesita operaciones uniformes.
|
||
|
|
- Existen responsabilidades transversales combinables.
|
||
|
|
|
||
|
|
### Señales de que puede ser innecesario
|
||
|
|
|
||
|
|
- Solo existe una implementación sencilla.
|
||
|
|
- La variación es hipotética.
|
||
|
|
- El patrón añade numerosos archivos sin simplificar las operaciones.
|
||
|
|
- Los consumidores necesitan conocer demasiados detalles del patrón.
|
||
|
|
- Una función o un objeto literal resuelve el problema de forma clara.
|
||
|
|
- El código original ya es comprensible y fácil de modificar.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. Lista de comprobación de patrones de diseño
|
||
|
|
|
||
|
|
### Selección
|
||
|
|
|
||
|
|
- [ ] ¿Existe un problema real que justifique el patrón?
|
||
|
|
- [ ] ¿Conozco una alternativa más sencilla?
|
||
|
|
- [ ] ¿El patrón representa correctamente el dominio?
|
||
|
|
- [ ] ¿Reduce el acoplamiento o mejora la cohesión?
|
||
|
|
- [ ] ¿La solución será comprensible para otros desarrolladores?
|
||
|
|
|
||
|
|
### Implementación
|
||
|
|
|
||
|
|
- [ ] ¿Los contratos están correctamente definidos?
|
||
|
|
- [ ] ¿Las implementaciones respetan los comportamientos esperados?
|
||
|
|
- [ ] ¿He evitado clases e interfaces innecesarias?
|
||
|
|
- [ ] ¿Las dependencias son explícitas cuando corresponde?
|
||
|
|
- [ ] ¿Se respetan las convenciones del proyecto?
|
||
|
|
|
||
|
|
### Mantenimiento
|
||
|
|
|
||
|
|
- [ ] ¿El patrón facilita incorporar cambios reales?
|
||
|
|
- [ ] ¿Se puede probar sin infraestructura innecesaria?
|
||
|
|
- [ ] ¿Introduce complejidad operativa adicional?
|
||
|
|
- [ ] ¿La estructura sigue siendo adecuada si se incorporan nuevos requisitos?
|
||
|
|
- [ ] ¿He evitado refactorizaciones ajenas al alcance?
|
||
|
|
|
||
|
|
### Verificación
|
||
|
|
|
||
|
|
- [ ] ¿Se conserva el comportamiento observable?
|
||
|
|
- [ ] ¿Los contratos existentes siguen siendo compatibles?
|
||
|
|
- [ ] ¿Se han ejecutado las pruebas pertinentes?
|
||
|
|
- [ ] ¿Los errores y estados excepcionales están correctamente tratados?
|
||
|
|
- [ ] ¿La nueva abstracción aporta una mejora verificable?
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. Referencia rápida
|
||
|
|
|
||
|
|
| Patrón | Problema principal | Precaución |
|
||
|
|
|---|---|---|
|
||
|
|
| **Singleton** | Instancia única controlada. | Evitar estado global oculto. |
|
||
|
|
| **Factory** | Construcción variable o compleja. | No crear factorías triviales. |
|
||
|
|
| **Builder** | Construcción por etapas. | No sustituir innecesariamente objetos de opciones. |
|
||
|
|
| **Prototype** | Clonación de objetos. | Comprobar referencias mutables. |
|
||
|
|
| **Adapter** | Interfaces incompatibles. | Preservar la semántica del contrato. |
|
||
|
|
| **Bridge** | Variaciones independientes. | Evitar jerarquías innecesarias. |
|
||
|
|
| **Decorator** | Comportamientos combinables. | Controlar orden y efectos secundarios. |
|
||
|
|
| **Proxy** | Control de acceso o carga. | Mantener transparencia contractual. |
|
||
|
|
| **Composite** | Estructuras jerárquicas. | Proteger invariantes y evitar ciclos. |
|
||
|
|
| **Strategy** | Algoritmos intercambiables. | Considerar funciones sencillas. |
|
||
|
|
| **Observer** | Notificaciones a consumidores. | Gestionar errores y suscripciones. |
|
||
|
|
| **Template Method** | Algoritmos con etapas variables. | Considerar composición como alternativa. |
|
||
|
|
| **Command** | Operaciones como unidades independientes. | Definir correctamente reversión y ejecución. |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Principio final
|
||
|
|
|
||
|
|
> "Los patrones de diseño son herramientas para resolver problemas recurrentes, no requisitos que deban imponerse a todas las implementaciones."
|
||
|
|
|
||
|
|
Utiliza patrones cuando permitan expresar responsabilidades con mayor claridad, controlar dependencias y facilitar la evolución del software.
|
||
|
|
|
||
|
|
No conviertas la identificación de patrones en una obligación de aplicarlos.
|
||
|
|
|
||
|
|
**La mejor solución es la más sencilla que satisface correctamente los requisitos y mantiene el software comprensible, verificable y preparado para evolucionar.**
|