metis-desk/.agents/skills/rdx-solid/referencias/patrones-de-diseño.md

1284 lines
36 KiB
Markdown
Raw Normal View History

# 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.**