One of the very first things you learn as a developer is that for code to be “good”, it needs to be DRY. It’s almost as though DRY code is some kind of badge of honor - the more you do it, the better you are as a developer. After all, how can code be clean if it’s written twice? And you know it’s always better to remove lines of code than add them. Also, what are you going to do when you need to change it? Go in and - gasp - make changes in two places??? It’s become such second nature, I’ve seen developers wrap helper functions in helper functions just so that the same sequence of functions isn’t written twice.
This fixation on DRYness is doing us a disservice. It’s a rule of thumb that’s easy to follow, but prevents us from deeply reasoning about our code and what makes it complex. More than that, it comes with a very high and often overlooked price tag - premature abstraction. We’re so hell-bent on DRYing up the code, that we do it too soon - before we know for sure what parts of our code are truly shared. We end up with bloated abstractions, full of flags and conditions that are piled on as we scramble to address every use-case while still avoiding repetition.
I once worked at a company that had a single popup component in the entire system. This could have been fine, if only the system didn’t have so many popups. We had info popups, alert popups, confirmation and error popups, of course. But we also had form popups, popups with multiple actions, popups that navigated away from the underlying page and popups that open on top of other popups. Dubious user experience aside, the developer experience was also suboptimal, since all those popups were ultimately created by a single component. This generic “modal” component could receive a type (such as
alert), as well as one of many different flags (
isSecondLevel...), and functions (
onSave...). Then the component itself had conditional statements for each of these parameters, to create an almost infinite number of combinations (and bugs). It was a monstrosity.
And you know what else? None of the existing team members, all veterans who played a significant role in building the system, thought there was anything wrong with it. It was DRY! We had a single popup component and were reusing it all over the system! So what if it was so complex that I, the newcomer, could make no sense of it. It made sense to them, because they had each come in when the component was smaller and more readable, then made incremental changes that were easy for them to reason about. But by the time I got there the thing was so convoluted it was impossible to understand or maintain.
This is how DRYness obscures premature abstraction. The first developer thinks to themselves “these two things are similar, I’ll just abstract them into one function”. The next developer comes along, sees that abstraction, and sees that it has most of the functionality she needs. She doesn’t want to duplicate code, so she decides to reuse the abstraction, and just add a condition to it. The next few people who consider reusing the abstraction do the same. No one wants to duplicate code because we’ve all been taught that DRY is king, and they each think they’re making a reasonable change. Because they know and understand the code, they assume the code itself is understandable, and that their change adds little complexity. But eventually the deluge of conditions and flags make the code unmanageable, and it goes the way of all bad abstractions - to be rewritten from scratch.
Around the same time this popup escapade was happening, I ran into a friend who was also a very experienced developer. I told him how hard it was for me to get into this new codebase and he said: “I don’t believe in DRY code, I believe in WET code”. WET, as in “write everything twice” (acronyms are fun!)
The reasoning behind WET code is this: writing things twice doesn’t, in fact, have such a high price tag associated with it. Duplicating some parts of my code has a relatively small impact on package size. And if I need to change them? Well, I could just do that twice. So until I have three usages for a piece of code - there’s really no pressing need to abstract it.
At the same time, before I have three usages of code, I would have a really hard time knowing what exactly to extract - what truly is shared, and what just looks shared but is in fact a special case relevant only to two instances. Having three instances of similar code allows us to start identifying patterns - what piece of code might truly have many uses in our codebase, what code belongs together, and what just works together but should probably be separate.
Imagine if those popups had been written using WET code: the first developer who needed a popup would just… create a popup for their usecase. The next one would do the same. The third popup would require some thinking and re-design: say the system now has a confirmation popup and an error popup, and a form popup needs to be added - what parts of those three are shared and might benefit from abstraction? The styles? The closing function?
You’ll notice a few things about this approach:
- It actually takes more time and effort than just instinctively DRYing any similar code into a shared abstraction
- When you put some thought into your abstractions like this - you may very well find that there’s less shared code than you think
- At the end of this process, the team might not have a shared component, but they will have some shared functionality. The goal isn’t to share as much as possible - it’s to share as much as is actually needed.
Writing WET is more difficult than writing DRY, but it absolutely pays off, especially if you want your codebase to last. It guards you against premature abstractions. It makes it easier to see which functionality is actually shared and should be abstracted together, and which functionality is just adjacent and might need to be abstracted separately, to avoid coupling. It also results in smaller abstractions that are easier to reason about and maintain.
It’s the way we should all be coding.