Original article: 4 Design Patterns You Should Know for Web Development: Observer, Singleton, Strategy, and Decorator

¿Alguna vez has estado en un equipo donde necesitas empezar un proyecto desde el principio? Ese es usualmente el caso en muchas start-ups y otras compañías pequeñas.

Hay demasiados lenguajes de programación distintos, arquitecturas, y otras preocupaciones que puede ser difícil decidir por dónde empezar. Ahí es donde los patrones de diseño entran.

Un patrón de diseño es como una plantilla para tu proyecto. Usa ciertas convenciones y puedes esperar un comportamiento de tipo específico. Estos patrones fueron inventados según las diversas experiencias de los desarrolladores, así que son realmente como distintos conjuntos de mejores prácticas.

Y tú y tu equipo deciden cuál conjunto de mejores prácticas es la más útil para su proyecto. Según el patrón de diseño que elijas, todos empezarán a tener expectativas de lo que el código debería estar haciendo y qué vocabulario todos estarán usando.

Los Patrones de Diseño de Programación pueden ser usados en todos los lenguajes de programación y pueden ser usados para coincidir en cualquier proyecto porque te dan solamente un esquema general de una solución.

Hay 23 patrones oficiales del libro Design Patterns - Elements of Reusable Object-Oriented Software, el cual es considerado uno de los libros más influyentes en la teoría de orientación a objetos y desarrollo de software.

En este artículo, voy a cubrir cuatro de esos patrones de diseño para darte una idea de lo que son algunos de los patrones y cuando los usarías.

El patrón de diseño singleton

El patrón singleton solamente permite a una clase u objeto tener una sola instancia y usa una variable global para almacenar esa instancia. Puedes usar carga perezosa para asegurar que hay solamente una instancia de la clase, porque creará la clase solamente cuando lo necesites.

Eso previene a múltiples instancias de estar activas al mismo tiempo, lo cual podría causar errores raros. La mayoría de las veces esto se implementa en el constructor. El objetivo del patrón singleton es típicamente regular el estado global de una aplicación.

Un ejemplo de un singleton es que probablemente uses todo el tiempo tu registrador.

Si trabajas con algunos de los frameworks de front-end como React o Angular, sabes todo lo tramposo que puede ser manejar registros viniendo de múltiples componentes. Este es un gran ejemplo para los singletons en acción porque nunca quieres más de una instancia de un objeto registrador, especialmente si estás usando algún tipo de herramienta de rastreo de error.

class RegistradorComida {
  constructor() {
    this.registroComida = []
  }
    
  registrar(orden) {
    this.registroComida.push(orden.itemComida)
    // hacer un código elegante para enviar este registro a algún lugar
  }
}

// este es el singleton
class SingletonRegistradorComida {
  constructor() {
    if (!SingletonRegistradorComida.instance) {
      SingletonRegistradorComida.instance = new RegistradorComida()
    }
  }
  
  getInstanciaRegistradorComida() {
    return SingletonRegistradorComida.instance
  }
}

module.exports = SingletonRegistradorComida
Un ejemplo de la clase singleton

Ahora, no tienes que preocuparte de perder registros de múltiples instancias porque solamente tienes una en tu proyecto. Así que cuando quieras registrar la comida que ha sido ordenada, puedes usar la misma instancia de RegistradorComida  a lo largo de múltiples archivos o componentes.

const RegistradorComida = require('./RegistradorComida')

const registradorComida = new RegistradorComida().getInstanciaRegistradorComida()

class Cliente {
  constructor(orden) {
    this.precio = orden.precio
    this.comida = orden.itemComida
    registradorComida.registrar(orden)
  }
  
  // otras cosas geniales realizando para el cliente
}

module.exports = Cliente
Un ejemplo de una clase Cliente usando el singleton
const RegistradorComida = require('./RegistradorComida')

const registradorComida = new RegistradorComida().getInstanciaRegistradorComida()

class Restaurante {
  constructor(inventario) {
    this.cantidad = inventario.cuenta
    this.comida = inventario.itemComida
    registradorComida.registrar(inventario)
  }
  
  // otras cosas realizando en el restaurante
}

module.exports = Restaurante
Un ejemplo de la clase Restaurante usando el mismo singleton como la clase Cliente

Con este patrón singleton en lugar, no tienes que preocuparte sobre obtener los registros del archivo principal. Puedes obtenerlos de cualquier lugar en la base de tu código e irán todos a la misma instancia exacta del registrador, lo cual significa que ninguno de tus registros deberían perderse debido a nuevas instancias.

El patrón de diseño estrategia

El patrón estrategia es como una versión avanzada de una sentencia if else. Es básicamente donde haces una interfaz para un método que tienes en tu clase base. Esta interfaz es usada luego para encontrar la implementación correcta de ese método que debería ser usado en una clase derivada. La implementación, en este caso, será decidida en el tiempo de ejecución basado en el cliente.

Este patrón es increíblemente útil en situaciones donde tienes métodos requeridos y opcionales para una clase. Algunas instancias de esa clase no necesitarán los métodos opcionales, y eso causa un problema para soluciones de herencia. Podrías usar interfaces para los métodos opcionales, pero después tendrías que escribir la implementación cada vez que uses la clase, ya que no habría una implementación por defecto.

Ahí es donde el patrón estrategia nos salva. En vez de que el cliente busque una implementación, lo delega a la interfaz de estrategia y la estrategia encuentra la implementación correcta. Un uso común para esto son los sistemas de procesamiento de pago.

Podrías tener un carrito de compras que solamente permita a los clientes pagar con sus tarjetas de crédito, pero perderás clientes que quieran usar otros métodos de pago.

El patrón de diseño estrategia nos permite desacoplar los métodos de pago desde el proceso de pago, lo cual significa que podemos agregar o actualizar estrategias sin cambiar ningún código en el carrito de compras o en el proceso de pago.

Aquí hay un ejemplo de una implementación del patrón estrategia usando el ejemplo del método de pago.

class EstrategiaMetodoPago {

  const tipoInfoCliente = {
    pais: string
    direccionEmail: string
    nombre: string
    numeroCuenta?: number
    direccion?: string
    numeroTarjeta?: number
    ciudad?: string
    numeroRuta?: number
    estado?: string
  }
  
  static CuentaBanco(infoCliente: tipoInfoCliente) {
    const { nombre, numeroCuenta, numeroRuta } = infoCliente
    // hace cosas para obtener el pago
  }
  
  static BitCoin(infoCliente: tipoInfoCliente) {
    const { direccionEmail, numeroCuenta } = infoCliente
    // hace cosas para obtener el pago
  }
  
  static TarjetaCredito(infoCliente: tipoInfoCliente) {
    const { nombre, tarjetaCredito, direccionEmail } = infoCliente
    // hace cosas para obtener el pago
  }
  
  static CorreoEntrante(infoCliente: tipoInfoCliente) {
    const { nombre, direccion, ciudad, estado, pais } = infoCliente
    // hace cosas para obtener el pago
  }
  
  static PayPal(infoCliente: tipoInfoCliente) {
    const { direccionEmail } = infoCliente
    // hace cosas para obtener el pago
  }
}
Un ejemplo de la implementación del patrón strategy

Para implementar nuestra estrategia del método de pago, hicimos una sola clase con múltiples métodos estáticos. Cada método toma el mismo parámetro, infoCliente, y ese parámetro tiene un tipo definido de tipoInfoCliente. (Hey, todos ustedes desarrolladores de TypeScript!??) Toma nota de que cada método tiene su propia implementación y usa valores distintos del infoCliente.

Con el patrón estrategia, también puedes cambiar dinámicamente la estrategia que es usada en tiempo de ejecución. Eso significa que serás capaz de cambiar la estrategia, o la implementación del método, que está siendo usado según la entrada de usuario o el entorno donde la app está corriendo.

También puedes establecer una implementación por defecto en un simple archivo config.json, así:

{
  "metodoPago": {
    "estrategia": "PayPal"
  }
}
config.json para establecer la implementación por defecto del metodoPago a "PayPal" en tiempo de ejecutación

Cuando sea que el cliente comience a ir a través del proceso de pago en tu sitio web, el método de pago por defecto que encuentren será la implementación de PayPal el cual viene del config.json. Esto podría fácilmente ser actualizado si el cliente selecciona un método de pago diferente.

Ahora crearemos un archivo para nuestro proceso de pago.

const EstrategiaMetodoPago = require('./EstrategiaMetodoPago')
const config = require('./config')

class Pago {
  constructor(estrategia='TarjetaCredito') {
    this.estrategia = EstrategiaMetodoPago[estrategia]
  }
  
  // hacer algo de código elegante aquí y obtener entrada del usuario y método de pago
  
  cambiarEstrategia(nuevaEstrategia) {
    this.estrategia = EstrategiaMetodoPago[nuevaEstrategia]
  }
  
  const entradaUsuario = {
    nombre: 'Malcolm',
    numeroTarjeta: 3910000034581941,
    direccionEmail: 'mac@gmailer.com',
    pais: 'US'
  }
  
  const estrategiaSeleccionada = 'Bitcoin'
  
  cambiarEstrategia(estrategiaSeleccionada)
  
  postPago(entradaUsuario) {
    this.estrategia(entradaUsuario)
  }
}

module.exports = new Pago(config.metodoPago.estrategia)

Esta clase Pago es donde el patrón estrategia viene a mostrarse. Importamos un par de archivos, así tenemos las estrategias de método de pago disponibles y la estrategia por defecto del config.

Entonces creamos la clase con el constructor y un valor de reserva para la estrategia por defecto en caso de que uno no haya sido puesto en el config. Después asignamos el valor estrategia a una variable de estado local.

Un método importante que necesitamos implementar en nuestra clase Pago es la habilidad de cambiar la estrategia de pago. Un cliente podría cambiar el método de pago que quieran usar y necesitarás ser capaz de manejar eso. Para eso está el método cambiarEstrategia.

Después que hayas hecho algún código elegante y hayas obtenido todas las entradas del cliente, entonces puedes actualizar la estrategia de pago inmediatamente según su entrada y pone la estrategia dinámicamente antes de que el pago sea enviado para ser procesado.

En algún punto podrías necesitar agregar más métodos de pago a tu carrito de compras y todo lo que tendrás que hacer es agregarlo a la clase EstrategiaMetodoPago. Estará disponible al instante en cualquier lugar donde la clase sea usada.

El patrón de diseño estrategia es poderoso cuando lidias con métodos que tienen múltiples implementaciones. Podría sentirse como que estás usando una interfaz, pero no tienes que escribir una implementación para el método cada vez que lo llames en una clase distinta. Te da más flexibilidad que las interfaces.

El patrón de diseño observador

Si alguna vez has usado el patrón MVC, ya has usado el patrón de diseño observador. La parte Modelo es como un sujeto y la parte Vista es como un observador de ese sujeto. Tu sujeto sostiene todos los datos y el estado de esos datos. Después tienes los observadores, como componentes distintos, que obtendrá esos datos del sujeto cuando los datos hayan sido actualizados.

El objetivo del patrón de diseño observador es crear la relación uno-a-muchos entre el sujeto y todos los observadores que esperan recibir datos para que sean actualizados. Así que en cualquier tiempo que el estado del sujeto cambie, todos los observadores serán notificados y actualizados instantáneamente.

Algunos ejemplos de cuando usarías este patrón incluyen: enviar notificaciones de usuario, actualizar, filtros, y manejar suscriptores.

Digamos que tienes una aplicación de una sola página que tiene tres características de listas despegables que son dependientes de la selección de una categoría de un nivel superior del despegable. Esto es común en muchos sitios de compras, como Depósito de Casa. Tienes un montón de filtros en la página que son dependientes del valor de un filtro de nivel superior.

El código para el despegable de nivel superior podría parecerse algo así:

class CategoriaDespegable {
  constructor() {
    this.categorias = ['accesorios', 'puertas', 'herramientas']
    this.suscriptor = []
  }
  
  // pretén que hay algún código elegante aquí
  
  suscribir(observador) {
    this.suscriptor.push(observador)
  }
  
  alCambiar(categoriaSeleccionada) {
    this.suscriptor.forEach(observador => observador.actualizar(categoriaSeleccionada))
  }
}
El sujeto que actualiza los observadores

Este archivo CategoriaDespegable es una simple clase con un constructor que inicializa las opciones de categoría que tenemos disponibles en el despegable. Este es el archivo que manejarías recuperando una lista del back-end o cualquier tipo de ordenación que quieras hacer antes de que el usuario vea las opciones.

El método suscribir es como cada filtro creado con esta clase recibirá actualizaciones sobre el estado del observador.

El método alCambiar es cómo mandamos notificaciones a todos los suscriptores de que un cambio de estado ha sucedido en el observador que está escuchando. Solo iteramos a través de todos los suscriptores y llamamos a su método actualizar con el categoriaSeleccionada.

El código para los otros filtros podría parecerse así:

class FiltrarDespegable {
  constructor(tipoFiltro) {
    this.tipoFiltro = tipoFiltro
    this.items = []
  }
  
  // más código elegante aquí; tal vez hacer una llamada a la API para obtener una lista de items según el tipo de filtro
  
  actualizar(categoria) {
    fetch('https://example.com')
      .then(res => this.items(res))
  }
}
A observador potencial del sujeto

Este archivo FiltroDespegable es otra simple clase que representa todos los despegables potenciales que podríamos usar en una página. Cuando una nueva instancia de esta clase es creada, un tipoFiltro necesita ser pasado. Esto podría ser usado para hacer llamadas específicas a la API para tener la lista de items.

El método actualizar es una implementación de lo que puedes hacer con la nueva categoría una vez que ha sido enviado desde el observador.

Ahora veremos qué significa usar estos archivos con el patrón observador:

const CategoriaDespegable = require('./CategoriaDespegable')
const FiltroDespegable = require('./FiltroDespegable')

const categoriaDespegable = new CategoriaDespegable() 

const despegableColores = new FiltroDespegable('colores')
const despegablePrecio = new FiltroDespegable('precio')
const despegableMarca = new FiltroDespegable('marca')

categoriaDespegable.suscribir(despegableColores)
categoriaDespegable.suscribir(despegablePrecio)
categoriaDespegable.suscribir(despegableMarca)
Un ejemplo del patrón observador en acción

Lo que este archivo nos muestra es que tenemos 3 despegables que son suscriptores a la categoría observable despegable. Luego suscribimos a cada uno de estos despegables al observador. Cuando sea que la categoría del observador es actualizada, enviará el valor a cada suscriptor el cual actualizará las listas despegables individuales instantáneamente.

El patrón de diseño decorador

Usar el patrón de diseño decorador es bastante simple. Puedes tener una clase base con métodos y propiedades que están presentes cuando haces un nuevo objeto con la clase. Ahora, digamos que tienes algunas instancias de la clase que necesita métodos o propiedades que no vinieron de la clase base.

Puedes agregar esos métodos y propiedades extras a la clase base, pero eso podría arruinar tus otras instancias. Podrías inclusive hacer sub-clases para mantener métodos y propiedades específicos que necesitas que no puedes poner en tu clase base.

Cualquiera de esos enfoques resolverán tu problema,pero son torpes e ineficientes. Ahí es donde el patrón decorador interviene. En vez de hacer feo a tu código base solo para agregar un poco de cosas a una instancia de un objeto, puedes agregar esas cosas específicas directamente a la instancia.

Así que si necesitas agregar una nueva propiedad que mantiene el precio de un objeto, puedes usar el patrón decorador para agregarlo directamente a esa instancia de objeto particular y no afectará ninguna de las otras instancias de ese objeto de clase.

¿Alguna vez has ordenado comida en línea? Entonces probablemente has encontrado el patrón decorador. Si vas a pedir un sandwich y quieres agregar coberturas especiales, el sitio web no agrega esas coberturas a cada instancia de sandwich que los usuarios actuales están tratando de ordenar.

Aquí hay un ejemplo de una clase cliente:

class Cliente {
  constructor(balance=20) {
    this.balance = balance
    this.itemsComida = []
  }
  
  comprar(comida) {
    if (comida.precio) < this.balance {
      console.log('deberías comprarlo')
      this.balance -= comida.precio
      this.itemsComida.push(comida)
    }
    else {
      console.log('tal vez deberías comprar otra cosa')
    }
  }
}

module.exports = Cliente
Un ejemplo de una clase Cliente

Y aquí hay un ejemplo de la clase Sandwich:

class Sandwich {
  constructor(tipo, precio) {
    this.tipo = tipo
    this.precio = precio
  }
  
  ordenar() {
    console.log(`Ordenaste un sandwich ${this.tipo} por $ ${this.precio}.`)
  }
}

class SandwichDeLujo {
  constructor(baseSandwich) {
    this.tipo = `${baseSandwich.tipo} Lujoso`
    this.precio = baseSandwich.precio + 1.75
  }
}

class SandwichExquisito {
  constructor(baseSandwich) {
    this.tipo = `${baseSandwich.tipo} Exquisito `
    this.precio = baseSandwich.precio + 10.75
  }
  
  ordenar() {
    console.log(`Ordenaste un sandwich ${this.tipo}. Tiene todo lo que necesitas para ser feliz por muchos días.`)
  }
}

module.exports = { Sandwich, SandwichDeLujo, SandwichExquisito }
Un ejemplo de una clase sandwich

En esta clase sandwich es donde se usa el patrón decorador. Tenemos una clase base Sandwich que pone las reglas de lo que sucede cuando una sandwich regular es ordenado. Los clientes podrían querer mejorar los sandwiches y eso sólo significa un cambio de ingrediente y de precio.

Solo quisiste agregar la funcionalidad incrementar el precio y actualizar el tipo de sandwich para el SandwichDeLujo sin cambiar la forma cómo es ordenado. Aunque podrías necesitar una orden distinta para un SandwichExiquisito porque hay un cambio drástico en la calidad de los ingredientes.

El patrón decorador te permite cambiar dinámicamente la clase base sin afectarlo o a ninguna de las otras clases. No tienes que preocuparte sobre implementar funciones que no conoces, como con interfaces, y no tienes que incluir propiedades que no usarás en cada clase.

Ahora iremos sobre un ejemplo donde esta clase se instancia como si el cliente estuviera haciendo un pedido de un sandwich.

const { Sandwich, SandwichDeLujo, SandwichExquisito } = require('./Sandwich')
const Cliente = require('./Cliente')

const cli1 = new Cliente(57)

const sandwhichPavo = new Sandwich('Pavo', 6.49)
const sandwichBLT = new Sandwich('BLT', 7.55)

const sandwichBltDeLujo = new SandwichDeLujo(sandwichBLT)
const sandwichPavoExquisito = new SandwichExquisito(sandwichPavo)

cust1.comprar(sandwichPavo)
cust1.comprar(sandwichBLT)

Pensamientos finales

Solía pensar que los patrones de diseño eran estas guías difíciles y extravagantes de desarrollo de software. ¡Luego descubrí que los uso todo el tiempo!

Unos de los patrones que cubrí son usados en muchísimas aplicaciones que te dejaría boquiabierto. Son sólo teorías al fin y al cabo. Depende de nosotros como desarrolladores usar esa teoría de manera que haga a nuestras aplicaciones fáciles de implementar y de mantener.

¿Has usado algunos de los otros patrones de diseño para tus proyectos? La mayoría de los lugares usualmente toman un patrón de diseño para sus proyectos y se apegan a él, así que me gustaría escuchar de todos ustedes sobre cuál usan.

Gracias por leer. Deberías seguirme en Twitter porque usualmente posteo cosas útiles / entretenidas: @FlippedCoding