La guía completa de patrones de diseño de JavaScript
Publicado: 2022-03-11Como buen desarrollador de JavaScript, se esfuerza por escribir código limpio, saludable y fácil de mantener. Resuelve desafíos interesantes que, si bien son únicos, no necesariamente requieren soluciones únicas. Es probable que se haya encontrado escribiendo un código que se parece a la solución de un problema completamente diferente que ha manejado antes. Puede que no lo sepa, pero ha utilizado un patrón de diseño de JavaScript. Los patrones de diseño son soluciones reutilizables para problemas comunes en el diseño de software.
Durante la vida útil de cualquier idioma, un gran número de desarrolladores de la comunidad de ese idioma crean y prueban muchas de estas soluciones reutilizables. Es debido a esta experiencia combinada de muchos desarrolladores que tales soluciones son tan útiles porque nos ayudan a escribir código de una manera optimizada y al mismo tiempo resuelven el problema en cuestión.
Los principales beneficios que obtenemos de los patrones de diseño son los siguientes:
- Son soluciones probadas: debido a que muchos desarrolladores suelen utilizar patrones de diseño, puede estar seguro de que funcionan. Y no solo eso, puede estar seguro de que se revisaron varias veces y probablemente se implementaron optimizaciones.
- Son fácilmente reutilizables: los patrones de diseño documentan una solución reutilizable que se puede modificar para resolver múltiples problemas particulares, ya que no están vinculados a un problema específico.
- Son expresivos: los patrones de diseño pueden explicar una gran solución con bastante elegancia.
- Facilitan la comunicación: cuando los desarrolladores están familiarizados con los patrones de diseño, pueden comunicarse más fácilmente entre sí sobre posibles soluciones a un problema determinado.
- Evitan la necesidad de refactorizar el código: si una aplicación se escribe con patrones de diseño en mente, a menudo no necesitará refactorizar el código más adelante porque aplicar el patrón de diseño correcto a un problema dado ya es una solución óptima. solución.
- Reducen el tamaño del código base: debido a que los patrones de diseño suelen ser soluciones elegantes y óptimas, normalmente requieren menos código que otras soluciones.
Sé que está listo para saltar en este punto, pero antes de que aprenda todo acerca de los patrones de diseño, repasemos algunos conceptos básicos de JavaScript.
Una breve historia de JavaScript
JavaScript es uno de los lenguajes de programación más populares para el desarrollo web en la actualidad. Inicialmente se hizo como una especie de "pegamento" para varios elementos HTML mostrados, conocido como lenguaje de secuencias de comandos del lado del cliente, para uno de los navegadores web iniciales. Llamado Netscape Navigator, solo podía mostrar HTML estático en ese momento. Como puede suponer, la idea de un lenguaje de secuencias de comandos de este tipo condujo a guerras de navegadores entre los grandes actores de la industria del desarrollo de navegadores en ese entonces, como Netscape Communications (hoy Mozilla), Microsoft y otros.
Cada uno de los grandes jugadores quería impulsar su propia implementación de este lenguaje de secuencias de comandos, por lo que Netscape hizo JavaScript (en realidad, Brendan Eich lo hizo), Microsoft hizo JScript, y así sucesivamente. Como puede imaginar, las diferencias entre estas implementaciones fueron grandes, por lo que el desarrollo para los navegadores web se realizó por navegador, con las pegatinas de mejor visualización que venían con una página web. Pronto quedó claro que necesitábamos una solución estándar para todos los navegadores que unificaría el proceso de desarrollo y simplificaría la creación de páginas web. Lo que se les ocurrió se llama ECMAScript.
ECMAScript es una especificación de lenguaje de secuencias de comandos estandarizada que todos los navegadores modernos intentan admitir, y existen múltiples implementaciones (se podría decir dialectos) de ECMAScript. El más popular es el tema de este artículo, JavaScript. Desde su lanzamiento inicial, ECMAScript ha estandarizado muchas cosas importantes, y para aquellos más interesados en los detalles, hay una lista detallada de elementos estandarizados para cada versión de ECMAScript disponible en Wikipedia. La compatibilidad del navegador con las versiones 6 (ES6) y posteriores de ECMAScript aún está incompleta y debe transferirse a ES5 para que sea totalmente compatible.
¿Qué es JavaScript?
Para comprender completamente el contenido de este artículo, hagamos una introducción a algunas características muy importantes del lenguaje que debemos tener en cuenta antes de sumergirnos en los patrones de diseño de JavaScript. Si alguien te preguntara "¿Qué es JavaScript?" usted podría responder en algún lugar en las líneas de:
JavaScript es un lenguaje de programación ligero, interpretado y orientado a objetos con funciones de primera clase, más comúnmente conocido como lenguaje de secuencias de comandos para páginas web.
La definición antes mencionada significa decir que el código JavaScript tiene una huella de memoria baja, es fácil de implementar y fácil de aprender, con una sintaxis similar a los lenguajes populares como C ++ y Java. Es un lenguaje de secuencias de comandos, lo que significa que su código se interpreta en lugar de compilarse. Tiene soporte para estilos de programación funcionales, orientados a objetos y procedimentales, lo que lo hace muy flexible para los desarrolladores.
Hasta ahora, hemos echado un vistazo a todas las características que suenan como muchos otros lenguajes, así que echemos un vistazo a lo que es específico de JavaScript con respecto a otros lenguajes. Voy a enumerar algunas características y dar lo mejor de mí para explicar por qué merecen una atención especial.
JavaScript admite funciones de primera clase
Esta característica solía ser difícil de comprender para mí cuando recién comenzaba con JavaScript, ya que provenía de un entorno de C/C++. JavaScript trata las funciones como ciudadanos de primera clase, lo que significa que puede pasar funciones como parámetros a otras funciones como lo haría con cualquier otra variable.
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })
JavaScript está basado en prototipos
Como es el caso con muchos otros lenguajes orientados a objetos, JavaScript admite objetos, y uno de los primeros términos que viene a la mente cuando se piensa en objetos es clases y herencia. Aquí es donde se complica un poco, ya que el lenguaje no admite clases en su forma de lenguaje sencillo, sino que usa algo llamado herencia basada en prototipos o basada en instancias.
Recién ahora, en ES6, se introdujo el término formal class , lo que significa que los navegadores aún no lo admiten (si recuerda, al momento de escribir, la última versión de ECMAScript totalmente compatible es 5.1). Sin embargo, es importante tener en cuenta que, aunque el término "clase" se introduce en JavaScript, todavía utiliza la herencia basada en prototipos bajo el capó.
La programación basada en prototipos es un estilo de programación orientada a objetos en el que la reutilización del comportamiento (conocida como herencia) se realiza a través de un proceso de reutilización de objetos existentes a través de delegaciones que sirven como prototipos. Profundizaremos en esto una vez que lleguemos a la sección de patrones de diseño del artículo, ya que esta característica se usa en muchos patrones de diseño de JavaScript.
Bucles de eventos de JavaScript
Si tiene experiencia trabajando con JavaScript, seguramente esté familiarizado con el término función de devolución de llamada. Para aquellos que no estén familiarizados con el término, una función de devolución de llamada es una función enviada como un parámetro (recuerde, JavaScript trata las funciones como ciudadanos de primera clase) a otra función y se ejecuta después de que se activa un evento. Esto generalmente se usa para suscribirse a eventos como un clic del mouse o presionar un botón del teclado.
Cada vez que se activa un evento, que tiene un oyente adjunto (de lo contrario, el evento se pierde), se envía un mensaje a una cola de mensajes que se procesan sincrónicamente, de forma FIFO (primero en entrar, primero en salir). ). Esto se llama el bucle de eventos .
Cada uno de los mensajes en la cola tiene una función asociada. Una vez que se elimina un mensaje de la cola, el tiempo de ejecución ejecuta la función por completo antes de procesar cualquier otro mensaje. Es decir, si una función contiene otras llamadas de función, todas se realizan antes de procesar un nuevo mensaje de la cola. Esto se llama ejecución hasta el final.
while (queue.waitForMessage()) { queue.processNextMessage(); }
queue.waitForMessage()
espera sincrónicamente nuevos mensajes. Cada uno de los mensajes que se procesan tiene su propia pila y se procesa hasta que la pila está vacía. Una vez que finaliza, se procesa un nuevo mensaje de la cola, si lo hay.
Es posible que también haya escuchado que JavaScript no bloquea, lo que significa que cuando se realiza una operación asincrónica, el programa puede procesar otras cosas, como recibir la entrada del usuario, mientras espera que se complete la operación asincrónica, sin bloquear el principal hilo de ejecución. Esta es una propiedad muy útil de JavaScript y se podría escribir un artículo completo solo sobre este tema; sin embargo, está fuera del alcance de este artículo.
¿Qué son los patrones de diseño?
Como dije antes, los patrones de diseño son soluciones reutilizables para problemas comunes en el diseño de software. Echemos un vistazo a algunas de las categorías de patrones de diseño.
Proto-patrones
¿Cómo se crea un patrón? Supongamos que reconoció un problema común y tiene su propia solución única para este problema, que no está reconocida ni documentada a nivel mundial. Utiliza esta solución cada vez que encuentra este problema y cree que es reutilizable y que la comunidad de desarrolladores podría beneficiarse de ella.
¿Se convierte inmediatamente en un patrón? Por suerte, no. A menudo, uno puede tener buenas prácticas de escritura de código y simplemente confundir algo que parece un patrón cuando, de hecho, no es un patrón.
¿Cómo puede saber cuándo lo que cree que reconoce es en realidad un patrón de diseño?
Obteniendo las opiniones de otros desarrolladores al respecto, conociendo el proceso de creación de un patrón en sí mismo y familiarizándose bien con los patrones existentes. Hay una fase por la que tiene que pasar un patrón antes de convertirse en un patrón completo, y esto se llama proto-patrón.
Un proto-patrón es un patrón a ser si pasa un cierto período de prueba por parte de varios desarrolladores y escenarios donde el patrón demuestra ser útil y da resultados correctos. Hay una gran cantidad de trabajo y documentación, la mayoría de los cuales está fuera del alcance de este artículo, por hacer para hacer un patrón completo reconocido por la comunidad.
Anti-patrones
Así como un patrón de diseño representa una buena práctica, un antipatrón representa una mala práctica.
Un ejemplo de antipatrón sería modificar el prototipo de la clase Object
. Casi todos los objetos en JavaScript heredan de Object
(recuerde que JavaScript usa herencia basada en prototipos), así que imagine un escenario en el que modificó este prototipo. Los cambios en el prototipo de Object
se verían en todos los objetos que heredan de este prototipo , que serían la mayoría de los objetos de JavaScript . Esto es un desastre esperando a suceder.
Otro ejemplo, similar al mencionado anteriormente, es la modificación de objetos que no son de su propiedad. Un ejemplo de esto sería anular una función de un objeto utilizado en muchos escenarios a lo largo de la aplicación. Si está trabajando con un equipo grande, imagine la confusión que esto causaría; rápidamente se encontraría con colisiones de nombres, implementaciones incompatibles y pesadillas de mantenimiento.
Así como es útil conocer todas las buenas prácticas y soluciones, también es muy importante conocer las malas. De esta manera, puede reconocerlos y evitar cometer el error al principio.
Categorización de patrones de diseño
Los patrones de diseño se pueden categorizar de varias maneras, pero la más popular es la siguiente:
- Patrones de diseño creacional
- Patrones de diseño estructural
- Patrones de diseño de comportamiento
- Patrones de diseño de concurrencia
- patrones de diseño arquitectónico
Patrones de diseño creacional
Estos patrones se ocupan de los mecanismos de creación de objetos que optimizan la creación de objetos en comparación con un enfoque básico. La forma básica de creación de objetos podría dar lugar a problemas de diseño o a una mayor complejidad del diseño. Los patrones de diseño creacional resuelven este problema al controlar de alguna manera la creación de objetos. Algunos de los patrones de diseño populares en esta categoría son:
- método de fábrica
- Fábrica abstracta
- Constructor
- Prototipo
- único
Patrones de diseño estructural
Estos patrones se ocupan de las relaciones de objetos. Aseguran que si una parte de un sistema cambia, el sistema completo no necesita cambiar junto con ella. Los patrones más populares en esta categoría son:
- Adaptador
- Puente
- Compuesto
- Decorador
- Fachada
- peso mosca
- Apoderado
Patrones de diseño de comportamiento
Estos tipos de patrones reconocen, implementan y mejoran la comunicación entre objetos dispares en un sistema. Ayudan a garantizar que partes dispares de un sistema tengan información sincronizada. Ejemplos populares de estos patrones son:
- Cadena de responsabilidad
- Mando
- iterador
- Mediador
- Recuerdo
- Observador
- Estado
- Estrategia
- Visitante
Patrones de diseño de concurrencia
Estos tipos de patrones de diseño tratan con paradigmas de programación de subprocesos múltiples. Algunos de los más populares son:
- Objeto activo
- Reacción nuclear
- programador
Patrones de diseño arquitectónico
Patrones de diseño que se utilizan con fines arquitectónicos. Algunos de los más famosos son:
- MVC (Modelo-Vista-Controlador)
- MVP (Modelo-Vista-Presentador)
- MVVM (Modelo-Vista-Modelo de vista)
En la siguiente sección, vamos a echar un vistazo más de cerca a algunos de los patrones de diseño antes mencionados con ejemplos proporcionados para una mejor comprensión.
Ejemplos de patrones de diseño
Cada uno de los patrones de diseño representa un tipo específico de solución para un tipo específico de problema. No existe un conjunto universal de patrones que sea siempre el mejor ajuste. Necesitamos saber cuándo un patrón en particular resultará útil y si proporcionará un valor real. Una vez que estamos familiarizados con los patrones y escenarios para los que son más adecuados, podemos determinar fácilmente si un patrón específico es adecuado o no para un problema determinado.
Recuerde, aplicar el patrón incorrecto a un problema determinado podría generar efectos no deseados, como una complejidad de código innecesaria, una sobrecarga innecesaria en el rendimiento o incluso la generación de un nuevo antipatrón.
Todas estas son cosas importantes a tener en cuenta al pensar en aplicar un patrón de diseño a nuestro código. Vamos a echar un vistazo a algunos de los patrones de diseño que personalmente encontré útiles y creo que todo desarrollador senior de JavaScript debería estar familiarizado.
Patrón de constructor
Al pensar en lenguajes clásicos orientados a objetos, un constructor es una función especial en una clase que inicializa un objeto con algún conjunto de valores predeterminados y/o enviados.
Las formas comunes de crear objetos en JavaScript son las siguientes tres formas:
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();
Después de crear un objeto, hay cuatro formas (desde ES3) de agregar propiedades a estos objetos. Ellos son los siguientes:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });
La forma más popular de crear objetos son los corchetes y, para agregar propiedades, la notación de puntos o corchetes. Cualquier persona con alguna experiencia con JavaScript los ha usado.
Anteriormente mencionamos que JavaScript no admite clases nativas, pero admite constructores mediante el uso de una palabra clave "nueva" antepuesta a una llamada de función. De esta manera, podemos usar la función como constructor e inicializar sus propiedades de la misma manera que lo haríamos con un constructor de lenguaje clásico.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Sin embargo, todavía hay margen de mejora aquí. Si recuerda, mencioné anteriormente que JavaScript usa herencia basada en prototipos. El problema con el enfoque anterior es que el método writesCode
se redefine para cada una de las instancias del constructor Person
. Podemos evitar esto configurando el método en el prototipo de función:
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Ahora, ambas instancias del constructor Person
pueden acceder a una instancia compartida del método writesCode()
.

Patrón de módulo
En cuanto a las peculiaridades, JavaScript nunca deja de sorprender. Otra cosa peculiar de JavaScript (al menos en lo que respecta a los lenguajes orientados a objetos) es que JavaScript no admite modificadores de acceso. En un lenguaje OOP clásico, un usuario define una clase y determina los derechos de acceso para sus miembros. Dado que JavaScript en su forma simple no admite clases ni modificadores de acceso, los desarrolladores de JavaScript descubrieron una forma de imitar este comportamiento cuando sea necesario.
Antes de entrar en los detalles del patrón del módulo, hablemos del concepto de cierre. Un cierre es una función con acceso al ámbito principal, incluso después de que la función principal se haya cerrado. Nos ayudan a imitar el comportamiento de los modificadores de acceso a través del alcance. Mostremos esto a través de un ejemplo:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());
Como puede ver, al usar IIFE, hemos vinculado la variable de contador a una función que se invocó y cerró, pero que aún puede acceder a la función secundaria que la incrementa. Dado que no podemos acceder a la variable de contador desde fuera de la expresión de la función, la hicimos privada a través de la manipulación del alcance.
Usando los cierres, podemos crear objetos con partes públicas y privadas. Estos se llaman módulos y son muy útiles cuando queremos ocultar ciertas partes de un objeto y solo exponer una interfaz al usuario del módulo. Mostremos esto en un ejemplo:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());
Lo más útil que presenta este patrón es la clara separación de las partes públicas y privadas de un objeto, que es un concepto muy similar a los desarrolladores que provienen de un entorno clásico orientado a objetos.
Sin embargo, no todo es tan perfecto. Cuando desee cambiar la visibilidad de un miembro, debe modificar el código siempre que haya utilizado este miembro debido a la naturaleza diferente de acceder a las partes públicas y privadas. Además, los métodos agregados al objeto después de su creación no pueden acceder a los miembros privados del objeto.
Patrón de módulo revelador
Este patrón es una mejora realizada al patrón del módulo como se ilustra arriba. La principal diferencia es que escribimos toda la lógica del objeto en el ámbito privado del módulo y luego simplemente exponemos las partes que queremos que sean públicas devolviendo un objeto anónimo. También podemos cambiar el nombre de los miembros privados al asignar miembros privados a sus miembros públicos correspondientes.
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());
El patrón de módulo revelador es una de al menos tres formas en las que podemos implementar un patrón de módulo. Las diferencias entre el patrón de módulo revelador y las otras variantes del patrón de módulo radican principalmente en cómo se hace referencia a los miembros públicos. Como resultado, el patrón del módulo revelador es mucho más fácil de usar y modificar; sin embargo, puede resultar frágil en ciertos escenarios, como usar objetos RMP como prototipos en una cadena de herencia. Las situaciones problemáticas son las siguientes:
- Si tenemos una función privada que hace referencia a una función pública, no podemos anular la función pública, ya que la función privada continuará haciendo referencia a la implementación privada de la función, lo que introducirá un error en nuestro sistema.
- Si tenemos un miembro público que apunta a una variable privada e intentamos anular el miembro público desde fuera del módulo, las otras funciones seguirán haciendo referencia al valor privado de la variable, introduciendo un error en nuestro sistema.
Patrón único
El patrón singleton se usa en escenarios cuando necesitamos exactamente una instancia de una clase. Por ejemplo, necesitamos tener un objeto que contenga alguna configuración para algo. En estos casos, no es necesario crear un nuevo objeto siempre que el objeto de configuración se requiera en algún lugar del sistema.
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);
Como puede ver en el ejemplo, el número aleatorio generado es siempre el mismo, así como los valores de configuración enviados.
Es importante tener en cuenta que el punto de acceso para recuperar el valor singleton debe ser único y muy conocido. Una desventaja de usar este patrón es que es bastante difícil de probar.
Patrón de observador
El patrón de observador es una herramienta muy útil cuando tenemos un escenario en el que necesitamos mejorar la comunicación entre partes dispares de nuestro sistema de forma optimizada. Promueve el acoplamiento flojo entre los objetos.
Hay varias versiones de este patrón, pero en su forma más básica, tenemos dos partes principales del patrón. El primero es un sujeto y el segundo son los observadores.
Un sujeto maneja todas las operaciones relacionadas con un determinado tema que los observadores suscriben. Estas operaciones suscriben a un observador a un tema determinado, dan de baja a un observador de un tema determinado y notifican a los observadores sobre un tema determinado cuando se publica un evento.
Sin embargo, hay una variación de este patrón llamado patrón de publicador/suscriptor, que voy a usar como ejemplo en esta sección. La principal diferencia entre un patrón de observador clásico y el patrón de editor/suscriptor es que el editor/suscriptor promueve un acoplamiento aún más flexible que el patrón de observador.
En el patrón observador, el sujeto mantiene las referencias a los observadores suscritos y llama a los métodos directamente desde los propios objetos, mientras que en el patrón publicador/suscriptor tenemos canales que sirven como puente de comunicación entre un suscriptor y un publicador. El editor activa un evento y simplemente ejecuta la función de devolución de llamada enviada para ese evento.
Voy a mostrar un breve ejemplo del patrón de editor/suscriptor, pero para aquellos interesados, un ejemplo de patrón de observador clásico se puede encontrar fácilmente en línea.
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});
Este patrón de diseño es útil en situaciones en las que necesitamos realizar múltiples operaciones en un solo evento que se activa. Imagine que tiene un escenario en el que necesitamos realizar varias llamadas AJAX a un servicio de back-end y luego realizar otras llamadas AJAX según el resultado. Tendría que anidar las llamadas AJAX una dentro de la otra, posiblemente entrando en una situación conocida como infierno de devolución de llamada. Usar el patrón editor/suscriptor es una solución mucho más elegante.
Una desventaja de usar este patrón es la dificultad de probar varias partes de nuestro sistema. No existe una forma elegante de saber si las partes suscriptoras del sistema se están comportando como se esperaba.
Patrón mediador
Cubriremos brevemente un patrón que también es muy útil cuando se habla de sistemas desacoplados. Cuando tenemos un escenario en el que varias partes de un sistema necesitan comunicarse y coordinarse, tal vez una buena solución sería introducir un mediador.
Un mediador es un objeto que se utiliza como punto central para la comunicación entre partes dispares de un sistema y maneja el flujo de trabajo entre ellas. Ahora, es importante enfatizar que maneja el flujo de trabajo. ¿Porque es esto importante?
Porque hay una gran similitud con el patrón editor/suscriptor. Podrías preguntarte, OK, estos dos patrones ayudan a implementar una mejor comunicación entre los objetos... ¿Cuál es la diferencia?
La diferencia es que un mediador maneja el flujo de trabajo, mientras que el editor/suscriptor usa algo llamado tipo de comunicación "dispara y olvida". El editor/suscriptor es simplemente un agregador de eventos, lo que significa que simplemente se encarga de activar los eventos y permitir que los suscriptores correctos sepan qué eventos se activaron. Al agregador de eventos no le importa lo que sucede una vez que se activa un evento, lo que no ocurre con un mediador.
Un buen ejemplo de un mediador es un tipo de interfaz de asistente. Supongamos que tiene un gran proceso de registro para un sistema en el que ha trabajado. A menudo, cuando se requiere mucha información de un usuario, es una buena práctica dividirla en varios pasos.
De esta forma, el código será mucho más limpio (más fácil de mantener) y el usuario no se verá abrumado por la cantidad de información que se solicita solo para finalizar el registro. Un mediador es un objeto que manejaría los pasos de registro, teniendo en cuenta diferentes flujos de trabajo posibles que podrían ocurrir debido al hecho de que cada usuario podría tener un proceso de registro único.
El beneficio obvio de este patrón de diseño es una mejor comunicación entre las diferentes partes de un sistema, que ahora se comunican a través del mediador y la base de código más limpia.
Una desventaja sería que ahora hemos introducido un único punto de falla en nuestro sistema, lo que significa que si nuestro mediador falla, todo el sistema podría dejar de funcionar.
Patrón de prototipo
Como ya hemos mencionado a lo largo del artículo, JavaScript no admite clases en su forma nativa. La herencia entre objetos se implementa mediante programación basada en prototipos.
Nos permite crear objetos que pueden servir como prototipo para otros objetos que se están creando. El objeto prototipo se utiliza como modelo para cada objeto que crea el constructor.
Como ya hemos hablado de esto en las secciones anteriores, mostremos un ejemplo simple de cómo se podría usar este patrón.
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();
Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));
Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")
It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
Próximos pasos
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.