I'm a huge believer in self-documenting code. In fact, I've already written about the idea that comments are a code smell - because I should just be able to read your code and understand what it's doing. I'm also a big fan of more descriptive code - meaning, variable/object/function names that (usually) avoid abbreviations and try to state, clearly, what they are representing.
So I've always been a little bothered by (what I perceive to be) a lack of namespacing in JS. This may be because, as an "older" dev, I have experience in numerous languages - and many other languages make heavy use of namespacing. It may be simply because I'm verbose as hell. But for whatever reason, I often look at JS code and feel - more than in other languages - the naming conventions are too simplified. Sometimes, to the point of confusion.
Granted, I don't think that JS devs have some genetic condition that precludes them from understanding/using namespacing. Rather, I believe there are some factors unique to the JS community that have fostered this "condition". Allow me to explain...
Class Warfare
Going all the way back to my second post on this site, it was all about JS's "class warfare" (https://dev.to/bytebodger/the-class-boogeyman-in-javascript-2949). Since then, I've written multiple pieces that touch on this aspect of JS dogma. As a React dev, the "Classes R Bad" mantra hits particularly close to home.
I won't bore you by trying to regurgitate all that content here. You can look through my past articles if you're at all interested. But there is one beneficial aspect of classes that I never even bothered to outline before: They create a natural opportunity for namespacing that, when used properly, can make your code much clearer.
For example, in a previous article I outlined a runtime validation library that I wrote for myself to ensure the integrity of all function inputs. Since this library needs to do many different type of validations, it's logical that it contains many different functions. But the library itself is part of one unifying class.
Why did I choose to leverage an evil, filthy, unconscionable class
?? Well... because I now have code that looks like this:
import { allow } from '../../classes/allow';
const populateLikelyDuplicates = (pairs = [[]]) => {
allow.anArrayOfArrays(pairs);
// function logic here...
}
const updateSelectedPlaylist = (event = eventModel) => {
allow.anInstanceOf(event, eventModel);
// function logic here...
}
The validation library lives in a utility class. The instance of the class is stored in allow
. And thus, when I import the utility class and use it in my code, it reads as natural language. And this clarity is made far easier by the natural namespacing that's afforded by the class.
To be absolutely clear, I realize that you don't need to use classes to get this namespacing. You could just save all of those validation functions under a single plain-ol' object. Then you could name the object allow
- and you'd still have all the same benefits. But I've come to believe that JS devs' aversion to classes has made this approach fairly rare.
Don't believe me? Well, think about how you would "normally" consume a validation library like mine. The library would be encapsulated within an NPM package. Then, when you wanted to use any of the particular validations, you would need to import them one-by-one. So your code would probably look something closer to this:
import { arrayOfArrays, instanceOf } from 'function-input-validation';
const populateLikelyDuplicates = (pairs = [[]]) => {
arrayOfArrays(pairs);
// function logic here...
}
const updateSelectedPlaylist = (event = eventModel) => {
instanceOf(event, eventModel);
// function logic here...
}
Now, I'm not going to try to tell you that the code above is, in any way, "unreadable". But I believe strongly that, simply by removing that namespace value of allow
, we've made the code a bit less self-explanatory.
This will also lead to a potentially burdensome import
statement if we keep adding different validations to the component. But when you use a class, you don't need to bother with individually importing each of the validations. You just import allow
, and you're done.
Of course, I could "fix" the lack of natural-language namespacing by making the names of each validation function more explicit, like this:
import { allowAnArrayOfArrays, allowAnInstanceOf } from 'function-input-validation';
const populateLikelyDuplicates = (pairs = [[]]) => {
allowAnArrayOfArrays(pairs);
// function logic here...
}
const updateSelectedPlaylist = (event = eventModel) => {
allowAnInstanceOf(event, eventModel);
// function logic here...
}
But this runs into a problem, because my validation library is designed to be chained. So, with my original allow
library, I can do this:
import { allow } from 'function-input-validation';
const doSomething = (userId = 0, name = '', isActive = false) => {
allow.anInteger(userId, 1).aString(name, 1).aBoolean(isActive);
// function logic here...
}
But if we want to strip the leading allow.
, the new code looks like this:
import { allowAnInteger, allowAString, allowABoolean } from 'function-input-validation';
const doSomething = (userId = 0, name = '', isActive = false) => {
allowAnInteger(userId, 1).allowAString(name, 1).allowABoolean(isActive);
// function logic here...
}
Umm... yuk.
IMHO, couching everything in that descriptive allow
wrapper makes the code more readable - while at the same time keeping us from having to repeat allow
in every function name. Yet it feels to me like I rarely see this in JS environments.
Reckless Destructuring
I think most JS devs would define destructing as a "net good". And I'm certainly in that crowd. But some JS devs have come to embrace destructuring to the point that they believe ALL THE THINGS!!! should be destructured. I'm definitely not in that crowd.
Lemme be honest. I've gone back-and-forth on this one over the last several years. When destructuring was first introduced, I looked at it and thought, "Yeah... that's nice. Not sure how much I'll really use it. But it's nice." Then, about 18 months ago, I went through a phase where I was hellbent on destructuring ALL THE THINGS!!! Now... I've cooled wayyyy off on the destructuring.
Why do I do less destructuring nowadays?? Well, destructuring effectively robs a variable of its context. When you're reading code, it's crucial to be able to quickly understand a variable's context.
Think of it like this: Let's say that your name is Joe. If I tell someone that I was just robbed by Joe, and that's all the information I can provide, I might as well just yell my complaint into the wind. Because "Joe" isn't even close to the level of info the authorities need to investigate the crime and make an arrest. If I said I was robbed by Joe Schmidt, who lives at 101 Main Street in Palookaville, Florida, and his SSN is 999-99-9999." Well... that's more than enough info to get the cops into a full-fledged investigation. When you destructure your objects, it's like you're limiting all of your identifiers to "Joe".
To be clear, I'm not trying to claim that destructing is somehow a bad thing. I use destructuring all the time. You should, too. It can make convoluted code read much clearer. The classic use-case for destructing is when you have a deeply-nested object, something like this:
const salesTax = price * tax.rates.states.florida.counties.duval.cities.jacksonville;
The snippet above can be particularly onerous if you need to refer to the Jacksonville sales tax rate multiple times. So it's clearly easier (and "cleaner") to destructure that value into a simple variable name.
But there are many other scenarios where I find destructuring to be a detriment to the code's clarity. That's because, when you destucture an object, you're stripping the nested values of context.
For example, in the snippet shown above, let's imagine that we just destructure that value down to jacksonville
. Then, at some point further down in the function, you're reading the code and the first instinct is to think, "Jacksonville what?"
Here's a tangible example that I run into all the time as a React dev:
const doSomething = (userId = 0) => {
if (userId === props.userId) {
// do some logic here
}
}
See what's happening here? In the "legacy" way of handling React components, you had an object of values that may-or-may-not have been supplied to the component. Those values always lived in a props
object. And quite frankly, I found that props
nomenclature to be incredibly useful when reading the code.
In the example above, there's some helper function inside the component, that expects a userId
. But there's also a userId
that was passed into the component. And I frequently find myself having to compare some temp value against the original value that was supplied to the component.
In these cases, I truly enjoy having the props.
moniker in front of all the component's passed-in values. It allows me to easily sort out what is a temp value in memory, versus what was supplied to the component from its caller. If you're hellbent on destucturing ALL THE THINGS!!!, it can quickly become confusing when you're trying to read through the code.
Top comments (23)
To provide a contrary perspective, why does
allow
need to be a class? Classes can be useful in situations where they might need to be instantiated with different parameters, but your example makes it looks like all the methods being consumed are static, and the class isn't being instantiated at all.Surely this is the perfect use case for a module, and the principle of least power suggests that we shouldn't reach for a class when a module will do.
If the only issue is namespacing, that can easily be solved by using the
import * as allow from '<path>'
syntax, and it also provides the flexibility to only import certain functions if the consumer of the module wishes to do so.It was created as a class to facilitate chaining by constantly returning
this
. That being said, it's not the only way to accomplish that goal.I don't personally disagree with your point - but I don't necessarily see it as a "contrary perspective" either. IMHO, it's more of an alternate perspective. Maybe that's splitting hairs. But the point is that I can't really see how a class, in this scenario, is wrong, but I can absolutely accept that maybe it's not preferred.
Consequently, I literally just started exploring making this an NPM package. (Like... last night.) And I'm thinking that it doesn't really need to be a class - but maybe a plain-ol' object.
Hmm, a fluent interface does lend itself to OO and OO lends itself to classes, but could be implemented with a plain object too. Not saying this would necessarily be perfect for your use case, but it could be done.
I'm actually implementing it now with a module design pattern. So basically, it's a function... that looks a heckuva lot like a class.
github.com/bytebodger/allow/blob/m...
Wait, are you saying that the following is not clean and pure and beautiful? ๐๐
Never understood why some prefer the above to a classic
user.city.id
.Good post!
Bingo! I didn't even get into all of the hoops that people sometimes jump through to destructure one choice little bit out of an object, but this is a perfect example.
I always look at JS from the perspective of a Lua developer, since that's my main language and they are both very similar in many aspects.
The lack of namespacing has always seemed a bit weird to me. The Lua equivalent to JS objects, tables, is used for namespacing almost everywhere, with most library code looking like this
yet in javascript nobody does this, despite it being possible in just the same way (except for the syntax, of course).
For small sections of code, what JS was originally built for, this may be enough, but for larger codebases this just seems like just a valuable tool that I wonder just how the whole JS community never started adopting this. Then again, maybe it's just a leftover from those times when JS really was just used for simple interactivity on plain hand-written HTML pages.
I appreciate that confirmation! As I was writing the article, I couldn't help but wonder whether this lack of namespacing was just in my head. But yeah - I don't understand why the approach you've shown above is almost never taken in JS.
In fact, as I outlined in the section about destructuring, it honestly feels to me like JS devs are ruthlessly going in the other direction - purposely stripping variables of all context. I'm not exaggerating when I say that I've read JS code where I had to repeatedly refer back to the top of the function to understand the values that particular variables were supposed to hold - because all of those variables had been destructured out of their original object.
Tree shaking
His approach can still be tree shaken.
I don't follow...
Sorry for not directly writing back, was busy these days ๐
So tree shaking can be performed by e.g. webpack and this is a strategy to minimize your bundled code
If you only import something like
import { myFunctionA } from 'moduleX';
, onlymyFunctionA
will be bundled in the output code but e.g.myFunctionB
would not.If you now have
my.functionA
andmy.functionB
and you useimport { my } from 'moduleX';
you are forced to bundle both functions into output code, also if you only needfunctionA
This hugely increases your bundled output, cause you are forced to import everything from
allow
in your example.I may be wrong and @SeanAllinNewell may have another idea how you can still benefit from tree shaking. But currently that is what I understand under the term tree shaking and plugins can show the imported
kb
of an import statement in e.g. VSCode.OK. I know what tree-shaking is. But I didn't understand what exactly you were getting at. A few thoughts on this:
I understand the need to import components separately. But I think it can sometimes get kinda silly when you're talking about functions. For example, my
allow
library is used throughout my entire app. Most of the functions in that library will be used somewhere in the app. So it's needlessly specific to force the coder to import each one of those functions independently.The
allow
library has 169 LoC that encompass 19 functions. The raw file is 6.59 KB. I haven't even tried to look up how small this becomes once it's minified. But the point is that it will be tiny.But you bring up a good point about bundle size. Specifically, I've found that, too often, JS devs make choices that undermine the readability (and thus, the maintainability) of their own code in the name of bundle size. So... a few more thoughts on that thought:
If you have absolute control over your app and how it's rendered, then I can kinda understand the desire to minimize bundle size. But this is rarely the case in corporate apps. Typically, once your app is deployed, it's put in a wrapper that has a ton of ads, trackers, analytics, images, video, and other such detritus. For example, if you go to espn.com with no ad blockers, their homepage currently uses up 6.5 mega bytes. And this is not unique in corporate environments. I just can't get too worked up about 5KB here-or-there in an app's bundle when the sites we frequent nowadays are often multiple megabytes.
IMHO, tweaking bundle size is usually a micro-optimization. I understand that there are some situations with some apps where bundle size can be critical. I also understand that, for most apps, the functional difference between a 100k bundle and a 105k bundle is... nothing. And even if you want to take the stance that all those things add up, well then... so what? Because, again, the functional difference between a 100k bundle and a 300k bundle is typically... nothing.
Although this article may feel a bit esoteric to most, I wrote it because this is another of those little issues that gnaws at me because it affects the readability of code. And readability isn't a "nice to have". More readable code is more maintainable code. If devs are consciously sacrificing readability for bundle size, then for most apps in most environments, they're making the wrong choice.
Totally understand your POV
Just wanted to say this so newbies that read your article don't follow it blindly without thinking about the consequences ๐
Good point!
You love classes!
<EvilLaughter>Muahahaha!</EvilLaughter>
useMuhHaha()
Huh???
At this point one could argue your comment section tends to get full of unwanted side effects. But at least you can defend your opinion by saying the naysayers have no class.
This is the optimal proportion of cheese, clever, and dad joke. ๐
There are no guarantees of effectual code regardless of destructuring. Importing anything can have unwanted side effects!!