re: Elegant patterns in modern JavaScript: RORO VIEW POST

FULL DISCUSSION
 

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.

 

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.

 

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.

 

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.

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.

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.

code of conduct - report abuse