Patrones de diseño: El vocabulario compartido del software

Patrones de diseño: El vocabulario compartido del software

Una guía completa sobre patrones de diseño explicada en lenguaje humano: qué son, cuándo usarlos, cómo implementarlos y por qué importan para tu equipo y tu negocio.

Por Omar Flores

Imagina que estás explicando cómo preparar café. Podrías decir: “Tomas agua, la calientas hasta que hierva, luego la pasas por un filtro que contiene granos de café molidos, esperas a que el agua absorba el sabor y color del café, y finalmente recoges el líquido resultante en una taza”. O podrías simplemente decir: “Haces café”. Todos entienden inmediatamente qué significa, qué pasos involucra, qué resultado esperar.

Los patrones de diseño son exactamente eso: un vocabulario compartido para soluciones comunes a problemas recurrentes en el desarrollo de software. Cuando un desarrollador dice “usemos el patrón Observer aquí”, otro desarrollador experimentado inmediatamente visualiza la estructura, las responsabilidades de cada componente, cómo fluye la información, y qué beneficios y trade-offs trae esa decisión.

He visto equipos perder semanas discutiendo cómo resolver un problema, cuando ese problema ya tenía una solución bien documentada y probada. He visto código que reinventa torpemente patrones establecidos, añadiendo complejidad innecesaria porque el desarrollador no sabía que existía una forma mejor. Y también he visto el extremo opuesto: código lleno de patrones aplicados indiscriminadamente, convirtiendo soluciones simples en laberintos de abstracciones.

Los patrones de diseño no son recetas mágicas. No son reglas que debes seguir ciegamente. Son herramientas en tu caja de herramientas, cada una apropiada para ciertos problemas, inapropiada para otros. Entenderlos te hace un mejor desarrollador, pero entender cuándo no usarlos te hace un desarrollador sabio.


Qué son realmente los patrones de diseño

Antes de sumergirnos en patrones específicos, necesitas entender qué son y qué no son. Un patrón de diseño no es código que copias y pegas. No es un framework o una librería. Es una descripción de una solución a un problema que ocurre repetidamente en diferentes contextos.

Piensa en los patrones de diseño como planos arquitectónicos. Cuando un arquitecto diseña una casa, no inventa desde cero cómo construir una escalera. Existen patrones establecidos para escaleras: escaleras rectas, escaleras en espiral, escaleras en L. Cada una tiene características conocidas, ventajas y desventajas, contextos donde funciona mejor. El arquitecto no necesita calcular cada vez el ángulo óptimo de los escalones; ese conocimiento ya está codificado en el patrón.

De manera similar, los patrones de diseño de software codifican soluciones que han demostrado funcionar en la práctica. Fueron descubiertos, no inventados. Desarrolladores observaron que ciertos problemas aparecían una y otra vez, y que ciertas soluciones funcionaban mejor que otras. Con el tiempo, estas soluciones se documentaron, se les dio nombres, y se convirtieron en conocimiento compartido de la profesión.

“Cada patrón describe un problema que ocurre una y otra vez en nuestro entorno, y luego describe el núcleo de la solución a ese problema, de tal manera que puedes usar esta solución un millón de veces sin hacerlo de la misma manera dos veces”

Esta cita, originalmente sobre patrones arquitectónicos, captura perfectamente la esencia de los patrones de diseño de software. No son plantillas rígidas, sino guías flexibles que adaptas a tu contexto específico.

Los tres tipos fundamentales de patrones

Los patrones de diseño tradicionalmente se clasifican en tres categorías, cada una abordando un tipo diferente de problema. Entender estas categorías te ayuda a saber dónde buscar cuando enfrentas un desafío específico.

Patrones Creacionales: Cómo nacen los objetos

Los patrones creacionales tratan sobre la creación de objetos. Esto suena trivial, pero crear objetos de la manera correcta puede ser sorprendentemente complejo. ¿Cómo creas un objeto sin especificar su clase exacta? ¿Cómo aseguras que solo exista una instancia de cierta clase? ¿Cómo construyes objetos complejos paso a paso?

El problema que resuelven: Cuando creas objetos directamente en tu código, estás acoplando tu código a clases específicas. Si necesitas cambiar qué clase se instancia, tienes que modificar el código. Si el proceso de creación es complejo, ese código de creación se esparce por toda tu aplicación. Los patrones creacionales centralizan y estandarizan la creación de objetos.

Cuándo los necesitas:

  • Cuando el proceso de creación de un objeto es complejo
  • Cuando necesitas flexibilidad sobre qué objetos se crean
  • Cuando quieres controlar cuántas instancias de una clase existen
  • Cuando la creación directa acoplaría demasiado tu código

Patrones Estructurales: Cómo se relacionan los objetos

Los patrones estructurales tratan sobre cómo componer clases y objetos para formar estructuras más grandes. ¿Cómo haces que objetos con interfaces incompatibles trabajen juntos? ¿Cómo añades funcionalidad a un objeto sin modificar su clase? ¿Cómo representas jerarquías complejas de objetos?

El problema que resuelven: A medida que tu sistema crece, necesitas que diferentes partes trabajen juntas. Pero estas partes pueden haber sido diseñadas independientemente, pueden venir de librerías externas, o pueden tener requisitos conflictivos. Los patrones estructurales te dan formas de organizar y conectar estas piezas.

Cuándo los necesitas:

  • Cuando necesitas integrar código que no puedes modificar
  • Cuando quieres añadir funcionalidad sin herencia
  • Cuando tienes estructuras de objetos complejas que gestionar
  • Cuando necesitas proveer una interfaz simplificada a un sistema complejo

Patrones Comportamentales: Cómo interactúan los objetos

Los patrones comportamentales tratan sobre la comunicación entre objetos. ¿Cómo notificas a múltiples objetos cuando algo cambia? ¿Cómo implementas algoritmos de manera que puedan variar independientemente? ¿Cómo defines una familia de algoritmos intercambiables?

El problema que resuelven: La interacción entre objetos puede volverse compleja rápidamente. Un objeto puede necesitar notificar a muchos otros. Un algoritmo puede necesitar comportarse diferente según el contexto. Los patrones comportamentales organizan estas interacciones de manera que sean flexibles y mantenibles.

Cuándo los necesitas:

  • Cuando múltiples objetos necesitan coordinarse
  • Cuando quieres que el comportamiento sea intercambiable
  • Cuando tienes algoritmos complejos que varían
  • Cuando necesitas comunicación flexible entre objetos

Patrones creacionales en profundidad

Vamos a explorar los patrones creacionales más importantes, entendiendo no solo qué son sino por qué existen y cuándo usarlos.

Singleton: El único en su especie

El problema: Hay ciertos recursos que solo deberían existir una vez en tu aplicación. Una conexión a la base de datos. Un gestor de configuración. Un sistema de logging. Si creas múltiples instancias, desperdicias recursos, creas inconsistencias, o peor, causas conflictos.

La solución: El patrón Singleton asegura que una clase tenga solo una instancia y proporciona un punto global de acceso a ella. La clase misma es responsable de rastrear su única instancia.

Cómo funciona: Imagina una oficina con una sola impresora compartida. No quieres que cada empleado tenga su propia impresora; quieres que todos usen la misma. El Singleton funciona así:

  1. La clase tiene un constructor privado, nadie puede crear instancias directamente
  2. La clase tiene una variable estática que guarda la única instancia
  3. La clase provee un método público para obtener esa instancia
  4. La primera vez que se llama ese método, crea la instancia
  5. Las siguientes veces, devuelve la instancia ya creada

Un caso real: En un sistema de comercio electrónico, tienes un gestor de configuración que lee ajustes desde archivos o variables de entorno. No quieres leer estos archivos cada vez que necesitas una configuración; leerías el archivo cientos de veces innecesariamente. El Singleton te permite leer la configuración una vez y compartirla por toda la aplicación.

ConfigurationManager:
  - Tiene un constructor privado
  - Mantiene la configuración en memoria
  - Provee un método getInstance() que:
    - Si es la primera llamada: lee archivos, crea instancia, la guarda
    - Si no: devuelve la instancia guardada

Uso en cualquier parte de la aplicación:
  config = ConfigurationManager.getInstance()
  puerto = config.get("puerto_servidor")

Cuándo usarlo:

  • Cuando exactamente una instancia debe existir
  • Cuando esa instancia debe ser accesible desde múltiples partes del código
  • Cuando la inicialización de ese recurso es costosa

Cuándo no usarlo:

  • Cuando realmente no necesitas restricción de instancia única
  • Cuando dificulta el testing (los Singletons son difíciles de mockear)
  • Cuando crea dependencias globales ocultas en tu código

Factory Method: Delegando la creación

El problema: Tu código necesita crear objetos, pero no quieres que dependa de las clases concretas de esos objetos. Quieres flexibilidad para decidir qué clase específica instanciar, quizás basándote en configuración, contexto, o lógica de negocio.

La solución: El patrón Factory Method define una interfaz para crear objetos, pero deja que las subclases decidan qué clase instanciar. Delega la creación a métodos especializados.

Cómo funciona: Imagina una pizzería. No hay una sola receta de pizza; hay pizzas napolitanas, pizzas neoyorquinas, pizzas al estilo Chicago. Cada estilo tiene su propia forma de prepararse. El Factory Method funciona así:

  1. Defines una interfaz o clase abstracta que declara un método para crear objetos
  2. Las subclases concretas implementan ese método
  3. Cada subclase decide qué clase concreta instanciar
  4. El código cliente trabaja con la interfaz, no con clases concretas

Un caso real: En un sistema de notificaciones, puedes enviar mensajes por email, SMS, o notificaciones push. Cada canal tiene su propia implementación, pero tu código de negocio no debería preocuparse por los detalles.

NotificationService (clase abstracta):
  - Define método: createNotifier()
  - Usa ese método para obtener un notificador
  - Envía mensajes sin saber qué tipo de notificador es

EmailNotificationService (subclase):
  - Implementa createNotifier() para devolver EmailNotifier
  - Configura el servidor SMTP, autenticación, etc.

SMSNotificationService (subclase):
  - Implementa createNotifier() para devolver SMSNotifier
  - Configura el proveedor de SMS, credenciales, etc.

En tiempo de ejecución:
  service = decidir qué servicio usar basado en preferencias del usuario
  service.sendNotification("Tu pedido ha sido enviado")
  // El código no sabe si es email o SMS, simplemente funciona

Cuándo usarlo:

  • Cuando no sabes de antemano qué tipos de objetos necesitarás
  • Cuando quieres que las subclases especifiquen qué objetos crear
  • Cuando quieres localizar la lógica de creación en un lugar

Cuándo no usarlo:

  • Cuando solo tienes un tipo de objeto a crear
  • Cuando la creación es trivial y no cambiará
  • Cuando añade complejidad sin beneficio claro

Builder: Construyendo paso a paso

El problema: Algunos objetos son complejos de crear. Tienen muchos parámetros, algunos opcionales, otros obligatorios. El orden de inicialización importa. Crear estos objetos directamente resulta en constructores con docenas de parámetros o en objetos parcialmente inicializados.

La solución: El patrón Builder separa la construcción de un objeto complejo de su representación. Construyes el objeto paso a paso, y diferentes builders pueden crear diferentes representaciones usando el mismo proceso.

Cómo funciona: Imagina construir una casa. No la construyes instantáneamente; la construyes paso a paso: cimientos, estructura, paredes, techo, instalaciones, acabados. El Builder funciona así:

  1. Defines una interfaz Builder con métodos para construir cada parte
  2. Implementas builders concretos que construyen diferentes variantes
  3. Opcionalmente, usas un Director que conoce la secuencia de construcción
  4. El código cliente usa el builder para especificar qué quiere
  5. Al final, obtienes el producto completo

Un caso real: En un sistema de generación de reportes, diferentes usuarios quieren diferentes tipos de reportes. Algunos quieren PDFs con gráficos. Otros quieren CSVs simples. Otros quieren HTMLs interactivos.

ReportBuilder (interfaz):
  - setTitle(titulo)
  - setDateRange(inicio, fin)
  - addSection(nombre, datos)
  - addChart(tipo, datos)
  - setFormat(formato)
  - build() -> devuelve el reporte

PDFReportBuilder (implementación):
  - setTitle: configura título con fuente PDF
  - addChart: genera gráfico como imagen embebida
  - build: genera PDF con todas las secciones

CSVReportBuilder (implementación):
  - setTitle: lo pone como primera línea
  - addChart: no hace nada (CSV no soporta gráficos)
  - build: genera archivo CSV con los datos

Uso:
  builder = elegir builder según preferencia del usuario
  builder.setTitle("Ventas Mensuales")
  builder.setDateRange(enero, diciembre)
  builder.addSection("Resumen", datosResumen)
  builder.addChart("barras", datosVentas)
  reporte = builder.build()
  // Obtienes el reporte en el formato deseado

Cuándo usarlo:

  • Cuando un objeto tiene muchos parámetros de construcción
  • Cuando quieres crear diferentes representaciones del mismo objeto
  • Cuando el proceso de construcción debe ser independiente de las partes
  • Cuando quieres construir objetos inmutables paso a paso

Cuándo no usarlo:

  • Cuando el objeto es simple con pocos parámetros
  • Cuando no necesitas diferentes representaciones
  • Cuando un constructor simple es suficiente

Patrones estructurales en profundidad

Los patrones estructurales nos ayudan a ensamblar objetos y clases en estructuras más grandes mientras mantenemos estas estructuras flexibles y eficientes.

Adapter: El traductor universal

El problema: Tienes dos piezas de código que necesitan trabajar juntas, pero tienen interfaces incompatibles. Una espera recibir datos en un formato, la otra los proporciona en otro. Modificar cualquiera de las dos no es opción porque están en librerías externas, o porque romperías otro código que depende de ellas.

La solución: El patrón Adapter convierte la interfaz de una clase en otra interfaz que el cliente espera. Permite que clases con interfaces incompatibles trabajen juntas.

Cómo funciona: Piensa en los adaptadores de corriente cuando viajas. Tu laptop tiene un enchufe tipo A, pero en ese país los enchufes son tipo C. Necesitas un adaptador que tome tipo A por un lado y provea tipo C por el otro. El Adapter funciona igual:

  1. Tienes una interfaz que tu código cliente espera
  2. Tienes una clase existente con una interfaz diferente
  3. Creas una clase Adapter que implementa la interfaz esperada
  4. Internamente, el Adapter traduce las llamadas a la clase existente
  5. El cliente usa el Adapter como si fuera la interfaz original

Un caso real: Tu aplicación usa un servicio de pagos, pero decides cambiarlo por otro proveedor. El nuevo proveedor tiene una API completamente diferente. No quieres cambiar todo tu código de negocio; solo quieres cambiar cómo se procesa el pago.

PaymentProcessor (interfaz que tu código usa):
  - processPayment(amount, cardNumber, cvv)
  - refundPayment(transactionId)

OldPaymentService (tu implementación actual):
  - processPayment: llama a la API del proveedor viejo

NewPaymentServiceAdapter (nueva implementación):
  - Envuelve NewPaymentService (la API del nuevo proveedor)
  - processPayment:
    - Toma los parámetros en tu formato
    - Los traduce al formato que NewPaymentService espera
    - Llama a NewPaymentService.charge(...)
    - Traduce la respuesta a tu formato
    - Devuelve el resultado

Tu código de negocio:
  processor = NewPaymentServiceAdapter(newService)
  result = processor.processPayment(100, "1234...", "123")
  // Tu código no cambió, solo cambiaste el adapter

Cuándo usarlo:

  • Cuando quieres usar una clase existente con interfaz incompatible
  • Cuando quieres crear una clase reusable que coopere con clases imprevistas
  • Cuando necesitas integrar librerías de terceros
  • Cuando quieres aislar tu código de cambios en dependencias externas

Cuándo no usarlo:

  • Cuando puedes modificar la clase original
  • Cuando las interfaces ya son compatibles
  • Cuando añade complejidad innecesaria

Decorator: Añadiendo responsabilidades dinámicamente

El problema: Quieres añadir funcionalidad a objetos individuales, no a toda la clase. Usar herencia crearía una explosión de subclases. Quieres poder combinar diferentes funcionalidades de manera flexible.

La solución: El patrón Decorator añade responsabilidades a un objeto dinámicamente. Los decoradores proveen una alternativa flexible a la herencia para extender funcionalidad.

Cómo funciona: Imagina un café. Empiezas con café simple. Algunos clientes quieren leche. Otros quieren leche y azúcar. Otros quieren leche, azúcar y crema. Crear una clase para cada combinación sería absurdo. El Decorator funciona así:

  1. Tienes un componente base (el café simple)
  2. Creas decoradores que envuelven el componente
  3. Cada decorador añade su funcionalidad y delega al componente envuelto
  4. Puedes apilar múltiples decoradores
  5. El cliente ve todo como el mismo tipo de componente

Un caso real: En un sistema de logging, a veces quieres logs simples. A veces quieres logs con timestamps. A veces con timestamps y nivel de severidad. A veces todo lo anterior más el nombre del usuario que causó el log.

Logger (interfaz base):
  - log(message)

SimpleLogger (implementación base):
  - log: imprime el mensaje tal cual

TimestampDecorator (decorador):
  - Envuelve un Logger
  - log:
    - Añade timestamp al inicio del mensaje
    - Llama a logger.log(mensajeConTimestamp)

SeverityDecorator (decorador):
  - Envuelve un Logger
  - log:
    - Añade nivel de severidad
    - Llama a logger.log(mensajeConSeveridad)

Uso flexible:
  // Log simple
  logger = SimpleLogger()

  // Log con timestamp
  logger = TimestampDecorator(SimpleLogger())

  // Log con timestamp y severidad
  logger = SeverityDecorator(TimestampDecorator(SimpleLogger()))

  logger.log("Usuario inició sesión")
  // Output: "[ERROR] 2025-09-30 10:30:45 - Usuario inició sesión"

Cuándo usarlo:

  • Cuando quieres añadir responsabilidades a objetos individuales
  • Cuando la extensión por herencia es impráctica
  • Cuando quieres poder combinar comportamientos de manera flexible
  • Cuando quieres poder añadir/quitar funcionalidad en runtime

Cuándo no usarlo:

  • Cuando el comportamiento adicional es parte integral del objeto
  • Cuando crea demasiados objetos pequeños
  • Cuando la complejidad de apilar decoradores es confusa

Facade: Simplificando interfaces complejas

El problema: Tienes un subsistema complejo con muchas clases, interfaces y dependencias. Los clientes necesitan interactuar con este subsistema, pero no necesitan conocer todos sus detalles internos. Exponer toda la complejidad hace el subsistema difícil de usar.

La solución: El patrón Facade proporciona una interfaz unificada y simplificada a un conjunto de interfaces en un subsistema. Hace el subsistema más fácil de usar.

Cómo funciona: Imagina el panel de control de un home theater. Detrás del panel hay un amplificador, un reproductor de DVD, un sistema de sonido, luces inteligentes. Para ver una película, podrías configurar cada componente individualmente, o simplemente presionar “Ver Película”. El Facade funciona así:

  1. Tienes un subsistema complejo con múltiples componentes
  2. Creas una clase Facade que conoce ese subsistema
  3. El Facade expone métodos simples que orquestan los componentes
  4. Los clientes usan el Facade en lugar de los componentes directamente
  5. El subsistema sigue siendo accesible para necesidades avanzadas

Un caso real: En un sistema de e-commerce, procesar una orden involucra verificar inventario, procesar pago, crear registro de envío, actualizar analytics, enviar confirmación por email. Son múltiples subsistemas complejos.

OrderProcessingFacade:

  processOrder(order):
    // Coordina múltiples subsistemas

    1. inventoryService.checkAvailability(order.items)
    2. Si no hay stock suficiente:
         return error("Stock insuficiente")

    3. paymentResult = paymentService.charge(order.payment)
    4. Si pago falla:
         return error("Pago rechazado")

    5. inventoryService.reserveItems(order.items)
    6. shippingService.createShipment(order.address, order.items)
    7. analyticsService.trackSale(order)
    8. emailService.sendConfirmation(order.customerEmail)
    9. return success(order.id)

El código del controlador web:
  facade = OrderProcessingFacade(...)
  result = facade.processOrder(order)
  // Una línea en lugar de orquestar 8 servicios

Cuándo usarlo:

  • Cuando quieres proveer una interfaz simple a un subsistema complejo
  • Cuando hay muchas dependencias entre clientes y clases de implementación
  • Cuando quieres layering en tu sistema
  • Cuando quieres desacoplar subsistemas de clientes

Cuándo no usarlo:

  • Cuando el subsistema es simple
  • Cuando los clientes necesitan control fino del subsistema
  • Cuando crear el Facade es más complejo que usar el subsistema directamente

Patrones comportamentales en profundidad

Los patrones comportamentales se ocupan de la comunicación eficiente y la asignación de responsabilidades entre objetos.

Observer: Notificando a los interesados

El problema: Un objeto cambia de estado y múltiples otros objetos necesitan ser notificados automáticamente. Hacer que el objeto conozca a todos sus dependientes crea acoplamiento fuerte. Quieres que los objetos puedan suscribirse y desuscribirse dinámicamente.

La solución: El patrón Observer define una dependencia uno-a-muchos entre objetos. Cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.

Cómo funciona: Piensa en una suscripción a un periódico. Los lectores se suscriben, el periódico mantiene una lista de suscriptores, y cuando hay una nueva edición, se envía a todos. Los lectores pueden suscribirse o cancelar en cualquier momento. El Observer funciona así:

  1. Tienes un Subject (el observado) que mantiene estado
  2. Tienes Observers (los observadores) que quieren saber sobre cambios
  3. Los Observers se registran con el Subject
  4. Cuando el Subject cambia, notifica a todos los Observers registrados
  5. Los Observers reaccionan al cambio como necesiten

Un caso real: En un dashboard financiero, múltiples componentes muestran información de una acción: un gráfico de precio, un indicador de cambio porcentual, alertas de precio. Cuando el precio de la acción se actualiza, todos deben reflejarlo.

StockData (Subject observado):
  - Mantiene precio actual de una acción
  - Mantiene lista de observers registrados
  - Métodos:
    - attach(observer): añade un observer
    - detach(observer): remueve un observer
    - notify(): llama a update() en cada observer
    - setPrice(newPrice):
        - Actualiza el precio
        - Llama a notify()

PriceChartObserver (Observer):
  - update(stockData):
      - Obtiene nuevo precio
      - Redibuja el gráfico

PercentageIndicatorObserver (Observer):
  - update(stockData):
      - Calcula cambio porcentual
      - Actualiza el indicador

PriceAlertObserver (Observer):
  - update(stockData):
      - Si precio cruza un umbral
      - Envía alerta al usuario

Inicialización:
  stock = StockData("AAPL")
  stock.attach(PriceChartObserver())
  stock.attach(PercentageIndicatorObserver())
  stock.attach(PriceAlertObserver(umbral=150))

Cuando llega nuevo precio:
  stock.setPrice(152.30)
  // Automáticamente todos los observers se actualizan

Cuándo usarlo:

  • Cuando un cambio en un objeto requiere cambiar otros
  • Cuando un objeto debe notificar a otros sin saber quiénes son
  • Cuando quieres acoplamiento bajo entre objetos
  • Cuando necesitas broadcast de eventos

Cuándo no usarlo:

  • Cuando hay un solo observador (usa llamada directa)
  • Cuando el orden de notificación importa
  • Cuando la cascada de actualizaciones puede ser compleja

Strategy: Intercambiando algoritmos

El problema: Tienes una familia de algoritmos relacionados. Quieres que sean intercambiables. No quieres que el cliente conozca los detalles de cada algoritmo. Cambiar de algoritmo no debería requerir cambiar el código cliente.

La solución: El patrón Strategy define una familia de algoritmos, encapsula cada uno, y los hace intercambiables. Strategy permite que el algoritmo varíe independientemente de los clientes que lo usan.

Cómo funciona: Imagina que vas del punto A al punto B. Puedes ir caminando, en bicicleta, en auto, en transporte público. El destino es el mismo, pero la estrategia para llegar varía. El Strategy funciona así:

  1. Defines una interfaz común para todos los algoritmos
  2. Implementas cada algoritmo como una clase separada
  3. El contexto mantiene una referencia a una estrategia
  4. El cliente puede cambiar la estrategia en runtime
  5. El contexto delega el trabajo a la estrategia actual

Un caso real: En un sistema de e-commerce, calcular el costo de envío depende del método elegido: envío estándar es más barato pero lento, envío express es más caro pero rápido, envío overnight es carísimo pero inmediato.

ShippingStrategy (interfaz):
  - calculateCost(package) -> devuelve costo
  - estimateDeliveryTime(package) -> devuelve días

StandardShippingStrategy:
  - calculateCost:
      - Calcula basado solo en peso
      - Usa tarifa económica
  - estimateDeliveryTime:
      - Devuelve 5-7 días

ExpressShippingStrategy:
  - calculateCost:
      - Calcula basado en peso y distancia
      - Usa tarifa premium
  - estimateDeliveryTime:
      - Devuelve 2-3 días

OvernightShippingStrategy:
  - calculateCost:
      - Calcula con tarifa muy alta
      - Añade cargo de urgencia
  - estimateDeliveryTime:
      - Devuelve 1 día

ShoppingCart (contexto):
  - items
  - shippingStrategy

  setShippingStrategy(strategy):
    - Guarda la estrategia elegida

  calculateTotal():
    - Suma precio de items
    - Añade shippingStrategy.calculateCost(items)
    - Devuelve total

Uso:
  cart = ShoppingCart()
  cart.addItem(producto1)
  cart.addItem(producto2)

  // Cliente elige envío estándar
  cart.setShippingStrategy(StandardShippingStrategy())
  total = cart.calculateTotal()  // $135

  // Cliente cambia a express
  cart.setShippingStrategy(ExpressShippingStrategy())
  total = cart.calculateTotal()  // $155

Cuándo usarlo:

  • Cuando tienes múltiples variantes de un algoritmo
  • Cuando quieres cambiar algoritmos en runtime
  • Cuando quieres evitar condicionales complejos
  • Cuando los algoritmos deben ser intercambiables

Cuándo no usarlo:

  • Cuando solo tienes un algoritmo
  • Cuando los algoritmos son muy similares
  • Cuando el cliente no debería elegir el algoritmo

Command: Encapsulando peticiones como objetos

El problema: Quieres parametrizar objetos con operaciones. Quieres poner operaciones en cola, registrarlas, o deshacerlas. Quieres desacoplar el objeto que invoca la operación del objeto que sabe cómo realizarla.

La solución: El patrón Command encapsula una petición como un objeto, permitiendo parametrizar clientes con diferentes peticiones, encolar peticiones, registrar peticiones, y soportar operaciones reversibles.

Cómo funciona: Imagina un control remoto universal. Cada botón ejecuta un comando diferente: encender TV, cambiar canal, ajustar volumen. El control no sabe cómo funciona cada dispositivo; solo envía comandos. El Command funciona así:

  1. Defines una interfaz Command con un método execute()
  2. Creas comandos concretos que encapsulan una acción y su receptor
  3. Un invoker mantiene comandos y los ejecuta
  4. Los comandos saben qué objeto receptor llamar y con qué parámetros
  5. Opcionalmente, los comandos pueden tener un método undo()

Un caso real: En un editor de texto, tienes operaciones: escribir texto, borrar, copiar, pegar, formatear. Quieres soportar deshacer/rehacer estas operaciones. Cada operación debe ser reversible.

Command (interfaz):
  - execute()
  - undo()

WriteTextCommand:
  - Guarda: el editor, el texto a escribir, la posición
  - execute():
      - Inserta el texto en el editor
      - Guarda la posición para undo
  - undo():
      - Borra el texto insertado

DeleteTextCommand:
  - Guarda: el editor, la posición, la longitud
  - execute():
      - Guarda el texto a borrar
      - Borra el texto del editor
  - undo():
      - Re-inserta el texto guardado

FormatTextCommand:
  - Guarda: el editor, el rango, el formato nuevo, el formato previo
  - execute():
      - Aplica el nuevo formato
  - undo():
      - Restaura el formato previo

TextEditor:
  - Mantiene dos pilas: historyCommands, redoCommands

  executeCommand(command):
    - command.execute()
    - Añade command a historyCommands
    - Limpia redoCommands

  undo():
    - Toma último command de historyCommands
    - command.undo()
    - Mueve command a redoCommands

  redo():
    - Toma último command de redoCommands
    - command.execute()
    - Mueve command a historyCommands

Uso:
  editor = TextEditor()

  // Usuario escribe "Hello"
  cmd1 = WriteTextCommand(editor, "Hello", 0)
  editor.executeCommand(cmd1)

  // Usuario formatea como bold
  cmd2 = FormatTextCommand(editor, range(0,5), "bold")
  editor.executeCommand(cmd2)

  // Usuario hace undo
  editor.undo()  // Se deshace el formato
  editor.undo()  // Se deshace el "Hello"

  // Usuario hace redo
  editor.redo()  // Reaparece "Hello"

Cuándo usarlo:

  • Cuando quieres parametrizar objetos con operaciones
  • Cuando quieres encolar, registrar, o deshacer operaciones
  • Cuando quieres desacoplar invoker de receiver
  • Cuando quieres soportar transacciones

Cuándo no usarlo:

  • Cuando las operaciones son triviales
  • Cuando no necesitas deshacer/rehacer
  • Cuando añade complejidad innecesaria

Cuándo usar patrones y cuándo no

Los patrones de diseño son herramientas poderosas, pero pueden ser mal usados. He visto código que aplica patrones indiscriminadamente, convirtiendo soluciones simples en laberintos de abstracciones. También he visto código que los necesita desesperadamente pero no los usa.

Señales de que necesitas un patrón

Estás resolviendo un problema conocido: Si tu problema suena familiar, probablemente hay un patrón para él. “Necesito notificar múltiples objetos cuando algo cambia” es Observer. “Necesito crear objetos sin especificar sus clases exactas” es Factory.

Estás repitiendo código: Si encuentras código similar en múltiples lugares, probablemente hay una abstracción faltante. Los patrones a menudo eliminan duplicación al extraer lo que varía.

Tu código es rígido: Si cambiar algo requiere modificar múltiples lugares no relacionados, probablemente necesitas mejor separación de responsabilidades. Los patrones ayudan a desacoplar.

Testing es difícil: Si no puedes probar un componente aisladamente, probablemente está muy acoplado. Patrones como Strategy, Observer, y Dependency Injection facilitan el testing.

Señales de que estás abusando de patrones

Complejidad innecesaria: Si has creado cinco clases para resolver un problema que requería una función, probablemente estás sobre-diseñando. YAGNI (You Aren’t Gonna Need It) es un principio válido.

Nadie entiende el código: Si tu equipo necesita un diagrama UML para entender un flujo simple, probablemente hay demasiadas abstracciones. El código debe comunicar intención.

Cambios simples son difíciles: Irónicamente, los patrones mal aplicados pueden hacer el código más rígido. Si añadir un campo requiere cambios en seis clases, algo está mal.

Estás aplicando patrones “porque sí”: Si no puedes articular qué problema específico resuelve un patrón en tu contexto, probablemente no lo necesitas. Los patrones deben tener justificación clara.

La evolución de los patrones en tu código

Los patrones de diseño rara vez se implementan desde el principio. Generalmente emergen a medida que el código evoluciona y se refactoriza. Este es el ciclo natural:

Fase 1: Código simple

Empiezas con la solución más simple que funciona. No hay patrones, no hay abstracciones complejas. Solo código que resuelve el problema inmediato. Esto está bien. Es exactamente donde deberías empezar.

Fase 2: Identificar el problema

A medida que el código crece, empiezas a ver problemas: duplicación, acoplamiento, rigidez. Aquí es donde prestas atención. ¿Este problema es conocido? ¿Hay un patrón que lo aborde?

Fase 3: Refactorizar hacia el patrón

No reescribas todo desde cero. Refactoriza gradualmente hacia el patrón. Mueve una pieza a la vez. Mantén los tests pasando. Este proceso incremental es más seguro y te permite aprender.

Fase 4: Validar la mejora

¿El código es más flexible? ¿Es más fácil de entender? ¿Facilita el testing? Si la respuesta es sí, el patrón fue útil. Si no, considera revertir. No todos los patrones funcionan en todos los contextos.

Comunicándose con patrones

Uno de los beneficios más subestimados de los patrones de diseño es cómo mejoran la comunicación en equipos.

Lenguaje compartido

Cuando dices “usemos Observer para esto”, todos en el equipo inmediatamente entienden la estructura, las responsabilidades, y las implicaciones. No necesitas explicar cómo funcionará la comunicación entre objetos; el patrón lo comunica.

Documentación implícita

El código que sigue patrones reconocibles es auto-documentado. Un desarrollador nuevo puede ver clases llamadas “Builder” o “Factory” y entender su propósito sin leer documentación.

Revisiones de código más efectivas

En revisiones, puedes discutir si un patrón es apropiado en lugar de debatir detalles de implementación. “¿Realmente necesitamos Strategy aquí?” es una pregunta más productiva que “¿Por qué creaste esta interfaz?”

Onboarding más rápido

Nuevos miembros del equipo que conocen patrones pueden entender codebases grandes más rápido. Los patrones son puntos de referencia familiares en terreno desconocido.

Recursos para profundizar

Los patrones de diseño son un tema profundo. Este artículo es una introducción, pero hay mucho más que explorar.

Lectura fundamental

“Design Patterns: Elements of Reusable Object-Oriented Software” por Gang of Four El libro original que catálogo los 23 patrones clásicos. Es denso y usa Smalltalk/C++, pero es el fundamento del conocimiento de patrones.

“Head First Design Patterns” por Freeman y Freeman Mucho más accesible que el Gang of Four. Usa Java pero los conceptos son universales. Excelente para comenzar.

“Refactoring to Patterns” por Joshua Kerievsky Muestra cómo evolucionar código existente hacia patrones. Extremadamente práctico.

Conceptos relacionados

SOLID Principles Los principios que subyacen muchos patrones. Entender SOLID te ayuda a entender por qué los patrones funcionan.

Domain-Driven Design Complementa patrones al enfocarse en modelar el dominio del negocio correctamente.

Clean Code Los patrones son parte de escribir código limpio, pero no lo son todo. Este libro provee el contexto más amplio.

Práctica deliberada

Refactoring Kata Ejercicios pequeños donde practicas identificar problemas y aplicar patrones. El Gilded Rose Kata es excelente.

Code Reviews Presta atención a patrones en code reviews. Tanto cuando están ausentes como cuando están presentes.

Proyectos Open Source Lee código de proyectos bien diseñados. Frameworks como Spring, Django, o Rails usan patrones extensivamente.


Los patrones de diseño no son recetas mágicas ni reglas absolutas. Son lecciones aprendidas por generaciones de desarrolladores, codificadas en forma reusable. Algunos resolverán problemas que enfrentas hoy. Otros te preparan para problemas que enfrentarás mañana.

He visto equipos transformarse al adoptar patrones. Código que era un desastre se vuelve mantenible. Comunicación que era confusa se vuelve clara. Desarrolladores que reinventaban la rueda empiezan a construir sobre conocimiento establecido.

Pero también he visto el daño que causan los patrones mal aplicados. Código simple convertido en laberintos de abstracciones. Desarrolladores más preocupados por usar patrones “correctamente” que por resolver problemas reales. Soluciones que lucen elegantes en diagramas UML pero son pesadillas de mantener en producción.

La sabiduría está en el equilibrio. Aprende los patrones. Entiende sus trade-offs. Úsalos cuando resuelvan problemas reales. No los uses solo porque los conoces. Y recuerda: el mejor código a menudo es el más simple que funciona.

“El diseño es el arte de balancear fuerzas conflictivas hacia un resultado óptimo”

Tu trabajo no es memorizar patrones. Tu trabajo es entender problemas profundamente y elegir soluciones apropiadas. A veces esa solución es un patrón establecido. A veces es algo completamente nuevo. La diferencia entre un desarrollador junior y uno senior no es cuántos patrones conoce, sino qué tan bien sabe cuándo aplicarlos y cuándo no.

Los patrones de diseño son un vocabulario compartido. Úsalo para comunicarte con tu equipo. Úsalo para aprender de la experiencia acumulada de miles de desarrolladores. Pero nunca pierdas de vista que son medios para un fin, no el fin en sí mismos. El fin es software que resuelve problemas reales de manera mantenible, flexible, y comprensible.

Ese es el verdadero propósito de los patrones: ayudarte a escribir mejor software, no software más complicado.

Tags

#design-patterns #software-design #best-practices #architecture