Introduction
โIndeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. ...[Therefore,] making it easy to read makes it easier to write.โ - Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
Writing code that is easy to understand, modify and extend is a good goal for a software developer. But it's hard. So hard that multiple books have been written about the topic and people do conference and meetup talks about it all the time around the world.
If you have been writing or reading a lot of Javascript, you have probably seen function calls like these:
getUnicorns(10, 5, 10, true);
getUnicorns(7);
This code is very hard to read. What do those numbers and booleans mean?
These functions grow up in the wild. Maybe it started as a very simple function and with new requirements, people just added these arguments one by one and finally someone with fresh look at the code feels very confused. Or maybe it started with four arguments but the one who wrote it and immediately after that called, knew what they wanted to get done and had everything fresh in mind.
Regardless of how it happened, there's a problem. In the future, someone (another person or even the future you) will encounter this code and to understand what's going on, they have to do research and keep a lot of things in mind. All of this is distracting them from building quality code.
Unreadable code attracts unreadable code (and bugs ๐).
How does Python do it? Named arguments
Learning from another languages can be a great way to help us become better developers. At the same time, it's important to stick to idiomatic style for the code that you write. So it's an elegant balance.
In Python, there's a mechanic called named arguments (also known as keyword arguments) which are arguments that when the function is called, must be named.
def get_unicorns(amount, min_age = 0, max_age = 100, only_magical = false):
pass
get_unicorns(10, min_age=5, max_age=10, only_magical=true)
Much easier to read!
Let's make our JS code better
Unfortunately we don't have named arguments in Javascript. However, with the ES6 Object Destructuring, we can do something similar.
A quick primer on Object Destructuring
/* Extract properties you're interested in */
const { name, age } = unicorn
/* is the same as */
const name = unicorn.name
const age = unicorn.age
/* ----- */
/* Give default values to destructured properties */
const { name, age = 5 } = unicorn
/* which is same as */
const name = unicorn.name
const age = unicorn.age !== undefined ? unicorn.age : 5
/* ----- */
/* Give default value to function parameter */
function getUnicorns(amount, options = {})
"Named arguments" in Javascript
Using our knowledge of object destructuring, let's make our original code more readable.
function getUnicorns(amount, {minAge = 0, maxAge = 99, onlyMagical = false} = {}) {}
getUnicorns(10, {minAge: 5, maxAge: 10, onlyMagical: true});
getUnicorns(7);
getUnicorns(5, {onlyMagical: true});
Just like in our Python example, reading through code like this is much more enjoyable and we can focus on important pieces: the logic and goals.
In addition to improved readability, another benefit of this object destructuring approach is that you don't have to worry about the order of arguments or even the existence of them all.
If you feel like destructuring in function definition gets crowded, especially with default values, you can extract that into the function body:
function getUnicorns(amount, opts = {}) {
const {
minAge = 0,
maxAge = 99,
onlyMagical = false
} = opts
/* rest of the function body here */
}
A downside of the previous example that you lose visibility on the default values and options available in your editor's inline autocomplete or docs.
Further improvement with Typescript
If you're using Typescript, you can gain a lot more with this approach.
type UnicornFilter = {
minAge?: Number;
maxAge?: Number;
onlyMagical?: Boolean;
}
function getUnicorns(
amount: Number,
{minAge = 0, maxAge = 99, onlyMagical = false}: UnicornFilter = {},
) {}
getUnicorns(10, {minAge: 5, maxAge: 10, onlyMagical: true});
getUnicorns(7);
getUnicorns(5, {onlyMagical: true});
By defining the type as UnicornFilter
, we gain a couple of things:
- you don't end up calling the function with arguments that shouldn't be there
- you pass in correct values for arguments
- the autocomplete in your editor becomes better because it knows what to expect.
Conclusion
Keeping readability in mind when you are working on code is worth the challenge. Usually things rot when small changes pile up over time. Starting simple and then being asked to "just add this one thing", you don't necessarily think about the long-term readability.
Be brave to refactor your code when you add small things to it. It's always easiest to do when the code and what it does is fresh on your mind rather than the time in 6 months when your new team member is looking at it and trying to figure out what it's supposed to do.
Top comments (3)
I like this.
I'm not so sure how I feel about
because it reads like assigning 'foo' to
foo
and then assigningbar.foo
tofoo
to override it... but it's aconst
. I mean, I've tried it and it behaves like yourconst age = unicorn.age !== undefined ? unicorn.age : 5
example, but that's not what it looks like it should do. What's in the box, amirite?I agree that the syntax for that is really confusing. Every time I write it, I have to check to make sure I'm putting things in the right place.
It gets even more confusing when you also rename properties:
What it does is essentially:
Excellent Best Practice.You may should mention the huge advantage in Class constructor arguments you gain in typescript with this Codestyle. Refactoring a parent or abstract class becomes so easy.