DEV Community

Bill Sourour
Bill Sourour

Posted on • Edited on

Elegant patterns in modern JavaScript: RORO

I wrote my first few lines of JavaScript not long after the language was invented. If you told me at the time that I would one day be writing a series of articles about elegant patterns in JavaScript, I would have laughed you out of the room. I thought of JavaScript as a strange little language that barely even qualified as “real programming.”

Well, a lot has changed in the 20 years since then. I now see in JavaScript what Douglas Crockford saw when he wrote JavaScript: The Good Parts: “An outstanding, dynamic programming language … with enormous, expressive power.”

So, without further ado, here is a wonderful little pattern I’ve been using in my code lately. I hope you come to enjoy it as much as I have.

Please note : I’m pretty sure I did not invent any of this. Chances are I came across it in other people’s code and eventually adopted it myself.

Receive an object, return an object (RORO)

Most of my functions now accept a single parameter of type object and many of them return or resolve to a value of type object as well.

Thanks in part to the destructuring feature introduced in ES2015, I’ve found this to be a powerful pattern. I’ve even given it the silly name, “RORO” because… branding? 🤷‍♂️

Note: Destructuring is one of my favorite features of modern JavaScript. We’re going to be taking advantage of it quite a bit throughout this article, so if you’re not familiar with it, here’s a quick video from Beau Carnes to get you up to speed.

Here are some reasons why you’ll love this pattern:

  • Named parameters
  • Cleaner default parameters
  • Richer return values
  • Easy function composition

Let’s look at each one.

Named Parameters

Suppose we have a function that returns a list of Users in a given Role and suppose we need to provide an option for including each User’s Contact Info and another option for including Inactive Users, traditionally we might write:

function findUsersByRole (
  role,
  withContactInfo,
  includeInactive
) {...}
Enter fullscreen mode Exit fullscreen mode

A call to this function might then look like:

findUsersByRole(
  'admin',
  true,
  true
)
Enter fullscreen mode Exit fullscreen mode

Notice how ambiguous those last two parameters are. What does “true, true” refer to?

What happens if our app almost never needs Contact Info but almost always needs Inactive Users? We have to contend with that middle parameter all the time, even though it’s not really relevant (more on that later).

In short, this traditional approach leaves us with potentially ambiguous, noisy code that’s harder to understand and trickier to write.

Let’s see what happens when we receive a single object instead:

function findUsersByRole ({
  role,
  withContactInfo,
  includeInactive
}) {...}
Enter fullscreen mode Exit fullscreen mode

Notice our function looks almost identical except that we’ve put braces around our parameters. This indicates that instead of receiving three distinct parameters, our function now expects a single object with properties named role, withContactInfo, and includeInactive.

This works because of a JavaScript feature introduced in ES2015 called Destructuring.

Now we can call our function like this:

findUsersByRole({
  role: 'admin',
  withContactInfo: true,
  includeInactive: true
})
Enter fullscreen mode Exit fullscreen mode

This is far less ambiguous and a lot easier to read and understand. Plus, omitting or re-ordering our parameters is no longer an issue since they are now the named properties of an object.

For example, this works:

findUsersByRole({
  withContactInfo: true,
  role: 'admin',
  includeInactive: true
})
Enter fullscreen mode Exit fullscreen mode

And so does this:

findUsersByRole({
  role: 'admin',
  includeInactive: true
})
Enter fullscreen mode Exit fullscreen mode

This also makes it possible to add new parameters without breaking old code.

One important note here is that if we want all the parameters to be optional, in other words, if the following is a valid call…

findUsersByRole()
Enter fullscreen mode Exit fullscreen mode

… we need to set a default value for our parameter object, like so:

function findUsersByRole ({
  role,
  withContactInfo,
  includeInactive
} = {}) {...}
Enter fullscreen mode Exit fullscreen mode

An added benefit of using destructuring for our parameter object is that it promotes immutability. When we destructure the object on its way into our function we assign the object’s properties to new variables. Changing the value of those variables will not alter the original object.

Consider the following:

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'
Enter fullscreen mode Exit fullscreen mode

Even though we change the value of role the value of options.role remains unchanged.

It’s worth noting that destructuring makes a_ shallow _copy so if any of the properties of our parameter object are of a complex type (e.g. array or object) changing them would indeed affect the original.

Cleaner Default Parameters

With ES2015 JavaScript functions gained the ability to define default parameters. In fact, we recently used a default parameter when we added ={} to the parameter object on our findUsersByRole function above.

With traditional default parameters, our findUsersByRole function might look like this.

function findUsersByRole (
  role,
  withContactInfo = true,
  includeInactive
) {...}
Enter fullscreen mode Exit fullscreen mode

If we want to set includeInactive to true we have to explicitly pass undefined as the value for withContactInfo to preserve the default, like this:

findUsersByRole(
  'Admin',
  undefined,
  true
)
Enter fullscreen mode Exit fullscreen mode

How hideous is that?

Compare it to using a parameter object like so:

function findUsersByRole ({
  role,
  withContactInfo = true,
  includeInactive
} = {}) {...}
Enter fullscreen mode Exit fullscreen mode

Now we can write…

findUsersByRole({
  role: Admin,
  includeInactive: true
})
Enter fullscreen mode Exit fullscreen mode

… and our default value for withContactInfo is preserved.

BONUS: Required Parameters

How often have you written something like this?

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

Note: We use double equals (==) above to test for both null and undefined with a single statement.

What if I told you that you could use default parameters to validate required parameters instead?

First, we need to define a requiredParam() function that throws an Error.

Like this:

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

  // preserve original stack trace
  if (typeof Error.captureStackTrace === function) {
    Error.captureStackTrace(
      requiredParamError,
      requiredParam
    )
  }

  throw requiredParamError
}
Enter fullscreen mode Exit fullscreen mode

I know, I know. requiredParam doesn’t RORO. That’s why I said many of my functions — not all .

Now, we can set an invocation of requiredParam as the default value for role, like so:

function findUsersByRole ({
  role = requiredParam('role'),
  withContactInfo,
  includeInactive
} = {}) {...}
Enter fullscreen mode Exit fullscreen mode

With the above code, if anyone calls findUsersByRole without supplying a role they will get an Error that says Required parameter, “role” is missing.

Technically, we can use this technique with regular default parameters as well; we don’t necessarily need an object. But this trick was too useful not to mention.

Richer Return Values

JavaScript functions can only return a single value. If that value is an object it can contain a lot more information.

Consider a function that saves a User to a database. When that function returns an object it can provide a lot of information to the caller.

For example, a common pattern is to “upsert” or “merge” data in a save function. Which means, we insert rows into a database table (if they do not already exist) or update them (if they do exist).

In such cases, it would be handy to know wether the operation performed by our Save function was an INSERT or an UPDATE. It would also be good to get an accurate representation of exactly what was stored in the database, and it would be good to know the status of the operation; did it succeed, is it pending as part of a larger transaction, did it timeout?

When returning an object, it’s easy to communicate all of this info at once.

Something like:

async saveUser({
  upsert = true,
  transaction,
  ...userInfo
}) {
  // save to the DB
  return {
    operation, // e.g 'INSERT'
    status, // e.g. 'Success'
    saved: userInfo
  }
}
Enter fullscreen mode Exit fullscreen mode

Technically, the above returns a Promise that resolves to an object but you get the idea.

Easy Function Composition

“Function composition is the process of combining two or more functions to produce a new function. Composing functions together is like snapping together a series of pipes for our data to flow through.” —  Eric Elliott

We can compose functions together using a pipe function that looks something like this:

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

The above function takes a list of functions and returns a function that can apply the list from left to right, starting with a given parameter and then passing the result of each function in the list to the next function in the list.

Don’t worry if you’re confused, there’s an example below that should clear things up.

One limitation of this approach is that each function in the list must only receive a single parameter. Luckily, when we RORO that’s not a problem!

Here’s an example where we have a saveUser function that pipes a userInfo object through 3 separate functions that validate, normalize, and persist the user information in sequence.

function saveUser(userInfo) {
  return pipe(
    validate,
    normalize,
    persist
  )(userInfo)
}
Enter fullscreen mode Exit fullscreen mode

We can use a rest parameter in our validate, normalize, and persist functions to destructure only the values that each function needs and still pass everything back to the caller.

Here’s a bit of code to give you the gist of it:

function validate(
  id,
  firstName,
  lastName,
  email = requiredParam(),
  username = requiredParam(),
  pass = requiredParam(),
  address,
  ...rest
) {
  // do some validation
  return {
    id,
    firstName,
    lastName,
    email,
    username,
    pass,
    address,
    ...rest
  }
}

function normalize(
  email,
  username,
  ...rest
) {
  // do some normalizing
  return {
    email,
    username,
    ...rest
  }
}

async function persist({
  upsert = true,
  ...info
}) {
  // save userInfo to the DB
  return {
    operation,
    status,
    saved: info
  }
}
Enter fullscreen mode Exit fullscreen mode

To RO or not to RO, that is the question

I said at the outset, most of my functions receive an object and many of them return an object too.

Like any pattern, RORO should be seen as just another tool in our tool box. We use it in places where it adds value by making a list of parameters more clear and flexible and by making a return value more expressive.

If you’re writing a function that will only ever need to receive a single parameter, then receiving an object is overkill. Likewise, if you’re writing a function that can communicate a clear and intuitive response to the caller by returning a simple value, there is no need to return an object.

An example where I almost never RORO is assertion functions. Suppose we have a function isPositiveInteger that checks wether or not a given parameter is a positive integer, such a function likely wouldn’t benefit from RORO at all.


If you enjoyed this article, please tap the ♥️ icon to help spread the word. And if you want to read more stuff like this, please sign up for my Dev Mastery newsletter below.

Sign up to the DevMastery Newsletter

I keep your info private and I NEVER spam.

Top comments (13)

Collapse
 
dmfay profile image
Dian Fay

I think the biggest problem I have with using this approach as a matter of course is that, great, you have a simple signature and can return whatever you want, but unless you put some serious effort into generating and maintaining documentation, it's impossible to use your function effectively without understanding it in its entirety. A signature that enumerates its arguments tells you how to use it, making API docs an improvement rather than a necessity. Primitive return values are similarly easier to process.

It makes sense if you're really dealing in, say, configuration objects, states, and other naturally complex values. But that's because functions that operate on complex objects are understood in complex terms. When you come into render(), decomposition works great because you're thinking about your props and your state. Replacing ordinary arguments and return values is a different story: it may make a function look nicer at first glance, but it's harder to work with in any context larger than the function by itself -- which includes practically every case except the unit tests.

Collapse
 
billsourour profile image
Bill Sourour • Edited

Thank you for your feedback Dian.

I first encountered the concept of receiving a single object rather than individual parameters back when jQuery ruled the world. Some functions in jQuery – especially those related to $ajax – received a so-called options object.

Back then, I didn't like this pattern at all for the exact reasons you mentioned; I found that it made functions more opaque and added complexity that could only be overcome by adding more documentation.

Since then, two things happened which changed my mind. The first is the advent of destructuring syntax. The second, equally important change, is in the sophistication of our code editors.

For example, here is how VS Code treats my findUserByRole function. No extra documentation needed.

Collapse
 
dmfay profile image
Dian Fay

I use options objects myself in Massive -- it's the only way to keep signature size under a sane maximum when you have a dozen different things that can affect the shape of your results and don't want to move to a builder-style pattern. The tradeoff is that I have to be very careful with documentation; that and the fact that the options are a known set applied consistently across the API are why it's usable at all.

Destructuring is more a convenience than a compelling argument for working like this. You could always pass objects and return objects; now you can express it a bit more elegantly, but it's still the same concept. What really enables you is your editor, and that's not something I'd be willing to depend on. People use other editors with different featuresets, and even if everyone standardized on VSCode, syntax tree analysis isn't available when you're reading diffs or browsing an open source codebase on the web. Readability as plain text is critical, and sacrificing that to achieve an architecture that actually increases local complexity seems like a bad trade for most cases.

Thread Thread
 
billsourour profile image
Bill Sourour • Edited

Thanks again for your feedback.

I would argue that we are actually increasing plain text readability rather than diminishing it.

In terms of reading the function signature, I don't think the extra curly braces impede readability. And, in terms of reading code that consumes our function, I think a destructured parameter object has the advantage.

Imagine coming across the following examples in plain text...

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

...isn't the second one clearer? Especially when we are in a plain text environment that can't easily navigate to the function's signature.

Thread Thread
 
dmfay profile image
Dian Fay • Edited

Essentially what you have with this example is a criteria object. This is another instance where the idea works out okay, because you're approaching the unary argument as criteria, not as a discrete role and flags. You don't even really need destructuring to work with it: you can just iterate Object.keys and pop the values into your prepared statement parameters or analogous structure.

Where the real problems start to pop up is when you bundle information that shouldn't be bundled. Think dispatchMessage('channel', 'message', true) versus dispatchMessage({channel: 'channel', message: 'message', emitOnReceipt: true}). It's true that you can make an educated guess as to what emitOnReceipt does without having to look at dispatchMessage's signature. But are there other behaviors? Can message itself be a complex object? What happens if I pass a "meta-message" object that contains x, y, z other fields (even if the function destructures arguments immediately, arguments is still around to make things interesting)?

A well-formed signature does as well with some of these kinds of questions as what's arguably an abuse of anonymous objects, and does better with others; notably, the possibility of pollution is obviated. If you're going to operate on a "meta-message" concept that ties these disparate values together, it should be a proper class. And sometimes it's worth doing that! But throwing anonymous objects around is something that really needs to be considered carefully.

Collapse
 
martinhaeusler profile image
Martin Häusler

These kinds of API usage issues can be avoided by using flow or TypeScript and a suitable editor. Code completion will tell you exactly what you can put into the options argument, and what to expect from the result. The actual downside in this approach lies elsewhere: constructing these additional "message" objects takes time. So I would not advice using this technique too excessively, especially in performance-critical regions of the code.

Collapse
 
ben profile image
Ben Halpern

Great to have you here Bill!

Collapse
 
billsourour profile image
Bill Sourour

Great to be here :)

Collapse
 
entrptaher profile image
Md Abu Taher

I love this pattern and use this almost all the time. It's obviously informative and clearer to read.

The only time it causes problem is when we have to write same arguments over and over in different lines.

Collapse
 
onekiloparsec profile image
Cédric Foellmi

Clear and convincing. Currently updating my code ... :-)

Collapse
 
decadef20 profile image
Decade

I have used this approach for a while which I did not even think it as a pattern.Thank your point.

Collapse
 
lukvonstrom profile image
Lukas Fruntke

I really like this article, it is pretty good explained and the examples are very clear and easy to understand :)

Collapse
 
mnayeem profile image
Nayeem Rahman

Not sure I'd use it in many places but definitely interesting. But how did I not know about that required parameter pattern?? Definitely adding that everywhere.