2023-06-09The Goldilocks Zone of Indirection
There's a certain trajectory you follow as a developer. Most of us start out as tiny programmer larvae, blindly fumbling our way through trial and error until things seem to work. We then pupate into overly clever Rockstar Ninjas who try to solve everything with ternary operators and arrogance. The Rockstar Ninja's code usually looks like someone lost a bet with Larry Wall and had to drink a whole-ass bottle of Perl 5.0 -- but that doesn't matter: the Ninja is there to kick ass and ship features, unlike you sorry lot. That means things like left-pad dependencies and rolling your own crypto.
After a few awkward years of Rockstardom, the final stage of a programmer's evolution is when we molt into shimmering purple Senior Developers. Now, the word "senior" here is interesting. It's Latin. For men, we go from Puer (boy) to Vir (man) and then on to Senex (old man). So seniority implies the great wisdom of a life well lived. You probably recognize all of these words from modern English: a puerile joke is immature. If you're virile, you're manly and forceful. And if you're senile, you're uhhh wait.
Go on
No, I don't like where that simile is taking me so let's instead talk about INDIRECTION! It's like catnip to the Rockstar Ninja. If you're not familiar with the concept, it has to do with how immediate your code is. Where does it live, basically. The concept can be applied almost everywhere in code but a nice and simple way to illustrate it is with pre-processed CSS. Let's change the color of our links:
a {
color: #f00;
}
That little foo will turn every link on the page intensely red. That's not always what you want, though: maybe most links on the page should be subdued but the ones on your profile page should be bright?
a {
color: #c00;
}
.profile {
a {
color: #f00;
}
}
We've added a touch of specificity which isn't exactly indirection, but bear with me. The next step is to make sure every shade of red on your site is the same, so we don't have to redefine it every time. We're using Sass, so let's reach for a variable:
$color-red: #f00;
$color-red-muted: #c00;
a {
color: $color-red-muted;
}
.profile {
a {
color: $color-red;
}
}
There's a little aphorism called the Fundamental theorem of software engineering stating "every problem can be solved by introducing an extra level of indirection" and while often meant to be funny, it's actually kind of true! We've added a layer of indirection that allowed us to get the same shade of red everywhere, and instantly change it across our site at will. Quite the win! But we can go even further:
$color-red: #f00;
$color-red-muted: #c00;
$color-link: $color-red-muted;
$color-profile-link: $color-red;
a {
color: $color-link;
}
.profile {
a {
color: $color-profile-link;
}
}
Another layer on our delicious and moist cake of indirection: now we'll always have a "link color" we can use, and suddenly we can change an entire category of links from a single place in our code, as long as we remember to use $color-link
for their color. Was such power even meant for mankind?
Indirection is the best, let's add another layer
Okay, time to add a "muting offset" variable and then make a little function that lets us apply that to any color. The function will tone our color down and enable us to define a new "muted" variant. That should be a good way to generalize the behavior, allowing us to do the same sort of operation on other colors and easily creating variants:
@function mute($color) {
$muting-offset: 10%;
@return darken($color, $muting-offset);
}
$color-red: #f00;
$color-blue: #00f;
$color-red-muted: mute($color-red);
$color-blue-muted: mute($color-blue);
$color-link: $color-red-muted;
$color-profile-link: $color-red;
$color-external-link: $color-blue-muted;
$color-external-profile-link: $color-blue;
a {
color: $color-link;
}
.external {
color: $color-external-link;
}
.profile {
a {
color: $color-profile-link;
}
.external {
color: $color-external-profile-link;
}
}
And it's easy to see how we can add even more. We could improve the function to define a bunch more variants for every color, and get things like "semitransparent" or "complementary" automatically set. That would get rid of a lot of repetitive definitions and get a predictable set of variants for every color, with ready-made usable CSS classes!
Oof
...but I think I've proven my point, and we'll end the experiment there. There are tradeoffs, you see. Every layer of indirection will incur a cost. Not only to your compilation performance (probably negligible in this case) but also to mental overhead, because getting into a taxi and shouting "follow that indirection!" really isn't anyone's idea of fun. Look:
You'll need to decide whether to add a new definition or re-use an existing one every time you add a component with a red color. Should it share the "link color" or should it get its own definition? By abstracting it into another layer, you've put yourself in a corner where every time you want to set a color on your site, you'll have to sit down and think, and possibly discuss it with teammates. Given your app it might be worthwhile, given how it's easier to change things as a group rather than individually, but these definitions will proliferate quickly.
When you want to look up the profile link color you're first going to have to find the color-profile-link
definition in a long and growing list it shares with such all-stars as color-aside-muted
and color-header-accent
. From there, find the actual color used.
The color you're looking for is probably defined in another section or stylesheet because, let's be realistic, few people have all their styles in a single document. So you'll have to look at the imports or do a text search in your code if you want to find it.
If you have a layer that obfuscates names like the muting offset function i proposed earlier, you can't even search for your variable name. You'll have to either know how it works (likely because you wrote it) or have up-to-date documentation at hand, or painstakingly read the code to see how it defines colors for you.
Compilation is already a layer of indirection, because while the generic a
is likely defined somewhere in a defaults
file, the .profile a
you're after probably lives somewhere in a component file. When inspecting the DOM and looking at the color, you get the compiled value of .profile a { color: #f00; }
so you frantically search your code for .profile a
. That gets you no results, because we're using Sass nesting. If you're lucky you have non-buggy sourcemaps that can guide you to the right file, but I wouldn't bet on it.
Doesn't sound so bad and CSS isn't even a real programming language you dweeb
With user input CSS is Turing complete but whatever: in this case the indirection will probably annoy rather than kneecap your developers. But you'd be surprised at how quickly this kind of setup becomes a liability rather than an asset. This might not be the biggest offender in your codebase. But imagine yourself trying to debug why a link color is wrong, and instead of quickly finding the offending selector and changing from #f00
to #c00
, you'll have to follow the trail of indirection layer by layer, and might also inadvertently end up changing a bunch of unrelated colors you didn't mean to change. CSS has gained serious notoriety as a footgun because of how the cascade works.
You see, the thing about indirection is that it works as a bug magnifier: before, you would have mucked up a single selector, now you get to muck up an entire section of your site! And it's easier to screw up when we have layers of indirection in place, because there are multiple mistakes you can make over several files and definitions, and keeping everything in your head is harder. This is how it works in all programming languages, it's not exclusive to CSS.
The Goldilocks Zone
I think there's a Goldilocks Zone of indirection. Adding layers can be really powerful: it allows you to re-use code, isolate concepts, and can unlock some pretty rad ideas. But the mental overhead of indirection scales exponentially. By the third or fourth level you're already losing track of what you were doing: I've had epic debugging sessions where I had to make like a pirate and draw a physical HERE BE DRAGONS style map and that's not somewhere you want to go unless you really have to. Remember YAGNI. As I've learned and grown during my career I've really come to really appreciate taking a direct and simple approach rather than going for flexibility and power.
Of course, in the end the boring and adult answer to "how much indirection should I use" is "it depends." Some parts call for more indirection, some parts call for less. But the arduous path from Rockstar Ninja to Senile Senior Developer has taught me that readability is far and away the most important property of any codebase. Be mindful of it whenever you feel like adding a layer of indirection. Stay in the Goldilocks Zone, so other humans can read what you've written. The computer doesn't care either way.