Elegant patterns in modern JavaScript (Bill Sourour compilation cycle article)

Hello, Habr! Quite well-known JavaScript teacher Bill Sourour at the time wrote several articles on modern patterns in JS. As part of this article, we will try to review the ideas that he shared. Not that it was some unique patters, but I hope the article will find its reader. This article is not a "translation" from the point of view of Habr’s policy since I describe my thoughts that Bill’s articles have pointed me at.



Rooro



The abbreviation means Receive an object, return an object - get an object, return an object. I provide a link to the original article: link



Bill wrote that he came up with a way to write functions in which most of them take only one parameter - an object with function arguments. They also return an object of results. Bill was inspired by the restructuring of this idea (one of the ES6 features).



For those who do not know about the destructuring, I will give the necessary explanations during the story.



Imagine that we have user data containing its rights to certain sections of the application presented in the data object. We need to show certain information based on this data. To do this, we could offer the following implementation:



//   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)
      
      





Using the code above, we would achieve the desired result. However, there are several pitfalls in writing code this way.



Firstly, the call to the findUsersByRule



function findUsersByRule



very doubtful. Notice how ambiguous the last two parameters are. What happens if our application almost never needs contact information (withContactInfo) but almost always needs inactive users (includeInactive)? We will always have to pass logical values. Now while the function declaration is next to its call, this is not so scary, but imagine that you see such a call somewhere made in another module. You will have to look for a module with a function declaration in order to understand why two logical values ​​in pure form are transferred to it.



Secondly, if we want to make some parameters mandatory, we will have to write something like this:



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





In this case, our function, in addition to its search responsibilities, will also perform validation, and we just wanted to find users by certain parameters. Of course, the search function can take validation functions, but then the list of input parameters will expand. This is also a minus of such a coding pattern.



Destructuring involves breaking down a complex structure into simple parts. In JavaScript, such a complex structure is usually an object or an array. Using the destructuring syntax, you can extract small fragments from arrays or objects. This syntax can be used to declare variables or their purpose. You can also manage nested structures using the syntax of nested destructuring already.



Using destructuring, the function from our previous example will look like this:



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





Please note that our function looks almost identical, except that we put brackets around our parameters. Instead of receiving three different parameters, our function now expects a single object with properties: rule



, withContactInfo



and includeInactive



.



This is much less ambiguous, much easier to read and understand. In addition, skipping or another order of our parameters is no longer a problem, because now they are named properties of the object. We can also safely add new parameters to the function declaration. In addition, since Since destructuring copies the passed value, its changes in the function will not affect the original.



The problem with the required parameters can also be solved in a more elegant way.



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





If we do not pass the value of rule, then the function passed by default will work, which will throw an exception.



Functions in JS can return only one value, so you can use an object to transfer more information. Of course, we don’t always need the function to return a lot of information, in some cases we will be satisfied with the return of the primitive, for example, findUserId



quite naturally return one identifier by some condition.



Also, this approach simplifies the composition of functions. Indeed, with composition, functions should take only one parameter. The RORO pattern adheres to the same contract.



Bill Sourour: “Like any template, RORO should be seen as another tool in our toolbox. We use it where it benefits, making the parameter list more understandable and flexible, and the return value more expressive. ”



Ice factory



You can find the original article at this link.



According to the author, this template is a function that creates and returns a frozen object.



Bill thinks. that in some situations this pattern can replace the usual ES6 classes for us. For example, we have a certain food basket in which we can add / remove products.



ES6 class:



 // 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 })
      
      





Objects created using the new



keyword are mutable, i.e. we can override the class instance method.



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





It should also be remembered that classes in JS are implemented on prototype delegation, therefore, we can change the implementation of the method in the prototype of the class and these changes will affect all existing instances (I talked about this in more detail in the article about 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!"
      
      





Agree, such features can cause us a lot of trouble.



Another common problem is assigning an instance method to an event handler.



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





Clicking the button will not empty the basket. The method assigns a new property to our button with the name db and sets this property to [] instead of affecting the db of the cart object. However, there are no errors in the console, and your common sense will tell you that the code should work, but it is not.



To make this code work, you have to write an arrow function:



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





Or bind the context with a bind:



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





The Ice Factory will help us avoid these traps.



 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 })
      
      





Features of this pattern:





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







Conclusion



When we are talking about the architecture of the software being developed, we should always make convenient compromises. There are no strict rules and restrictions in this path, each situation is unique, so the more patterns in our arsenal, the more likely it is that we will choose the best architecture option in a particular situation.



All Articles