Patrones elegantes en JavaScript moderno (ciclo de equipo de Bill Sourour)

Hola Habr! El maestro de JavaScript bastante conocido Bill Sourour en ese momento escribió varios artículos sobre patrones modernos en JS. Como parte de este artículo, trataremos de revisar las ideas que compartió. No es que se tratara de patrones únicos, pero espero que el artículo encuentre su lector. Este artículo no es una "traducción" desde el punto de vista de la política de Habr desde Describo mis pensamientos que los artículos de Bill me han señalado.

Rooro


La abreviatura significa Recibir un objeto, devolver un objeto: obtener un objeto, devolver un objeto. Proporciono un enlace al artículo original: enlace

Bill escribió que se le ocurrió una forma de escribir funciones en la que la mayoría de ellos aceptan solo un parámetro: un objeto con argumentos de función. También devuelven un objeto de resultados. Bill se inspiró en la reestructuración de esta idea (una de las características de ES6).

Para aquellos que no saben sobre la desestructuración, daré las explicaciones necesarias durante la historia.

Imagine que tenemos datos de usuario que contienen sus derechos sobre ciertas secciones de la aplicación presentadas en el objeto de datos. Necesitamos mostrar cierta información basada en estos datos. Para hacer esto, podríamos ofrecer la siguiente implementación:

//   const user = { name: 'John Doe', login: 'john_doe', password: 12345, active: true, rules: { finance: true, analitics: true, hr: false } }; //   const users = [user]; //,     function findUsersByRule ( rule, withContactInfo, includeInactive) { //        active const filtredUsers= users.filter(item => includeInactive ? item.rules[rule] : item.active && item.rules[rule]); //  ()   ( )     withContactInfo return withContactInfo ? filtredUsers.reduce((acc, curr) => { acc[curr.id] = curr; return acc; }, {}) : filtredUsers.map(item => item.id) } //     findUsersByRule( 'finance', true, true) 

Usando el código anterior, obtendríamos el resultado deseado. Sin embargo, hay varias trampas al escribir código de esta manera.

En primer lugar, la llamada a la función findUsersByRule muy dudosa. Observe cuán ambiguos son los dos últimos parámetros. ¿Qué sucede si nuestra aplicación casi nunca necesita información de contacto (conContactInfo) pero casi siempre necesita usuarios inactivos (includeInactive)? Siempre tendremos que pasar valores lógicos. Ahora, mientras la declaración de función está al lado de su llamada, esto no da tanto miedo, pero imagine que ve una llamada de este tipo en otro módulo. Deberá buscar un módulo con una declaración de función para comprender por qué se le transfieren dos valores lógicos en forma pura.

En segundo lugar, si queremos que algunos parámetros sean obligatorios, tendremos que escribir algo como esto:

 function findUsersByRule ( role, withContactInfo, includeInactive) { if (!role) { throw Error(...) ; } //...  } 

En este caso, nuestra función, además de sus responsabilidades de búsqueda, también realizará la validación, y solo queríamos encontrar usuarios por ciertos parámetros. Por supuesto, la función de búsqueda puede tomar funciones de validación, pero luego la lista de parámetros de entrada se expandirá. Esto también es una desventaja de dicho patrón de codificación.

La desestructuración implica descomponer una estructura compleja en partes simples. En JavaScript, una estructura tan compleja suele ser un objeto o una matriz. Usando la sintaxis de desestructuración, puede extraer pequeños fragmentos de matrices u objetos. Esta sintaxis se puede usar para declarar variables o su propósito. También puede gestionar estructuras anidadas utilizando la sintaxis de la desestructuración anidada.

Usando la desestructuración, la función de nuestro ejemplo anterior se verá así:

 function findUsersByRule ({ rule, withContactInfo, includeInactive}) { //    } findUsersByRule({ rule: 'finance', withContactInfo: true, includeInactive: true}) 

Tenga en cuenta que nuestra función se ve casi idéntica, excepto que ponemos corchetes alrededor de nuestros parámetros. En lugar de recibir tres parámetros diferentes, nuestra función ahora espera un solo objeto con propiedades: rule , withContactInfo e includeInactive .

Esto es mucho menos ambiguo, mucho más fácil de leer y comprender. Además, omitir u otro orden de nuestros parámetros ya no es un problema, porque ahora se denominan propiedades del objeto. También podemos agregar con seguridad nuevos parámetros a la declaración de la función. Además, desde Como la desestructuración copia el valor pasado, sus cambios en la función no afectarán al original.

El problema con los parámetros requeridos también se puede resolver de una manera más elegante.

 function requiredParam (param) { const requiredParamError = new Error( `Required parameter, "${param}" is missing.` ) } function findUsersByRule ({ rule = requiredParam('rule'), withContactInfo, includeInactive} = {}) {...} 

Si no pasamos el valor de la regla, entonces la función pasada por defecto funcionará, lo que arrojará una excepción.

Las funciones en JS pueden devolver solo un valor, por lo que puede usar un objeto para transferir más información. Por supuesto, no siempre necesitamos una función para devolver mucha información, en algunos casos estaremos satisfechos con el retorno de una primitiva, por ejemplo, findUserId , naturalmente, devolverá un identificador por alguna condición.

Además, este enfoque simplifica la composición de funciones. De hecho, con la composición, las funciones deben tomar solo un parámetro. El patrón RORO se adhiere al mismo contrato.

Bill Sourour: “Al igual que cualquier plantilla, RORO debería verse como otra herramienta en nuestra caja de herramientas. "Lo usamos donde se beneficia, haciendo que la lista de parámetros sea más comprensible y flexible, y el valor de retorno más expresivo".

Fabrica de hielo


Puede encontrar el artículo original en este enlace.

Según el autor, esta plantilla es una función que crea y devuelve un objeto congelado.

Bill piensa. que en algunas situaciones este patrón puede reemplazar las clases habituales de ES6 para nosotros. Por ejemplo, tenemos una determinada canasta de alimentos en la que podemos agregar / eliminar productos.

Clase ES6:

 // ShoppingCart.js class ShoppingCart { constructor({db}) { this.db = db } addProduct (product) { this.db.push(product) } empty () { this.db = [] } get products () { return Object .freeze([...this.db]) } removeProduct (id) { // remove a product } // other methods } // someOtherModule.js const db = [] const cart = new ShoppingCart({db}) cart.addProduct({ name: 'foo', price: 9.99 }) 

Los objetos creados con la new palabra clave son mutables, es decir, podemos anular el método de instancia de clase.

 const db = [] const cart = new ShoppingCart({db}) cart.addProduct = () => 'nope!' //   JS  cart.addProduct({ name: 'foo', price: 9.99 }) // output: "nope!"     

También debe recordarse que las clases en JS se implementan en la delegación de prototipos, por lo tanto, podemos cambiar la implementación del método en el prototipo de la clase y estos cambios afectarán a todas las instancias existentes (hablé de esto con más detalle en el artículo sobre OOP ).

 const cart = new ShoppingCart({db: []}) const other = new ShoppingCart({db: []}) ShoppingCart.prototype .addProduct = () => 'nope!' //     JS cart.addProduct({ name: 'foo', price: 9.99 }) // output: "nope!" other.addProduct({ name: 'bar', price: 8.88 }) // output: "nope!" 

De acuerdo, tales características pueden causarnos muchos problemas.

Otro problema común es asignar un método de instancia a un controlador de eventos.

 document .querySelector('#empty') .addEventListener( 'click', cart.empty ) 

Al hacer clic en el botón no se vaciará la cesta. El método asigna una nueva propiedad a nuestro botón llamado db y establece esta propiedad en [] en lugar de afectar la db del objeto cart. Sin embargo, no hay errores en la consola, y su sentido común le dirá que el código debería funcionar, pero no es así.

Para que este código funcione, debe escribir una función de flecha:

 document .querySelector("#empty") .addEventListener( "click", () => cart.empty() ) 

O enlazar el contexto con un enlace:

 document .querySelector("#empty") .addEventListener( "click", cart.empty.bind(cart) ) 

La fábrica de hielo nos ayudará a evitar estas trampas.

 function makeShoppingCart({ db }) { return Object.freeze({ addProduct, empty, getProducts, removeProduct, // others }) function addProduct (product) { db.push(product) } function empty () { db = [] } function getProducts () { return Object .freeze([...db]) } function removeProduct (id) { // remove a product } // other functions } // someOtherModule.js const db = [] const cart = makeShoppingCart({ db }) cart.addProduct({ name: 'foo', price: 9.99 }) 

Características de este patrón:

  • no es necesario usar la nueva palabra clave
  • no es necesario atar esto
  • el carrito es totalmente portátil
  • se pueden declarar variables locales que no serán visibles desde el exterior

 function makeThing(spec) { const secret = 'shhh!' return Object.freeze({ doStuff }) function doStuff () { //    secret } } // secret    const thing = makeThing() thing.secret // undefined 

  • patrón admite herencia
  • crear objetos usando Ice Factory es más lento y requiere más memoria que usar una clase (en muchas situaciones, es posible que necesitemos clases, por lo que recomiendo este artículo )
  • Esta es una función común que se puede asignar como devolución de llamada

Conclusión


Cuando hablamos de la arquitectura del software que se está desarrollando, siempre debemos hacer compromisos convenientes. No hay reglas y restricciones estrictas en este camino, cada situación es única, por lo tanto, cuanto más patrones haya en nuestro arsenal, más probable es que elijamos la mejor opción de arquitectura en una situación particular.

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


All Articles