# 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; } class EmailNotification implements Notification { async send(message: string): Promise { await sendEmail(message); } } class SmsNotification implements Notification { async send(message: string): Promise { await sendSms(message); } } class PushNotification implements Notification { async send(message: string): Promise { 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; } ``` ```typescript id="0fxjr3" class PdfRenderer implements ReportRenderer { async render(content: string): Promise { await generatePdf(content); } } class HtmlRenderer implements ReportRenderer { async render(content: string): Promise { await generateHtml(content); } } ``` ```typescript id="12xjux" class Report { constructor( private readonly renderer: ReportRenderer, ) {} async generate(content: string): Promise { 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; } class EmailNotifier implements Notifier { async send(message: string): Promise { 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 { 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 { 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(); 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.**