DEV Community 👩‍💻👨‍💻

Cover image for Stop Writing DRY Code
Dylan Anthony
Dylan Anthony

Posted on • Originally published at dylananthony.com

Stop Writing DRY Code

Cover photo created by Jazlyn Borkowski

"DRY" is an acronym introduced, seemingly universally, to software engineers early in their education. For example, when I searched "software engineering best practices," 4 of the top 5 results mentioned DRY. It stands for "Don't Repeat Yourself" and is one of the worst things you can teach a fledgling developer.

If you take nothing else away from this post, take this: DRY should never be a goal when writing code. It is not an indicator of code quality; it is, at best, a tool to be applied in some circumstances. Code that does not repeat itself is not inherently better than code that does, so "DRY" should never appear as a recommendation in your code reviews.

MOIST

Code is a bit like cake. A dry cake is brittle and likely crumble when you touch it. Likewise, DRY code—code that has no repetition—will resist extension and alteration. By combining two implementations into a single one, you bind them together, making it so you can't change one without changing both.

On the other hand, a wet cake will fall apart, not holding its structure. In software, WET usually means something to the effect of "write every time"—it's the opposite of DRY in that you constantly repeat yourself. If your code is WET, you can change one piece without breaking any others. However, two pieces of information that should be the same can easily fall out of sync.

The best cake, and code, is MOIST—"Maintain One Indisputable Source of Truth." Rather than being a hard and fast rule about writing code, it gives you a reason why to refactor code. MOIST gives you the best of both worlds by increasing rigidity to prevent bugs and keeping flexibility wherever possible. As with all things, balance is vital.

Let's look at an example where keeping your code MOIST is essential. We're writing a program that handles semantic versioning for projects. It has two commands: auto reads your commit history and generates a new version. The manual command takes a rule name from the user and bumps the version according to that rule. A first attempt might look like this:

fn auto(history: &[Commit]) {
    let mut version = Version::get_current();
    let mut major = false;
    let mut minor = false;
    let mut patch = false;

    for commit in history {
        if commit.message.contains("BREAKING CHANGE") {
            major = true;
            version.major += 1;
            version.minor = 0;
            version.patch = 0;
            break;
        } else if commit.message.contains("feat") && !minor {
            minor = true;
            version.minor += 1;
            version.patch = 0;
        } else if commit.message.contains("fix") && !patch && !minor {
            patch = true;
            version.patch += 1;
        }
    }
    write_version(version);
}

fn manual(rule: String) {
    let mut version = Version::get_current();
    if rule == "major" {
        version.major += 1;
        version.minor = 0;
        version.patch = 0;
    } else if rule == "minor" {
        version.minor += 1;
        version.patch = 0;
    } else if rule == "patch" {
        version.patch += 1;
    } else {
        panic!("Unknown rule: {}", rule);
    }
    write_version(version);
}
Enter fullscreen mode Exit fullscreen mode

Here we're maintaining two different implementations for applying a semantic rule to a semantic version—two distinct truths about the rules! Unfortunately, this separation could easily lead to disparate behaviors between the two commands, confusing users. So let's maintain only a single source of truth for how to bump that version number!

fn auto(history: &[Commit]) {
    let mut rule = "patch";

    for commit in history {
        if commit.message.contains("BREAKING CHANGE") {
            rule = "major";
            break;
        } else if commit.message.contains("feat") {
            rule = "minor";
        }
    }
    bump_version(rule);
}

fn bump_version(rule: String) {
    let mut version = Version::get_current();

    if rule == "major" {
        version.major += 1;
        version.minor = 0;
        version.patch = 0;
    } else if rule == "minor" {
        version.minor += 1;
        version.patch = 0;
    } else if rule == "patch" {
        version.patch += 1;
    }
    write_version(version);
}

fn manual(rule: String) {
    if rule != "major" && rule != "minor" && rule != "patch" {
        panic!("Unknown rule: {}", rule);
    }
    bump_rule(rule);
}
Enter fullscreen mode Exit fullscreen mode

There, all good, right? Well, not entirely. Our new bump_version function is the arbiter of applying rules to versions—but now we have three different places those rules are defined! If we want to add a "prerelease" rule, we'd have to remember to change every location separately, which could easily cause a bug! So, again, we'll maintain only a single source of truth.

enum Rule {
    Major,
    Minor,
    Patch,
}

fn auto(history: &[Commit]) {
    let mut rule = Rule::Patch;

    for commit in history {
        if commit.message.contains("BREAKING CHANGE") {
            rule = Rule::Major;
            break;
        } else if commit.message.contains("feat") {
            rule = Rule::Minor;
        }
    }
    bump_version(rule);
}

fn bump_version(rule: Rule) {
    let mut version = Version::get_current();

    match rule {
        Rule::Major => {
            version.major += 1;
            version.minor = 0;
            version.patch = 0;
        },
        Rule::Minor => {
            version.minor += 1;
            version.patch = 0;
        },
        Rule::Patch => {
            version.patch += 1;
        },
    }

    write_version(version);
}

fn manual(rule: String) {
    if rule == "major" {
        bump_version(Rule::Major);
    } else if rule == "minor" {
        bump_version(Rule::Minor);
    } else if rule == "patch" {
        bump_version(Rule::Patch);
    } else {
        panic!("Unknown rule: {}", rule);
    }
}
Enter fullscreen mode Exit fullscreen mode

There we go, one source of truth for the rules, how to apply them to a semantic version, how to generate them from commit messages, and how to interpret a user's input.

But I see more repetition! What if we changed to a single commit.message.contains statement with a map from the keywords to the rule they represent?

When considering any refactor, it's crucial to think about the goal. In the case of MOIST, we have to ask ourselves, "what is the truth we're trying to protect?" Is it true that "breaking changes" and "features" should always be determined the same way? No! In fact, this implementation is not consistent with Semantic Versioning yet, and the two branches will eventually diverge further! Coupling these two pieces of information would increase rigidity without protecting a single source of truth, so we should not combine them.

What about the user input? Surely we should factor out the string-to-rule map and only have a single function call location! Let's try the same test—what is the single truth we're trying to protect? Is it "every rule should be determined by only a single string?" That feels more like an implementation detail. In fact, if we add a prerelease rule, we'll need some additional info to select a prefix. This change feels like it would be a reduction in repetition without a clear goal—it would bind the separate rules together, making it harder to change just one without an obvious benefit.

MOIST can get subjective and be a bit mushy—as can any coding practice, but it tries to draw a line between harmful repetition and benign code. The key to successfully applying any "best practice" is understanding the true goal and always keeping that in mind.

RAINY

Cake aside, there are a couple more worthwhile goals semi-related to DRY. I'll attempt to shoehorn them into more acronyms semi-antonymic to DRY. "Reusable Abstractions, Ideally Not Yours" is another way of saying "don't build when you can buy." It is far more efficient for one person to solve a problem and share that solution than for hundreds of people to solve it independently. This practice is applicable at many scales but achieves another one of the fundamental goals that DRY tries to stand for.

The best example of this is the open-source community. Rather than re-do work that someone else has done, you can build on top of what exists. Likewise, you can share solutions to problems you've faced so that others don't need to waste future effort—and you multiply the impact of your work. RAINY is like taking "don't repeat yourself" and applying it across our entire community. More like "let's not repeat ourselves."

As with everything, there is a balance to be struck. We don't want a MONSOON, where "Maintaining Open source is a Nuisance So Others Often Neglect it." 🤪 Basically, as a consumer, installing dependencies is a pain, and keeping them up to date is even more of a pain (you should be using Renovate to mitigate that, though). On the other side, staying on top of issues and pull requests as a maintainer is burdensome and often thankless. This level of effort on both sides can lead to relatively simple bugs never being fixed.

Balancing this is difficult, and I think a conversation about the good and bad of open source can go far beyond this article—so I'm going to leave it there. However, if you'd like a post titled either "Do You Really Need that Dependency?" or "When to Open Source it", let me know.

FRESH

"Functions Read Easier in Short Hunks." Yes—my acronyms are getting more and more unhinged and also straying further from DRY. I'm not sorry.

Often, reviewers use DRY as a code word for "make that into a function." Code readability is vital for maintainability, and an easy way to improve readability is to split large functions into several smaller ones. This way, a reader can get a high-level understanding of what the code is doing just by reading the function names in order—and they should read a bit like prose.

Of course, there's a tradeoff to be made here; a function call is not always easier to read than the statements that make it up, and calling functions can often have performance penalties. Still, as a goal, refactoring to readability is far more valuable than refactoring simply to reduce duplication.

A Test

Here's a set of questions I suggest you ask yourself next time you're wondering if you should be repeating yourself or not:

  1. Are there two separate sources of truth for a single concept? If yes, try to unify them.
  2. Is this code solving a general problem or something specific to me / my work? If it can be generalized, consider publishing a reusable module (whether open source or in a private/corporate location).
  3. Can I read through this code and understand what it's doing without stopping to inspect it? For this one, ideally, have someone who didn't write the code answer the question in a code review. If it's hard to parse for humans, consider abstracting away details that aren't needed, like with functions.

Hopefully, all of my acronymic nonsense has provided you with enough alternatives to DRY that you never use it again. Remember, not repeating yourself is not a valuable outcome of refactoring. Instead, try keeping a goal in mind like improving correctness, benefitting the community, or making the code easier to read. Now go enjoy some cake; you've earned it.


Was this post super helpful to you? Tip me on GitHub, Patreon, or Ko-Fi.

Have a question or comment about this post? Leave it in the discussion thread on GitHub!

Want to be notified of future posts? Watch releases in the GitHub repo or follow me on Twitter.

Have an idea or request for a future blog topic? Drop it in the GitHub discussions under ideas.

Top comments (16)

Collapse
 
jayjeckel profile image
Jay Jeckel

That's DRY. No need for a different acronym just because some people misapply the original. You can go around talking about MOIST code, but your basically just describing proper application of DRY.

As I was taught it, Don't Repeat Yourself, but also Write Everything Twice because it takes at least three repetitions to make an abstractable pattern.

Collapse
 
auroratide profile image
Timothy Foster • Edited on

Indeed, no need to replace DRY in our day-to-day. Rather, MOIST is one tool to teach the true intent of DRY and dispel misconceptions. Of course there are different ways to teach that, but MOIST is a great comical option and one I may likely use.

Collapse
 
paddy3118 profile image
Paddy3118

Exactly, the pattern needs to be established.

Collapse
 
arvinyorro profile image
Arvin Yorro

Agreed. This is DRY with extra steps.

Refactoring should be viewed as a journey or an evolution. Avoid hasty abstractions.

Collapse
 
auroratide profile image
Timothy Foster

I love the MOIST concept! My mental model for DRY has generally been that it applies to business logic, not code (though of course the two will coincide), but MOIST summarizes that idea in a much more catchy and general way.

I can't give you a cake, but here's a 🦄!

Collapse
 
etienneburdet profile image
Etienne Burdet

I agree that sometimes, flat, repetitive code is easier to maintain. A good exemple is tests : kentcdodds.com/blog/avoid-nesting-...
They are linear an repetitive by nature and trying to factor them too much will cause more headaches than anything when having them to evolve.

Part of CSS is like that, some constants or just things that "happen to be alike" but won't necessary exactly the same all the time.

Collapse
 
peerreynders profile image
peerreynders

A good example is tests

Tests have different priorities - Tests Too DRY? Make Them DAMP!:

"Since tests don't have tests, it should be easy for humans to manually inspect them for correctness, even at the expense of greater code duplication. …which emphasizes readability over uniqueness"

and yet

"the DRY principle is still relevant in tests; for example, using a helper function for creating value objects can increase clarity by removing redundant details from the test body."

The challenge is to keep inappropriate coupling (resulting from overzealous DRY) out of tests because that can lead to fragile tests.

Collapse
 
jeremyf profile image
Jeremy Friesen

I also humbly submit DoRK code: Do not Repeat Knowledge.

But MOIST is good.

Collapse
 
anuoluwapods profile image
Anuoluwapo Balogun

😁

Collapse
 
jwhenry3 profile image
Justin Henry

The problem here is not with DRY, its with design patterns. Yes you eliminated duplication, but in the wrong way. You should rather use registry or something like service-location in order to convey the operation to be performed. Creating configuration files/objects to specify the process or procedure to be performed when a certain value is received. If you pass value A, then it finds the operation for value A, and so on. When you want additional functionality to happen in certain cases and not in others, you then employ another design pattern, perhaps event driven design, where you have a publisher that tells a system that a value was received, and in other areas of the application, it will apply its own side effects for said value. There are ways to reach DRY standards without sacrificing code quality. You just have to hunt for them and keep improving your own way of coding.

Collapse
 
190245 profile image
Dave

A rather click-batesque title, some assertions made about DRY being harmful (and I don't think the example proves or disproves those assertions), and then a "lets not stick to DRY!"

I mean, I applaud you for not wanting to stick to any extreme, because we shouldn't do that.

But I have to wonder, if we're being "MOIST" without valid reason, we're burning CPU cycles needlessly somewhere (either at build time, which is mildly annoying, or at runtime, which is rather concerning).

Maybe JS developers don't care about CPU cycles though? :)

If we're being DRY without valid reason, we're over optimising/engineering the solution, and burning time in SDLC, which is also concerning. If we're being WET without valid reason, the product probably never ships in the first place (unless any of us are making AAA game titles, in which case, they ship and the dev's keep enjoying typing).

PS, for WET, I prefer "We Enjoy Typing"

Collapse
 
thiagocarvalho0877 profile image
Thiago Souza de Carvalho

I totally agree!

Collapse
 
jwp profile image
John Peters

Bad Title.

S in SOLID and DRY are the top concern for good coding.

Collapse
 
jzombie profile image
jzombie

TLDR: A function does something. If something else is needed that does the same thing, call the function.

Collapse
 
shriji profile image
Shriji

WET = Write Everything Twice

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.