This article has been on my to do list for quite some time now. But it seems that only today I found the energy and time to take it from idea to implementation. Coincidence or not, I'm at the same coffee shop I posted my first article, a while ago. Must be that they put something in the drinks they serve...
So the good, old advice of following best practices, right? We hear about them all the time. We've somehow made acronyms such as DRY or KISS defaults in our technical conversations. We follow the concepts religiously and if, by chance, desire or lack of knowledge, someone strays away from them, we make sure we rain a shit storm of criticism upon them. We're caught in this dogma and we refuse to look away.
For sure I don't want to imply that principles such as DRY are bad. Definitely not. I just think context matters. A lot. In regards to DRY specifically, this brings in the following logical conclusion: I actually am the guy that will sometimes advise in favour of duplication rather than abstraction.
Yes, you read that right. Duplicating code (a.k.a copy + paste) can be a good thing. Namely when the abstraction that would replace repetitive portions of your codebase is a pain to understand.
How programming time is divided
When I tell people what I do for a living, they all imagine I must be some kind of weirdo who keeps slapping the keyboard ten hours a day or more.
Although, I'm not sure I'm not a little weird, I'm pretty sure I don't code for ten hours straight. The truth is that we, as programmers, spend far more time reading code than writing code. I'm not sure if you ever tracked this in one form or another, but research and Robert C. Martin claim to have some sort of ratio on this. And I, for one, find the difference to be staggering. It turns out that for every hour we code, we spend other ten hours reading code (ours or someone else's).
This is extremely important. The effort we put in a day of work goes mostly towards reading code. Of course, reading is not enough. We need to also understand. Which means we have to do our best to produce code that is clear, concise and easy to read. For everyone's benefit, including ours in the long term. Keep this in mind as we'll get back to this idea later on.
A note on DRY
For those who are not familiar with what DRY means, it basically stands for don't repeat yourself. This programming principle or best practice if you'd like, is advocating for creating abstractions on top of every repetitive portion of your codebase.
DRY has tons of advantages. For one, abstractions mean that, if changes will be required in the future, you will have to deal with them in one single place - the abstraction itself.
Consuming another module's functionality, someone else's API, etc. you're only concerned with how the interface (or abstraction) looks like. You don't care about the underlying implementation. Thus, software design patterns such as the facade pattern allow for easy refactoring of the implementation, without hindering the abstraction used by others.
So abstractions are good and DRY totally makes sense. Then why am I still insisting on repeating code in some scenarios?
Well, it's because...
The cost of abstractions
Every abstraction comes at a cost. A cost that may not be immediately visible. But it reveals itself in time.
Abstractions carry an extra layer of knowledge. Knowledge that you may not necessarily understand why it exists in the first place (especially if you didn't write it). And every new piece of information places cognitive load on your brain. Which, in turn, increases the time spent on reading that piece of code.
Down the rabbit hole
The problem with DRY and religious following of this principle is not visible in small projects. But in mid-sized and large ones.
In those kind of projects, it's very rare that you will write only one abstraction per duplication. This is because, as the project evolves and new requirements come in, old code must always adjust.
Imagine this. You enter a new project and scan the codebase for the first time. After getting used to what's there and learning your way around, you start implementing features, changing old ones, etc. You get to touch existing code and existing abstractions. You didn't write those, but they're there. And probably there's a very good reason for that.
As Sandi Metz said:
Existing code exerts a powerful influence. Its very presence argues that it is both correct and necessary.
Which makes you not wanting to touch it.
Now the new feature that barely came in could definitely make use of that nice abstraction that you have there. But, as it turns out, the abstraction needs a bit of tweaking. It wasn't thought for this particular use case. If only you could change it a bit... Or maybe write a new abstraction on top of it that could encapsulate some other repetitive logic as well? Yeah, that seems to be the answer, right? That's what DRY says anyway...
It's easy to see how we can drive this madness to absolute extremes. Extremes where everything is encapsulated in abstractions. And those, in turn, have their own abstractions on top. And so on and so forth...
In these cases, the abstraction lost its value. Its existence was determined by dogmas we follow blindly. And this makes the abstraction a wrongful one. It exists just because we can.
As mentioned, abstractions have a cognitive cost associated with them. If anything and alongside the added benefits, that cost almost always hinders and increases the time we need to understand it (remember we're spending more time reading code than writing it). But even worse than a good abstraction, is a wrongful one.
Because wrongful abstractions not only kick you in the nuts, they also laugh while doing it.
To DRY or not to DRY
So when do you encapsulate repetitive code and when do you not?
The answer is simple in itself. It's just hard to get it right from a practical point of view. But this does come with experience as well.
Always abstract if the cost of abstraction does not surpass the cost of duplicating code.
That is, if the abstraction you wrote requires someone new on the project to spend hours understanding it, then you're probably doing something wrong. Don't abstract just because you can. Predict whether code duplication will happen again on that particular portion or not and decide accordingly.
Sometimes, repeating code might hinder much less than following a tree of nested calls to different methods and keeping track of passed parameters, possible side effects, etc.
Closing thought - in defence of myself
Hopefully this article does not scream "To hell with DRY and other shit!". I absolutely think that is a very good programming principle. But I also urge you to not follow it blindly. Put everything you learned in context and always question the validity of your ideas and actions. This is the only sane way towards becoming a better professional.
Looking forward to your comments. Remember to smile often and always question best practices.
PS: I couldn't forget about the dreaded CACTUS EMOJI -> 🌵.