Patrones de diseño de JavaScript

El autor del material, cuya traducción publicamos, dice que al comenzar un proyecto, no comienzan a escribir código de inmediato. En primer lugar, determinan el objetivo y los límites del proyecto, luego identifican las oportunidades que debería tener. Después de eso, o bien escriben inmediatamente el código o, si es un proyecto bastante complicado, seleccionan patrones de diseño adecuados que forman su base. Este material trata sobre patrones de diseño de JavaScript. Está diseñado principalmente para desarrolladores principiantes.



¿Qué es un patrón de diseño?


En el campo del desarrollo de software, un patrón de diseño es un diseño arquitectónico repetible que es una solución a un problema de diseño dentro de un contexto que a menudo surge. Los patrones de diseño son un resumen de la experiencia de los desarrolladores profesionales de software. Un patrón de diseño puede considerarse como un tipo de patrón según el cual se escriben los programas.

¿Por qué se necesitan patrones de diseño?


Muchos programadores piensan que los patrones de diseño son una pérdida de tiempo o simplemente no saben cómo aplicarlos correctamente. Sin embargo, el uso de un patrón adecuado puede ayudar a escribir un código mejor y más comprensible, que, debido a su comprensión, será más fácil de mantener.

Lo más importante aquí, tal vez, es que el uso de patrones les da a los desarrolladores de software algo así como un diccionario de términos conocidos que son muy útiles, por ejemplo, al analizar el código de otra persona. Los patrones revelan el propósito de ciertos fragmentos del programa para aquellos que están tratando de lidiar con el dispositivo de un proyecto.

Por ejemplo, si usa el patrón "Decorador", informará de inmediato al nuevo programador que vino al proyecto sobre qué tareas resuelve un código en particular y por qué es necesario. Gracias a esto, dicho programador podrá dedicar más tiempo a las tareas prácticas que resuelve el programa, en lugar de tratar de comprender su estructura interna.

Ahora que hemos descubierto qué son los patrones de diseño y para qué sirven, pasaremos a los patrones mismos y describiremos su implementación usando JavaScript.

Patrón "Módulo"


Un módulo es una pieza de código independiente que se puede cambiar sin afectar a otro código de proyecto. Los módulos, además, permiten evitar un fenómeno como la contaminación de las áreas de visibilidad debido al hecho de que crean áreas de visibilidad separadas para las variables declaradas en ellos. Los módulos escritos para un proyecto pueden reutilizarse en otros proyectos en caso de que sus mecanismos sean universales y no estén vinculados a los detalles de un proyecto en particular.

Los módulos son una parte integral de cualquier aplicación moderna de JavaScript. Ayudan a mantener la limpieza del código, ayudan a separar el código en fragmentos significativos y ayudan a organizarlo. JavaScript tiene muchas formas de crear módulos, uno de los cuales es el patrón "Módulo".

A diferencia de otros lenguajes de programación, JavaScript no tiene modificadores de acceso. Es decir, las variables no pueden declararse como privadas o públicas. Como resultado, el patrón "Módulo" también se utiliza para emular el concepto de encapsulación.

Este patrón utiliza IIFE (expresión funcional invocada inmediatamente), cierres y ámbitos de función para imitar este concepto. Por ejemplo:

const myModule = (function() {   const privateVariable = 'Hello World';   function privateMethod() {    console.log(privateVariable);  }  return {    publicMethod: function() {      privateMethod();    }  } })(); myModule.publicMethod(); 

Como tenemos IIFE, el código se ejecuta inmediatamente y el objeto devuelto por la expresión se asigna a la constante myModule . Debido al hecho de que hay un cierre, el objeto devuelto tiene acceso a funciones y variables declaradas dentro de IIFE, incluso después de que IIFE complete su trabajo.

Como resultado, las variables y funciones declaradas dentro de IIFE están ocultas de los mecanismos que están dentro del alcance de la visibilidad externa a ellas. Resultan ser entidades privadas de la constante myModule .

Después de ejecutar este código, myModule se verá así:

 const myModule = { publicMethod: function() {   privateMethod(); }}; 

Es decir, refiriéndose a esta constante, puede llamar al método público del objeto publicMethod() , que, a su vez, llamará al método privado privateMethod() . Por ejemplo:

 //  'Hello World' module.publicMethod(); 

Patrón de módulo abierto


El patrón Revealing Module es una versión ligeramente mejorada del patrón Module que propuso Christian Heilmann. El problema con el patrón "Módulo" es que tenemos que crear funciones públicas solo para acceder a funciones y variables privadas.

En este patrón, asignamos funciones privadas a las propiedades del objeto devuelto que queremos hacer público. Es por eso que este patrón se llama el "Módulo abierto". Considere un ejemplo:

 const myRevealingModule = (function() { let privateVar = 'Peter'; const publicVar  = 'Hello World'; function privateFunction() {   console.log('Name: '+ privateVar); } function publicSetName(name) {   privateVar = name; } function publicGetName() {   privateFunction(); } /**    ,     */ return {   setName: publicSetName,   greeting: publicVar,   getName: publicGetName }; })(); myRevealingModule.setName('Mark'); //  Name: Mark myRevealingModule.getName(); 

La aplicación de este patrón facilita la comprensión de qué funciones y variables del módulo están disponibles públicamente, lo que ayuda a mejorar la legibilidad del código.

Después de ejecutar IIFE, myRevealingModule ve así:

 const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName }; 

Podemos, por ejemplo, llamar al myRevealingModule.setName('Mark') , que es una referencia a la función interna publicSetName . El método myRevealingModule.getName() se refiere a la función interna publicGetName . Por ejemplo:

 myRevealingModule.setName('Mark'); //  Name: Mark myRevealingModule.getName(); 

Considere las ventajas del patrón "Módulo abierto" sobre el patrón "Módulo":

  • El "módulo abierto" le permite hacer públicas las entidades ocultas del módulo (y ocultarlas nuevamente si es necesario), modificando, para cada una de ellas, solo una línea en el objeto devuelto después de IIFE.
  • El objeto devuelto no contiene una definición de función. Todo a la derecha de sus nombres de propiedad se define en IIFE. Esto ayuda a mantener el código limpio y fácil de leer.

Módulos en ES6


Antes de que se lanzara el estándar ES6, JavaScript no tenía una herramienta estándar para trabajar con módulos, como resultado, los desarrolladores tenían que usar bibliotecas de terceros o el patrón "Módulo" para implementar los mecanismos apropiados. Pero con la llegada de ES6, apareció un sistema de módulos estándar en JavaScript.

Los módulos ES6 se almacenan en archivos. Un archivo puede contener solo un módulo. Todo lo que hay dentro del módulo es privado por defecto. Las funciones, variables y clases se pueden hacer públicas utilizando la palabra clave export . El código dentro del módulo siempre se ejecuta en modo estricto.

▍ Módulo de exportación


Hay dos formas de exportar una función o variable declarada en un módulo:

  • La exportación se realiza agregando la palabra clave export antes de declarar una función o variable. Por ejemplo:

     // utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } //  -   function privateLog() { console.log('Private Function'); } 
  • La exportación se realiza agregando la palabra clave de export al final del código que enumera los nombres de las funciones y variables que se exportarán. Por ejemplo:

     // utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } //    function privateLog() { console.log('Private Function'); } export {multiply, divide}; 

▍Módulo de importación


Así como hay dos formas de exportar, hay dos formas de importar módulos. Esto se hace usando la palabra clave import :

  • Importar múltiples elementos seleccionados. Por ejemplo:

     // main.js //     import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7)); 
  • Importa todo lo que exporta el módulo. Por ejemplo:

     // main.js //  ,    import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7)); 

▍ Alias ​​para entidades exportadas e importadas


Si los nombres de las funciones o variables exportadas al código pueden causar una colisión, se pueden cambiar durante la exportación o la importación.

Para cambiar el nombre de las entidades durante la exportación, puede hacer esto:

 // utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply}; 

Para cambiar el nombre de las entidades durante la importación, se utiliza la siguiente construcción:

 // main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7)); 

Patrón Singleton


El patrón "Singleton" o "Singleton" es un objeto que solo puede existir en una sola copia. Como parte de la aplicación de este patrón, se crea una nueva instancia de una clase si aún no se ha creado. Si la instancia de clase ya existe, al intentar acceder al constructor, se devuelve una referencia al objeto correspondiente. Las llamadas posteriores al constructor siempre devolverán el mismo objeto.

De hecho, lo que llamamos el patrón "Singleton" siempre ha existido en JavaScript, pero no lo llaman "Singleton", sino "objeto literal". Considere un ejemplo:

 const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() {   console.log('Hello!'); } }; 

Como cada objeto en JavaScript ocupa su propia área de memoria y no la comparte con otros objetos, siempre que accedemos a la variable de user , obtenemos un enlace al mismo objeto.

El patrón Singleton puede implementarse utilizando la función constructora. Se ve así:

 let instance = null; function User(name, age) { if(instance) {   return instance; } instance = this; this.name = name; this.age = age; return instance; } const user1 = new User('Peter', 25); const user2 = new User('Mark', 24); //  true console.log(user1 === user2); 

Cuando se llama a la función constructora, primero verifica si existe el objeto de instance . Si la variable correspondiente no se inicializa, this escribe en la instance . Si la variable ya tiene una referencia a un objeto, el constructor simplemente devuelve una instance , es decir, una referencia a un objeto existente.

El patrón Singleton se puede implementar usando el patrón Módulo. Por ejemplo:

 const singleton = (function() { let instance; function User(name, age) {   this.name = name;   this.age = age; } return {   getInstance: function(name, age) {     if(!instance) {       instance = new User(name, age);     }     return instance;   } } })(); const user1 = singleton.getInstance('Peter', 24); const user2 = singleton.getInstance('Mark', 26); // prints true console.log(user1 === user2); 

Aquí creamos una nueva instancia de user llamando al método singleton.getInstance() . Si ya existe una instancia del objeto, entonces este método simplemente lo devolverá. Si aún no existe dicho objeto, el método crea una nueva instancia del mismo llamando a la función de constructor User .

Patrón de fábrica


El patrón Factory usa los llamados métodos de fábrica para crear objetos. No necesita especificar clases o funciones de constructor que se utilizan para crear objetos.

Este patrón se utiliza para crear objetos en los casos en que no es necesario hacer pública la lógica de su creación. El patrón Factory se puede usar si necesita crear diferentes objetos según las condiciones específicas. Por ejemplo:

 class Car{ constructor(options) {   this.doors = options.doors || 4;   this.state = options.state || 'brand new';   this.color = options.color || 'white'; } } class Truck { constructor(options) {   this.doors = options.doors || 4;   this.state = options.state || 'used';   this.color = options.color || 'black'; } } class VehicleFactory { createVehicle(options) {   if(options.vehicleType === 'car') {     return new Car(options);   } else if(options.vehicleType === 'truck') {     return new Truck(options);     } } } 

Aquí se crean las clases Car y Truck , que permiten el uso de ciertos valores estándar. Se utilizan para crear objetos de car y truck . La clase VehicleFactory también se declara aquí, que se usa para crear nuevos objetos basados ​​en el análisis de la propiedad vehicleType , pasada al método correspondiente del objeto que devuelve en el objeto con options . Aquí se explica cómo trabajar con todo esto:

 const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 4, color: 'silver', state: 'Brand New' }); const truck= factory.createVehicle({ vehicleType: 'truck', doors: 2, color: 'white', state: 'used' }); //  Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); //  Truck {doors: 2, state: "used", color: "white"} console.log(truck); 

VehicleFactory se VehicleFactory objeto de factory de la clase VehicleFactory . Después de eso, puede crear objetos de las clases Car o Truck llamando al método factory.createVehicle() y pasándole el objeto de options con la propiedad vehicleType establecida en car o truck .

Patrón decorador


El patrón Decorator se usa para extender la funcionalidad de los objetos sin modificar las clases existentes o las funciones de constructor. Este patrón se puede usar para agregar ciertas capacidades a los objetos sin modificar el código responsable de su creación.

Aquí hay un ejemplo simple del uso de este patrón:

 function Car(name) { this.name = name; //    this.color = 'White'; } //   ,    const tesla= new Car('Tesla Model 3'); //   -    tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); //  black console.log(tesla.color); 

Consideremos ahora un ejemplo práctico de la aplicación de este patrón. Supongamos que el costo de los automóviles depende de sus características, de las funciones adicionales disponibles para ellos. Sin el uso del patrón Decorador, para describir estos automóviles, tendríamos que crear diferentes clases para diferentes combinaciones de estas funciones adicionales, cada una de las cuales tendría un método para encontrar el costo de un automóvil. Por ejemplo, podría verse así:

 class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { } 

Gracias al patrón en cuestión, puede crear un Car clase base, describiendo, por ejemplo, un automóvil en la configuración básica, cuyo costo se expresa en una cantidad fija. Después de eso, el objeto estándar creado sobre la base de esta clase se puede expandir utilizando funciones de decorador. El "automóvil" estándar procesado por esta función obtiene nuevas oportunidades, lo que, además, afecta su precio. Por ejemplo, este esquema se puede implementar de la siguiente manera:

 class Car { constructor() { //   this.cost = function() { return 20000; } } } // - function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() {   return prevCost + 500; } } // - function carWithAutoTransmission(car) { car.hasAutoTransmission = true;  const prevCost = car.cost(); car.cost = function() {   return prevCost + 2000; } } // - function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() {   return prevCost + 500; } } 

Aquí creamos primero el Car clase base, usado para crear objetos que representan autos como estándar. Luego creamos varias funciones de decorador que nos permiten extender los objetos de la clase base de Car con propiedades adicionales. Estas funciones toman los objetos correspondientes como parámetros. Después de eso, agregamos una nueva propiedad al objeto, indicando con qué nueva característica estará equipado el automóvil, y redefinimos la función de cost del objeto, que ahora devuelve el nuevo costo del automóvil. Como resultado, para "equipar" el auto de configuración estándar con algo nuevo, podemos usar el siguiente diseño:

 const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car); 

Después de eso, puede averiguar el costo del automóvil en una configuración mejorada:

 //       console.log(car.cost()); 

Resumen


En este artículo, examinamos varios patrones de diseño utilizados en JavaScript, pero, de hecho, todavía hay muchos patrones que pueden utilizarse para resolver una amplia gama de problemas.

Si bien el conocimiento de los diversos patrones de diseño es importante para el programador, su uso apropiado es igualmente importante. Al conocer los patrones y el alcance de su aplicación, el programador, al analizar la tarea que tiene ante sí, puede comprender qué tipo de patrón puede ayudar a resolverlo.

Estimados lectores! ¿Qué patrones de diseño usas con más frecuencia?

Source: https://habr.com/ru/post/es427293/


All Articles