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 error
or alert
), as well as one of many different flags (isForm
, isDismissable
, isSecondLevel
...), and functions (onClose
, onConfirm
, onSubmit
, 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.
Top comments (27)
You make a good point against premature abstraction and against overloading components with too many options. The latter can be avoided by composition rather than repetition. While I don't think, WET should seriously be a best practice, I know many situations where some repetition is fine and where I would rather focus on getting things done than winning a clean-coding award in the first place, even more so as you might not need it (YAGNI) later. But often there is a time to refactor and make the code cleaner, before it gets messy.
Abstraction is helpful when done right!
Avoid premature abstraction, but abstract when repetition patterns become clear and you find yourself writing the same come again for the third time ;-)
my point exactly :)
Rule of 3
Do any situations come to mind that could illustrate composition over DRY or WET?
You mentioned a modal component that is used to display various types of notifications. Instead of adding more parameters to one common component, you could have different components that use this one combined with other components / interfaces like buttons, groups of buttons, style wrappers etc.
I found Alexi Taylor's article: React Component Patterns worth reading. It explains composition patterns using practical examples in ReactJS.
Right, I think "modular" is the key here. Can we set up the component in such a way, that we can expand on it if needed, without breaking anything else. Atomic design could be of great benefit here.
Example:
._base_popuptitle{
// base styles
}
._variant1_popuptitle{
@extends ._base_popuptitle;
}
You could extend it any way needed. Be thoughtfull though, to not be too specific, but stay somewhat generic.
This can be applied then for each contentsection you have or add later, without breaking stuff.
It's something you need to get the hang off, but it can help greatly.
A function (or... "method", if you will) should
Of course, those two simple guidelines are both somewhat subjective. If you set two variables in your function, is it no longer doing "one thing"? (Most certainly not - but you get my point.) And whether the function is doing its thing "well" is always debatable.
But any time you start abstracting the heck outta stuff, you're almost never doing it well. And we often reach for abstraction when we're trying to make a single function do many different things.
So if you have one block of code that's become a true utility function - the sorta thing you write once and use all over the place, then yeah, of course you should centralize that code. But when you start trying to turn that utility into a Swiss Army Knife - the kinda "utility" that will serve every single alert, that meets every single use case, in every single scenario - then you're probably creating an abomination.
I think that thought is a bit misleading: functions serve as an interface between two levels of abstraction and the "do one thing" rule should apply mostly on the level of the caller, not the internals of the function.
A function (or any other type of subroutine, really) might do lots of things internally that present present as a single atomic action to the outside.
Also, there's one statement that can be generalized a bit:
See? Much more DRY now 😁
Software Development By Slogan usually does.
Don't Repeat Yourself is a slogan that refers to:
When phrased that way, "duplication" has a lot more nuance.
The Wrong Abstraction
YouTube
I really like that phrasing. Thanks for sharing that article :D
I'll also use the chance to throw in a link to one of my favourite programming books: 97 things every programmer should know, thing 7: beware the share
That particular piece is by Udi Dahan, Founder & CEO, Particular Software. The talks given by his employees can be interesting as well.
In particular finding your service boundaries comes to the conclusion that startups have difficulties identifying boundaries because their business rules aren't stable - which means that μservices are not a good fit in that situation.
And it's in bounded contexts (which is what microservices are all about) where naive application of DRY (or overzealous pursuit of reuse) can be counterproductive. Note that both the sales context and the support context have their own version of
Customer
andProduct
.While sharing the same name these entities will likely have a very different shape inside each context as they support very different roles. Unifying them into one single entity would only increase coupling, undermining the autonomy of each context. Instead representations of
Customer
andProduct
that are shared between contexts are minimal - just enough information to correlate the support version with the sales version.I agree with the sentiment that devs tend to over-abstract, especially newer devs. The more experienced you get and the more you understand your domain, the easier it is to know what abstractions you need and will be useful.
To your point about the single modal function, we have a rule of thumb. If you find yourself "configuring a function" (passing string parameters like "alert" is a good indicator) then you're abstracting wrong. It's not necessarily that it shouldn't be abstracted, but it's much better to have an alert modal as a separate function from a form modal function, perhaps with some shared underlying function that wires up a generic modal.
I think good advice for newer devs is to lean more toward WET but consult with more senior devs if you think there's an opportunity for abstraction. You will learn a lot about the domain, the architecture, and how they think during those discussions.
I agree that anticipating factoring too much can lead to bloat. I think some of us also just have bias for the puzzle part of refactoring things 😝
That being said, refactoring (thus DRYing), is better done early. DRYing a project once it's done and/or you forget about everything, will lead to debugging hell. There a reason why people have gone mad on DRY code. But it's all down to common sense and the fact DRY is a tool, not a goal. Clean and readable code is.
In FP, I really don't mind having a few small functions that are only slightly different and easy to duplicate if you need a new one. A simple cmd+D might be enough to update all of them. In OOP, I don't mind having the same method on multiple classes, because I might want to explicitely change each of them, since they refer to different objects.
Really, if you focus on your code being elegant and readable, DRY will be one of your best tool, almost without thinking about it. Over DRYing should pretty easy to avoid—and quite rare, since it is a pretty painful task in the first place.
In the case you described, I fully agree. The concept of YAGNI should be always on top of developers' minds whenever they write code, but going WET by default doesn't solve bad thinking or premature abstractions.
+1
My thinking is that if we can do stupid things, we're going to. So we need systems in place that help us avoid that. If my norm is WET, then for the third usage I have to sit down and think what is actually shared - I have a better chance of avoiding premature abstractions.
I really don't think that PopUp you are describing was DRY at all... rather a huge mess with everything in one single place. ( I also worked in a company where we had a very similar pop up. very very similar. hundreds if not 1000+ LOC with incredible cyclomatic complexity due to all those flags and conditions. working and debugging that was pure hell.
But again, that was far away from being DRY, and anything but SOLID.
I absolutely agree that premature abstraction is wrong. Most of the time YAGNI, so why bother. but as soon, as you said, you realize you wrote more than TWICE, you should do something about that. which is NOT a God Component like the one you described. rather a more functional approach, with tiny components or methods that can be composed, chained together, decorated and adapted, and built according to your need using different design patterns.
Great article!
I especially prefer "WET" code in unit tests, where constantly trying to cut lines by consolidating similar tests using "helpers" can result in giant methods with many flags.
The original goal was to make maintaining the unit tests easy, but now the helpers are so complex so that adding new test paths takes hours.
Personally i believe in DRY code and i try as much as possible to stick to it, it comes with a few hitches because the more complex the problem or the solution is the harder it is to get the abstraction layer right, but in the end if you spend some time thinking about the problem or your solution to it, you will find recursive patterns that can help you get your abstraction right. WET code gives you more work than you should have to deal with, imagine having to update over 50 lines of the same code in like 5 components?? At the ending of the day, even if you find dry code quite complicated at first, ask the guys or yourself about the problem, then loom more closely at the solution.
" 50 lines of the same code in like 5 components"
if it appears 5 times, then you should look really hard into abstracting it. But if only appears twice, it may be too early to tell.
I agree it's important to write something at least twice or three times before trying to abstract or generalize it. Also, if the abstraction makes the code less clear, don't do it. YAGNI the abstraction if you only write the thing a few times.