DEV Community

Cover image for We Should All Be Writing WET Code
NettaB
NettaB

Posted on

We Should All Be Writing WET Code

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:

  1. It actually takes more time and effort than just instinctively DRYing any similar code into a shared abstraction
  2. When you put some thought into your abstractions like this - you may very well find that there’s less shared code than you think
  3. 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)

Collapse
 
ingosteinke profile image
Ingo Steinke

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 ;-)

Collapse
 
nettab profile image
NettaB

my point exactly :)

Collapse
 
msucorey profile image
Corey Wofford

Rule of 3

Collapse
 
voboda profile image
voboda

Do any situations come to mind that could illustrate composition over DRY or WET?

Collapse
 
ingosteinke profile image
Ingo Steinke

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.

Thread Thread
 
nieuwepixels profile image
Nieuwe Pixels

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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

A function (or... "method", if you will) should

Do one thing, and do it well.

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.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

If you set two variables in your function, is it no longer doing "one thing"?

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:

But any time you start [insert almost any best practice here] the heck outta stuff, you're almost never doing it well.

See? Much more DRY now 😁

Collapse
 
peerreynders profile image
peerreynders

This fixation on DRYness is doing us a disservice.

Software Development By Slogan usually does.

Don't Repeat Yourself is a slogan that refers to:

"every piece of knowledge must have a single, unambiguous, authoritative representation within a system."

When phrased that way, "duplication" has a lot more nuance.

The Wrong Abstraction
YouTube

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Development by Slogan

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

Collapse
 
peerreynders profile image
peerreynders

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 and Product.

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 and Product that are shared between contexts are minimal - just enough information to correlate the support version with the sales version.

Collapse
 
jdforsythe profile image
Jeremy Forsythe

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.

Collapse
 
etienneburdet profile image
Etienne Burdet

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.

Collapse
 
eavichay profile image
Avichay Eyal

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

Collapse
 
nettab profile image
NettaB

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.

Collapse
 
dvddpl profile image
Davide de Paolis

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.

Collapse
 
7tonshark profile image
Elliot Nelson

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.

Collapse
 
kalashin1 profile image
Kinanee Samson

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.

Collapse
 
vlaem profile image
Alvaro

" 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.

Collapse
 
sswam profile image
Sam Watkins • Edited

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.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

The thing with DRY code is, you need to look at your abstractions from further away than just the code level.

If a chunk of code does a very well defined thing, something that's a reasonable action within the problem domain? Then it should be abstracted into some form of construct (be that a class, function, whatever). Think of methods like cancelling an order; this will likely happen at more than one place and is a distinct process even if you set aside the code.

Something like a popup is a bit tricky; when the code is well written, there's nothing inherently wrong with a single modal class that takes a bunch of callbacks and has a bunch of different variants. In OOP these would normally be separated out into subclasses, but in languages like C it's also not that strange to simply use flags for such behaviour.

The problem really starts when the code structure is bad and the class turns into an entangled mess.

Generally speaking, DRY code is better than WET code (which I prefer to read as "We Enjoy Typing"), because copy-paste errors are a very real threat and keeping two different sections of code in sync is another pitfall that may introduce bugs when one of these sections is forgotten.

Another problem that often gets overlooked with DRY code is that some times two sections of code just happen to follow the same logic, but don't really relate to each other in any way. Abstracting this common code out into a separate construct might turn out to be a terrible mistake if one of these sections ends up changing while the other doesn't: now you have to roll the abstraction back or bloat your abstraction.

Always be aware of which code is shared incidentally and which code share reflects actual shared domain logic.

Collapse
 
moubi profile image
Miroslav Nikolov

Dan Abramov has a blog post about it too: The WET Codebase

Collapse
 
ericbeland profile image
HereC

I think there's a few problems with WET. Firstly, when you copy, you may not realize whether you're copying the first, or the copy of a copy of a copy because the names will all be different. So advocating for a little wet is advocating for a viral bloat-iness. The find-all-the methods problem is compounded because these code bases naturally are bigger than their DRY counterparts. I've personally been bit by this. As a newbie to a particular codebase, I implemented the exact feature modification asked for by the Product Owner with tests, etc. Only to find out it "didn't work" for the customer, which was because their path was triggering the "wrote it twice" copy of the implementation in the giant, bloated file with duplicate implementations everywhere. Hard pass on WET from me.

Collapse
 
eljayadobe profile image
Eljay-Adobe • Edited

I've heard the acronym in this context:

DRY • Don't Repeat Yourself

for code, but...

WET • Write Expressive Tests

for unit tests.

Meaning that unit tests should be independent of one another, self-contained, and not entangled with other tests, or global state, or unit test order dependent (i.e., the finishing state of one unit test is presupposed as the starting state of a different unit test, so they are implicitly conjoined).

Some unit test frameworks provide a convenient setup / teardown used for all the bundled unit tests... even that is suspect (but not slavishly disallowed) for WET, because the tests are now entangled with the setup and teardown, making them harder to maintain.

As far as "write everything twice"...

My tipping point for refactoring something used to be "third times the charm". If I had the same logic in three or more places in the code, I'd take some time to refactor that to a single helper routine.

I changed my mind later to more faithful DRY, when the same logic in two different parts of the code (not my code, but the same sentiment) had the same bug, and another developer fixed the bug in one of the two duplicate parts of the code.

The unfixed bug in the other code path probably cost the company upwards of ten million dollars.

An expensive lesson on someone else's dime.

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt