Patrones de diseño de JavaScript

La guía definitiva para los patrones de diseño más útiles.

NOTA DE ACTUALIZACIÓN: Se actualizó el ejemplo de Patrón de proxy para usar ES6 Proxy y Reflect. Se reemplazaron las imágenes de fragmentos de código fuente con lo esencial de GitHub.

En este artículo, vamos a hablar sobre patrones de diseño que pueden y deben usarse para escribir un código JavaScript mejor y mantenible. Supongo que tiene una comprensión básica de JavaScript y conceptos como clases (las clases en JavaScript pueden ser complicadas), objetos, herencia de prototipos, cierres, etc.

Este artículo es una lectura larga en su conjunto debido a la naturaleza del tema, por lo que he tratado de mantener las secciones independientes. De modo que usted, como lector, puede elegir partes específicas (o, en este caso, patrones específicos) e ignorar las que no le interesan o con las que está muy familiarizado. Ahora, comencemos.

Nota: El código fuente para la implementación de todos los patrones de diseño explicados aquí está en GitHub.

Introducción

Escribimos código para resolver problemas. Estos problemas generalmente tienen muchas similitudes y, cuando intentamos resolverlos, notamos varios patrones comunes. Aquí es donde entran los patrones de diseño.

Un patrón de diseño es un término utilizado en ingeniería de software para una solución general y reutilizable a un problema común en el diseño de software.

El concepto subyacente de los patrones de diseño ha existido en la industria de la ingeniería de software desde el principio, pero en realidad no estaban tan formalizados. Patrones de diseño: elementos de software orientado a objetos reutilizables escritos por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides, la famosa Gang of Four (GoF), fueron fundamentales para impulsar el concepto formalizado de patrones de diseño en ingeniería de software. Ahora, los patrones de diseño son una parte esencial del desarrollo de software y lo han sido durante mucho tiempo.

Hubo 23 patrones de diseño introducidos en el libro original.

Los 23 patrones clásicos introducidos por GoF

Los patrones de diseño son beneficiosos por varias razones. Son soluciones comprobadas que los veteranos de la industria han probado y probado. Son enfoques sólidos que resuelven problemas de una manera ampliamente aceptada y reflejan la experiencia y los conocimientos de los desarrolladores líderes de la industria que ayudaron a definirlos. Los patrones también hacen que su código sea más reutilizable y legible al tiempo que acelera enormemente el proceso de desarrollo.

Los patrones de diseño no son soluciones terminadas. Solo nos proporcionan enfoques o esquemas para resolver un problema.

Nota: En este artículo, hablaremos principalmente sobre patrones de diseño desde un punto de vista orientado a objetos y en el contexto de su usabilidad en JavaScript moderno. Es por eso que se pueden omitir muchos patrones clásicos de GoF, y se incluirán algunos patrones modernos de fuentes como Learn JavaScript Design Patterns de Addy Osmani. Los ejemplos se mantienen simples para facilitar la comprensión y, por lo tanto, no son la implementación más optimizada de sus respectivos patrones de diseño.

Categorías de patrones de diseño

Los patrones de diseño generalmente se clasifican en tres grupos principales.

Patrones de diseño creacional

Como su nombre indica, estos patrones son para manejar mecanismos de creación de objetos. Un patrón de diseño creacional básicamente resuelve un problema controlando el proceso de creación de un objeto.

Discutiremos los siguientes patrones en detalle: Patrón de constructor, Patrón de fábrica, Patrón de prototipo y Patrón de Singleton.

Patrones de diseño estructural

Estos patrones se refieren a la composición de clases y objetos. Ayudan a estructurar o reestructurar una o más partes sin afectar todo el sistema. En otras palabras, ayudan a obtener nuevas funcionalidades sin alterar las existentes.

Discutiremos los siguientes patrones en detalle: Patrón de adaptador, Patrón compuesto, Patrón de decoración, Patrón de fachada, Patrón de peso mosca y Patrón de proxy.

Patrones de diseño conductual

Estos patrones están relacionados con la mejora de la comunicación entre objetos diferentes.

Analizaremos en detalle los siguientes patrones: Patrón de cadena de responsabilidad, Patrón de comando, Patrón de iterador, Patrón de mediador, Patrón de observador, Patrón de estado, Patrón de estrategia y Patrón de plantilla.

Patrón de constructor

Este es un patrón de diseño creacional basado en clases. Los constructores son funciones especiales que se pueden usar para crear instancias de nuevos objetos con métodos y propiedades definidas por esa función.

No es uno de los patrones de diseño clásicos. De hecho, es más una construcción de lenguaje básico que un patrón en la mayoría de los lenguajes orientados a objetos. Pero en JavaScript, los objetos se pueden crear sobre la marcha sin ninguna función de constructor o definición de "clase". Por lo tanto, creo que es importante sentar las bases para que otros patrones vengan con este sencillo.

El patrón de constructor es uno de los patrones más utilizados en JavaScript para crear nuevos objetos de un tipo determinado.

En este ejemplo, definimos una clase Hero con atributos como name y specialAbility y métodos como getDetails. Luego, instanciamos un objeto IronMan invocando el método constructor con la nueva palabra clave pasando los valores de los atributos respectivos como argumentos.

Patrón de fábrica

El patrón de fábrica es otro patrón de creación basado en clases. En esto, proporcionamos una interfaz genérica que delega la responsabilidad de instanciación de objetos a sus subclases.

Este patrón se usa con frecuencia cuando necesitamos administrar o manipular colecciones de objetos que son diferentes pero que tienen muchas características similares.

En este ejemplo, creamos una clase de fábrica llamada BallFactory que tiene un método que toma parámetros y, dependiendo de los parámetros, delega la responsabilidad de instanciación de objetos a la clase respectiva. Si el parámetro de tipo es "fútbol" o "fútbol", la clase de Baloncesto maneja la instanciación de objetos, pero si es "baloncesto", la clase de Baloncesto maneja la instanciación de objetos.

Patrón prototipo

Este patrón es un patrón de diseño de creación basado en objetos. En esto, usamos una especie de "esqueleto" de un objeto existente para crear o instanciar nuevos objetos.

Este patrón es específicamente importante y beneficioso para JavaScript porque utiliza la herencia prototípica en lugar de una herencia clásica orientada a objetos. Por lo tanto, juega con la fortaleza de JavaScript y tiene soporte nativo.

En este ejemplo, tenemos un objeto de automóvil que usamos como prototipo para crear otro objeto myCar con la función Object.create de JavaScript y definir un propietario de propiedad adicional en el nuevo objeto.

Patrón Singleton

Singleton es un patrón de diseño creacional especial en el que solo puede existir una instancia de una clase. Funciona así: si no existe una instancia de la clase singleton, se crea y devuelve una nueva instancia, pero si ya existe una instancia, se devuelve la referencia a la instancia existente.

Un ejemplo perfecto de la vida real sería el de mangosta (la famosa biblioteca ODM Node.js para MongoDB). Utiliza el patrón singleton.

En este ejemplo, tenemos una clase de base de datos que es un singleton. Primero, creamos un objeto mongo usando el nuevo operador para invocar el constructor de la clase de base de datos. Esta vez se instancia un objeto porque ya no existe ninguno. La segunda vez, cuando creamos el objeto mysql, no se crea una instancia de ningún objeto nuevo, sino que se devuelve la referencia al objeto que se instanciaba anteriormente, es decir, el objeto mongo.

Patrón adaptador

Este es un patrón estructural donde la interfaz de una clase se traduce en otra. Este patrón permite que las clases trabajen juntas y que de otro modo no podrían debido a interfaces incompatibles.

Este patrón se usa a menudo para crear envoltorios para nuevas API refactorizadas para que otras API antiguas existentes puedan trabajar con ellas. Esto generalmente se hace cuando las nuevas implementaciones o la refactorización de código (hecho por razones como ganancias de rendimiento) dan como resultado una API pública diferente, mientras que las otras partes del sistema todavía usan la API anterior y necesitan adaptarse para trabajar juntas.

En este ejemplo, tenemos una API antigua, es decir, la clase OldCalculator, y una nueva API, es decir, la clase NewCalculator. La clase OldCalculator proporciona un método de operación para sumas y restas, mientras que NewCalculator proporciona métodos separados para sumas y restas. La clase de adaptador CalcAdapter envuelve el NewCalculator para agregar el método de operación a la API pública mientras usa su propia implementación de suma y resta bajo el capó.

Patrón compuesto

Este es un patrón de diseño estructural que compone objetos en estructuras en forma de árbol para representar jerarquías de partes enteras. En este patrón, cada nodo en la estructura en forma de árbol puede ser un objeto individual o una colección compuesta de objetos. Independientemente, cada nodo se trata de manera uniforme.

Una estructura de menú multinivel

Es un poco complejo visualizar este patrón. La forma más fácil de pensar en esto es con el ejemplo de un menú de varios niveles. Cada nodo puede ser una opción distinta, o puede ser un menú en sí mismo, que tiene múltiples opciones como elemento secundario. Un componente de nodo con hijos es un componente compuesto, mientras que un componente de nodo sin hijos es un componente hoja.

En este ejemplo, creamos una clase base de Componente que implementa las funcionalidades comunes necesarias y abstrae los otros métodos necesarios. La clase base también tiene un método estático que utiliza la recursividad para atravesar una estructura de árbol compuesta hecha con sus subclases. Luego creamos dos subclases que amplían la clase base: Leaf que no tiene hijos y Composite que puede tener hijos, y por lo tanto, tenemos métodos que manejan agregar, buscar y eliminar funcionalidades secundarias. Las dos subclases se utilizan para crear una estructura compuesta: un árbol, en este caso.

Patrón decorador

Este también es un patrón de diseño estructural que se centra en la capacidad de agregar dinámicamente comportamiento o funcionalidades a las clases existentes. Es otra alternativa viable a la subclasificación.

El comportamiento de tipo decorador es muy fácil de implementar en JavaScript porque JavaScript nos permite agregar métodos y propiedades para objetar dinámicamente. El enfoque más simple sería simplemente agregar una propiedad a un objeto, pero no será eficientemente reutilizable.

De hecho, hay una propuesta para agregar decoradores al lenguaje JavaScript. Echa un vistazo a la publicación de Addy Osmani sobre decoradores en JavaScript.

Si desea leer sobre la propuesta en sí, siéntase libre.

En este ejemplo, creamos una clase de libro. Además, creamos dos funciones de decorador que aceptan un objeto de libro y devuelven un objeto de libro "decorado": giftWrap que agrega un nuevo atributo y una nueva función y hardbindBook que agrega un nuevo atributo y edita el valor de un atributo existente.

Patrón de fachada

Este es un patrón de diseño estructural que se usa ampliamente en las bibliotecas de JavaScript. Se utiliza para proporcionar una interfaz unificada y más simple, orientada al público para facilitar su uso que protege de las complejidades de sus subsistemas o subclases consistentes.

El uso de este patrón es muy común en bibliotecas como jQuery.

En este ejemplo, creamos una API pública con la clase ComplaintRegistry. Expone solo un método para ser utilizado por el cliente, es decir, registerComplaint. Maneja internamente la creación de instancias de los objetos requeridos de ProductComplaint o ServiceComplaint en función del argumento de tipo. También maneja todas las demás funcionalidades complejas, como generar una ID única, almacenar la queja en la memoria, etc. Pero, todas estas complejidades se ocultan utilizando el patrón de fachada.

Patrón de peso mosca

Este es un patrón de diseño estructural centrado en el intercambio eficiente de datos a través de objetos de grano fino. Se utiliza con fines de eficiencia y conservación de la memoria.

Este patrón se puede utilizar para cualquier tipo de almacenamiento en caché. De hecho, los navegadores modernos usan una variante de un patrón de peso mosca para evitar cargar las mismas imágenes dos veces.

En este ejemplo, creamos un helado de clase mosca fina de grano fino para compartir datos sobre sabores de helados y una fábrica IcecreamFactory de clase de fábrica para crear esos objetos de peso mosca. Para la conservación de la memoria, los objetos se reciclan si el mismo objeto se instancia dos veces. Este es un ejemplo simple de implementación de peso mosca.

Patrón Proxy

Este es un patrón de diseño estructural que se comporta exactamente como su nombre lo indica. Actúa como un sustituto o marcador de posición para otro objeto para controlar el acceso a él.

Por lo general, se usa en situaciones en las que un objeto objetivo está bajo restricciones y es posible que no pueda manejar todas sus responsabilidades de manera eficiente. Un proxy, en este caso, generalmente proporciona la misma interfaz al cliente y agrega un nivel de indirección para admitir el acceso controlado al objeto de destino para evitar una presión indebida sobre él.

El patrón proxy puede ser muy útil cuando se trabaja con aplicaciones pesadas de solicitud de red para evitar solicitudes de red innecesarias o redundantes.

En este ejemplo, utilizaremos dos nuevas funciones de ES6, Proxy y Reflect. Un objeto Proxy se usa para definir un comportamiento personalizado para operaciones fundamentales de un objeto JavaScript (recuerde, la función y las matrices también son objetos en JavaScript). Es un método constructor que puede usarse para crear un objeto Proxy. Acepta un objeto de destino que debe ser proxy y un objeto controlador que definirá la personalización necesaria. El objeto controlador permite definir algunas funciones de captura como get, set, has, apply, etc. que se utilizan para agregar un comportamiento personalizado asociado a su uso. Reflect, por otro lado, es un objeto incorporado que proporciona métodos similares que son compatibles con el objeto controlador de Proxy como métodos estáticos en sí mismo. No es un constructor; Sus métodos estáticos se utilizan para operaciones JavaScript interceptables.

Ahora, creamos una función que puede considerarse como una solicitud de red. Lo nombramos como networkFetch. Acepta una URL y responde en consecuencia. Queremos implementar un proxy donde solo obtengamos la respuesta de la red si no está disponible en nuestro caché. De lo contrario, solo devolveremos una respuesta del caché.

La variable global de caché almacenará nuestras respuestas almacenadas en caché. Creamos un proxy llamado proxiedNetworkFetch con nuestro networkFetch original como objetivo y utilizamos el método de aplicación en nuestro objeto controlador para representar la invocación de la función. El método de aplicación se pasa al objeto de destino en sí. Este valor como thisArg y los argumentos se le pasan en una estructura de args de tipo matriz.

Verificamos si el argumento url pasado está en el caché. Si existe en el caché, devolvemos la respuesta desde allí, nunca invocando la función de destino original. Si no es así, entonces usamos el método Reflect.apply para invocar la función de destino con thisArg (aunque no es de ninguna importancia en nuestro caso aquí) y los argumentos que pasó.

Patrón de cadena de responsabilidad

Este es un patrón de diseño de comportamiento que proporciona una cadena de objetos sueltos. Cada uno de estos objetos puede elegir actuar o manejar la solicitud del cliente.

Un buen ejemplo del patrón de la cadena de responsabilidad es el evento que burbujea en DOM en el que un evento se propaga a través de una serie de elementos DOM anidados, uno de los cuales puede tener un "escucha de eventos" adjunto para escuchar y actuar en el evento.

En este ejemplo, creamos una clase CumulativeSum, que se puede instanciar con un valor inicial opcional. Tiene un método add que agrega el valor pasado al atributo sum del objeto y devuelve el objeto en sí mismo para permitir el encadenamiento de llamadas al método add.

Este es un patrón común que también se puede ver en jQuery, donde casi cualquier llamada a un método en un objeto jQuery devuelve un objeto jQuery para que las llamadas a los métodos se puedan encadenar.

Patrón de comando

Este es un patrón de diseño de comportamiento que tiene como objetivo encapsular acciones u operaciones como objetos. Este patrón permite un acoplamiento flexible de sistemas y clases al separar los objetos que solicitan una operación o invocan un método de los que ejecutan o procesan la implementación real.

La API de interacción del portapapeles se parece un poco al patrón de comando. Si eres un usuario de Redux, ya has encontrado el patrón de comando. Las acciones que permiten la asombrosa función de depuración de viajes en el tiempo no son más que operaciones encapsuladas que pueden rastrearse para rehacer o deshacer operaciones. Por lo tanto, viajar en el tiempo fue posible.

En este ejemplo, tenemos una clase llamada SpecialMath que tiene múltiples métodos y una clase de Comando que encapsula los comandos que se ejecutarán en su tema, es decir, un objeto de la clase SpecialMath. La clase Command también realiza un seguimiento de todos los comandos ejecutados, que se pueden usar para ampliar su funcionalidad para incluir operaciones de tipo deshacer y rehacer.

Patrón de iterador

Es un patrón de diseño de comportamiento que proporciona una forma de acceder a los elementos de un objeto agregado secuencialmente sin exponer su representación subyacente.

Los iteradores tienen un tipo especial de comportamiento en el que pasamos a través de un conjunto ordenado de valores uno por uno llamando a next () hasta llegar al final. La introducción de Iterator and Generators en ES6 hizo que la implementación del patrón iterador fuera extremadamente sencilla.

Tenemos dos ejemplos a continuación. Primero, un IteratorClass usa especificaciones de iterador, mientras que el otro iteratorUsingGenerator usa funciones de generador.

El Symbol.iterator (Símbolo, un nuevo tipo de tipo de datos primitivo) se utiliza para especificar el iterador predeterminado para un objeto. Debe definirse para que una colección pueda usar el for ... de la construcción en bucle. En el primer ejemplo, definimos el constructor para almacenar una colección de datos y luego definimos Symbol.iterator, que devuelve un objeto con el siguiente método para la iteración.

Para el segundo caso, definimos una función generadora que le pasa una matriz de datos y devuelve sus elementos iterativamente usando next y yield. Una función generadora es un tipo especial de función que funciona como una fábrica para iteradores y puede mantener explícitamente su propio estado interno y generar valores de forma iterativa. Puede pausar y reanudar su propio ciclo de ejecución.

Patrón de mediador

Es un patrón de diseño de comportamiento que encapsula cómo un conjunto de objetos interactúa entre sí. Proporciona la autoridad central sobre un grupo de objetos al promover un acoplamiento suelto, evitando que los objetos se refieran entre sí explícitamente.

En este ejemplo, tenemos TrafficTower como mediador que controla la forma en que los objetos del avión interactúan entre sí. Todos los objetos Airplane se registran con un objeto TrafficTower, y es el objeto de clase mediador el que maneja cómo un objeto Airplane recibe datos de coordenadas de todos los demás objetos Airplane.

Patrón de observador

Es un patrón de diseño de comportamiento crucial que define las dependencias de uno a muchos entre los objetos, de modo que cuando un objeto (editor) cambia su estado, todos los demás objetos dependientes (suscriptores) son notificados y actualizados automáticamente. Esto también se llama PubSub (editor / suscriptores) o patrón de despachador de eventos / oyentes. El editor a veces se llama el tema, y ​​los suscriptores a veces se llaman observadores.

Lo más probable es que ya esté familiarizado con este patrón si ha utilizado addEventListener o jQuery's .on para escribir código de manejo uniforme. También tiene sus influencias en la programación reactiva (piense en RxJS).

En el ejemplo, creamos una clase Asunto simple que tiene métodos para agregar y eliminar objetos de la clase Observer de la colección de suscriptores. Además, un método de fuego para propagar cualquier cambio en el objeto de la clase Asunto a los Observadores suscritos. La clase Observer, por otro lado, tiene su estado interno y un método para actualizar su estado interno basado en el cambio propagado desde el Asunto al que se ha suscrito.

Patrón de estado

Es un patrón de diseño de comportamiento que permite que un objeto altere su comportamiento en función de los cambios en su estado interno. El objeto devuelto por una clase de patrón de estado parece cambiar su clase. Proporciona lógica específica de estado a un conjunto limitado de objetos en el que cada tipo de objeto representa un estado particular.

Tomaremos un ejemplo simple de un semáforo para comprender este patrón. La clase TrafficLight cambia el objeto que devuelve en función de su estado interno, que es un objeto de la clase Roja, Amarilla o Verde.

Patrón de estrategia

Es un patrón de diseño de comportamiento que permite la encapsulación de algoritmos alternativos para una tarea en particular. Define una familia de algoritmos y los encapsula de tal manera que son intercambiables en tiempo de ejecución sin interferencia o conocimiento del cliente.

En el siguiente ejemplo, creamos una clase Commute para encapsular todas las estrategias posibles para ir al trabajo. Luego, definimos tres estrategias: Bus, PersonalCar y Taxi. Usando este patrón, podemos intercambiar la implementación para usar para el método de viaje del objeto Commute en tiempo de ejecución.

Patrón de plantilla

Este es un patrón de diseño de comportamiento basado en la definición del esqueleto del algoritmo o la implementación de una operación, pero diferiendo algunos pasos a las subclases. Permite que las subclases redefinan ciertos pasos de un algoritmo sin cambiar la estructura externa del algoritmo.

En este ejemplo, tenemos una clase de plantilla Empleado que implementa el método de trabajo parcialmente. Corresponde a las subclases implementar el método de responsabilidades para que funcione como un todo. Luego creamos dos subclases Developer y Tester que amplían la clase de plantilla e implementan el método requerido para llenar el vacío de implementación.

Conclusión

Los patrones de diseño son cruciales para la ingeniería de software y pueden ser muy útiles para resolver problemas comunes. Pero este es un tema muy vasto, y simplemente no es posible incluir todo sobre ellos en una pieza corta. Por lo tanto, tomé la decisión de hablar breve y concisamente sobre los que creo que pueden ser realmente útiles para escribir JavaScript moderno. Para profundizar, le sugiero que eche un vistazo a estos libros:

  1. Patrones de diseño: elementos de software orientado a objetos reutilizables por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides (Gang of Four)
  2. Aprender patrones de diseño de JavaScript por Addy Osmani
  3. Patrones JavaScript de Stoyan Stefanov