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:
- It uses named function parameters.
- It allows you to more clearly highlight the parameters of functions assigned by default.
- It helps return complex sets of values from functions.
- It simplifies the composition of functions.
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.` )
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 ElliottYou 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?
