Stop Using Function Parameters Now
The contract says “given parameters X, Y, Z” you’ll get a result “R”.
The thing is, contracts tend to change over time (and boy, do they).
Let’s take a look at a very simple example:
The contract is simple — provided a name, the function will return a greeting message. A name is all we’ve got and all we need at the moment.
In a few months though, when the function is already used all over the code base, we find ourselves in a need of adding a last name to the greeting message.
We could, of course, just use the name parameter for the last name as well, but since we’re deeply aware of the differences between Internationalization and Localization we know that in some countries the last name can come first. We want our product to be the best fit for our users around the globe, so we want to support this use case.
Since the function is already used in many places across the code base we want to make sure it’s backwards compatible.
Let’s do this, again, in a very naive way:
Thus, for the backwards compatibility, if the function isn’t provided with the last name it will just greet the old way, but if it is, it will look at the ordering and greet accordingly.
So far so good, let’s take a look at all the possible usages of this function:
There is one slight problem you can notice here and it is the readability. For example, we can guess by the usage that the first parameter is the name and the second is the last name. However, without knowing the implementation we can’t say or possibly know what the true stands for.
Not the biggest problem we have though, at the end of the day we have autocompletion and signature description in almost any IDE.
Let’s give it a few more months to spread across the code base and then realize that there is also a name prefix and (hell, why not!) a middle name.
And the ordering… oh boy, we better not talk about the implementation.
Anyways, back to the signature. We want to support name prefixes (Mr. , Mrs. , Ms. etc.) and middle names. The signature should be changed again, and once again it should be backwards compatible:
It is backwards compatible, meaning the invocations we saw previously would still work without issues:
But what if we try to use the new API in all these use cases? Since you can’t just pass an argument without passing all the previous arguments it would look like that:
All this could have been easily avoided if we used a single object as a function parameter and seized the great power of object destructuring.
Let’s reiterate on our example.
A function that receives a name and returns a greeting message:
Backwards compatible function that works with name and last name for all countries:
Backwards compatible function that works with name, last name, middle name and prefix:
Ain’t that great?!
Wait, but what about the disadvantages?
The destructuring approach is flexible and sometimes it’s too flexible.
For instance, in our example lastNameFirst doesn’t make any sense without lastName and while with ordered parameters approach it actually cannot be passed without lastName being passed , with destructuring approach you can pass it even if lastName isn’t passed. While it doesn’t pose any problem with the current implementation, it could be problematic in some cases.
Arguably though, the problem is not with the approach itself but with the definition of contract. When we define an interface like in the example, we declare that any parameter can be passed optionally. If we want to create a bond between two parameters we should reflect this bond in the contract, for example like this (or in any one of dozens of ways it can be solved):
Although this is a very specific use case it’s worth to mention.
For example sum(...numbersToSum) . This can’t be done with the object destructuring pattern.
Other than that I can’t think of examples where the ordered approach would beat the destructuring approach, but I’d love to learn more about such use cases from your comments.
Many will argue that if we pass the parameters as an object then the creation of this object every time we invoke the function would negatively impact the performance of the app.
Next, let’s actually run some benchmark and see how big the difference is (if at all).
I used PerfLink JS playground to run and compare invocations of ordered parameters vs named parameters in a loop and here are the results:
10000 invocations — 1698 ops/s for ordered (no object) vs 1339 ops/s for named (creating a new object on each invocation). That makes it 22% difference.
100000 invocations — 252 ops/s for ordered vs 233 ops/s for named which is 8% difference
1000000 invocations — 33 ops/s for ordered vs 32 ops/s for named which is 3% difference
10000000 invocations — 2 ops/s for ordered vs 2 ops/s for named, which is no difference at all.
My take on this (and please correct me if I’m wrong): the more intensive the program, the less performance difference it makes, which, in turn, means that the object creation relative impact on the app is negligible.
Using ordered function parameters is:
Using single parameter and object destructuring is:
Clear and concise
Easy to keep backwards compatible
Explicit, hence less room for mistakes
Follow me if you liked the article, comment/send a message here, or DM me on Twitter if you have any questions.