📜 ⬆️ ⬇️

Elegant Patterns of Modern JavaScript: RORO

The author of the material, the translation of which we publish today, Bill Soro, says that he wrote the first lines of JavaScript code shortly after the appearance of this language. According to him, if then he would have been told that one day he would release a series of articles on elegant design patterns in JavaScript, he would have died laughing. Then he perceived JS as a strange little language, the writing on which one could with a stretch be called "programming".

But in 20 years, much has changed. Now Bill perceives JavaScript the way Douglas Crockford saw him when he was working on the book JavaScript. Strengths ”: beautiful, elegant and expressive dynamic programming language.


')
In this article, Bill wants to talk about a wonderful little pattern that he has been enjoying with for some time. He hopes that this design pattern will be useful to other programmers as well. Bill says that he does not consider himself the pioneer of this pattern, rather, it’s about the fact that he saw something similar in someone’s code, and then adapted it to fit his needs.

RORO pattern


RORO (Receive an object, return an object - received an object, returned an object), this is the pattern through which most of my functions now accept a single parameter of type object , and many of them also return a value of type object , or are resolved by a similar value.

Partly due to the destructuring capabilities that appeared in ES2015, I realized that RORO is a powerful and useful pattern. I even gave him the name - RORO, which sounds somewhat frivolous, probably, in order to highlight it in a special way.

I want to draw your attention to the fact that destructuring is one of my favorite features in modern JavaScript. We will talk about the strengths of this mechanism a little later, so if you are not familiar with restructuring, you can read more interestingly to continue reading this video .

Here are some reasons why you might like the RORO pattern:


Consider each of these RORO features in more detail.

Named Parameters


Imagine that we have a function that returns a list of users (Users) with a given role (Role), and suppose that we need to provide this function with an argument that is needed to be included in the output of contact information (Contact Info) of each user, and one more argument for inclusion in issuing inactive (Inactive) users. In the traditional approach, declaring such a function would look like this:

 function findUsersByRole ( role, withContactInfo, includeInactive ) {...} 

Calling this function would look like this:

 findUsersByRole( 'admin', true, true ) 

When reading the code, the role of the last two arguments in this call is completely incomprehensible. What do the two true values ​​mean?

What happens if the application almost never needs the contact information of users, but almost always needs data on inactive users? We will have to struggle all the time with the same argument, even though it almost never changes (we will talk more about this later).

In a nutshell, this traditional approach provides potentially incomprehensible code overloaded with unnecessary details that is difficult to understand and difficult to write.

Let's see what happens if instead of the usual list of arguments at the input of a function, a single object is expected with arguments:

 function findUsersByRole ({ role, withContactInfo, includeInactive }) {...} 

Notice that the function declaration looks almost the same as in the previous example, the only difference is that the parameters are now included in curly brackets. This indicates that the function now, instead of receiving three separate arguments, expects one object with the properties role , withContactInfo , and includeInactive .

This mechanism works through destructuring — an opportunity that appeared in ES2015.

Now we can call the function like this:

 findUsersByRole({ role: 'admin', withContactInfo: true, includeInactive: true }) 

So in the code it turns out to be much less ambiguity, now it is easier to read, it is much clearer. In addition, omitting arguments or changing their order no longer causes problems, since the parameters are now represented by the named properties of the object.

For example, we can call a function like this:

 findUsersByRole({ withContactInfo: true, role: 'admin', includeInactive: true }) 

And we can and so:

 findUsersByRole({ role: 'admin', includeInactive: true }) 

In addition, new parameters can be added so that their appearance does not break the code written earlier.

Here it is worth noting one important thing, which is connected with the possibility of calling functions without arguments, that is, so that all the parameters of the function could be optional. Calling this function looks like this:

 findUsersByRole() 

In order for our function to be called without arguments, you need to set the default values ​​of the arguments:

 function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) {...} 

An additional benefit of using destructuring for an object with parameters is that this approach contributes to immunity. When we unstructure an object when it enters the function, we assign the properties of the object to new variables. Changing the values ​​of these variables will not change the source object.

Consider the following example:

 const options = { role: 'Admin', includeInactive: true } findUsersByRole(options) function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) { role = role.toLowerCase() console.log(role) // 'admin' ... } console.log(options.role) // 'Admin' 

Here, despite the fact that we changed the value of the role variable, the value of the properties of the options.role object remained unchanged.

It should be noted that destructuring makes a surface copy of an object, as a result, if any of the properties of an object with arguments is an object (for example, of type array or object ), changing this property, even if assigned to a separate variable, will lead to a change in the original object. I express my gratitude to Yury Khomyakov for paying attention to this .

Default Default Values


Thanks to the innovations of ES2015, when declaring JS functions, you can now set parameter values ​​that are assigned to them by default. In fact, we have already demonstrated the use of default parameters here by adding functions ={} to the declaration after describing an object with parameters.

When using traditional parameters, adding default parameter values ​​might look like this:

 function findUsersByRole ( role, withContactInfo = true, includeInactive ) {...} 

If we need to set the includeInactive parameter to true , we need to explicitly pass undefined as the value for withContactInfo in order to keep the default value:

 findUsersByRole( 'Admin', undefined, true ) 

It all looks very mysterious.

Compare this with the use of an object function with parameters in the declaration:

 function findUsersByRole ({ role, withContactInfo = true, includeInactive } = {}) {...} 

Now this function can be called like this:

 findUsersByRole({ role: 'Admin', includeInactive: true }) 

At the same time, the default value specified for withContactInfo does not go anywhere.

About the necessary parameters of the function


Here we deviate a little from our main topic in order to consider one useful trick concerning the indication of the function parameters, which are absolutely necessary for its proper operation.

How often did you have to write something like the code below?

 function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) { if (role == null) {    throw Error(...) } ... } 

Notice that the double equal sign, == , is used to check the value for both null and undefined .

What if I tell you that instead of all this, you can use the default parameters, thanks to which you can check the receipt of the necessary data in the function?

In order to perform such a check, you first need to declare the function requiredParam() , which returns an error:

 function requiredParam (param) { const requiredParamError = new Error(  `Required parameter, "${param}" is missing.` ) //   - if (typeof Error.captureStackTrace === 'function') {   Error.captureStackTrace(     requiredParamError,     requiredParam   ) } throw requiredParamError } 

By the way, I know that this function does not use RORO, but therefore at the very beginning I didn’t say that absolutely all my functions use this pattern.

Now we can, as the default value for a role , set the function call to requiredParam :

 function findUsersByRole ({ role = requiredParam('role'), withContactInfo, includeInactive } = {}) {...} 

The result is that if someone calls the findUserByRole function without specifying a role , he will see an error message that indicates that he forgot to pass the required parameter to the function, which in our case will look like the Required parameter, "role" is missing .

From a technical point of view, we can use this approach with the usual default parameters. The object here is voluntary. However, it is very convenient, so I told you about it here.

Return from functions of complex value sets


Functions in JavaScript can return only one value. If this value is of type object , it can contain a lot of interesting things.

Imagine a function that saves User objects to a database. When this function returns an object, it can transfer much more information to where it was called from than without using this approach.

For example, usually when saving information to the database in the corresponding functions, the approach is used which means that new rows are inserted into the table, if they do not exist yet, if the corresponding rows exist, they are updated.

In such cases, it would be useful to know what kind of database operation was performed by our function of storing information - INSERT or UPDATE . It would be nice to get an accurate description of what was stored in the database, and the details of the result of the operation would not hurt. For example, it may succeed, it may be merged with a larger transaction and put in a queue, the write attempt may be unsuccessful, as the timeout for the operation has expired.

Returning an object from a function makes it very easy to put all the necessary information into it:

 async saveUser({ upsert = true, transaction, ...userInfo }) { //   return {   operation, //  'INSERT'   status, //  'Success'   saved: userInfo } } 

Technically, the following construct returns a Promise object, which is resolved by an ordinary object of type object , but I suppose this example well illustrates the idea considered here.

Simplify the composition of functions


Function composition is the process of combining two or more functions to create a new function. The composition of functions is similar to the assembly of a system of several pipes through which it is planned to pass our data.
Eric Elliott

You can compose functions using the special function pipe , whose declaration looks like this:

 function pipe(...fns) { return param => fns.reduce(   (result, fn) => fn(result),   param ) } 

This function takes a list of functions and returns one function that can perform these functions from left to right, passing in the first of these functions the arguments passed to the pipe , the second one that the first function returns, and so on.

Perhaps it all looks a bit confusing, so now we look at an example that puts everything in its place. The only limitation of this approach is that each function in the list should receive only one parameter. Fortunately, when we use the RORO pattern, this is not a problem.

Here is our example. There is a function saveUser , which passes the userInfo object through three separate functions, which, respectively, check, normalize and save the data.

 function saveUser(userInfo) { return pipe(   validate,   normalize,   persist )(userInfo) } 

We can use the so-called rest-parameters in the functions validate , normalize , and persist to restructuring only those values ​​that are needed by each of the functions, passing everything else on.

Here, to make it clearer, a bit of code:

 function validate( id, firstName, lastName, email = requiredParam(), username = requiredParam(), pass = requiredParam(), address, ...rest ) { //  -  return {   id,   firstName,   lastName,   email,   username,   pass,   address,   ...rest } } function normalize( email, username, ...rest ) { //   return {   email,   username,   ...rest } } async function persist({ upsert = true, ...info }) { //  userInfo    return {   operation,   status,   saved: info } } 

RO or RO - that is the question


At the very beginning, I said that most of my functions accept objects, and many of them also return objects. Many, but not all. Like any pattern, RORO should be considered only as one of the tools in the developer’s arsenal. We use it where it is useful, making the function parameter list more comprehensible and flexible, and the value returned by the function more expressive.

If you are writing a function that is enough to pass one argument, then passing it an object is already a brute force. Similarly, if you write a function that can pass a simple value to the place of the call, which clearly and clearly expresses everything that is needed, then it is clear that such a function should not return a value of type object .

For example, I almost never use RORO in assertion checking functions. Suppose there is a function isPositiveInteger that checks the value passed to it on whether it is a non-negative integer. When writing such a function, it is very likely that there will be no sense from the RORO pattern. However, when developing JavaScript applications, this pattern can be useful in many other situations.

Dear readers! Do you plan to use the RORO pattern in your code? Or maybe you already use something similar?

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


All Articles