skies.dev

Make Your Code DRY: Don't Repeat Yourself!

6 min read

Learning how to make my code DRY has helped make my code easier to work with. DRY is an acronym for "Don't Repeat Yourself." Among software engineers, we use this acronym like an adjective, saying things like

This looks like a copy and paste, so can we DRY it up a little?

So what exactly do we mean?

Don't Repeat Yourself is a design pattern used to make software more maintainable and reusable. The problem DRY solves is duplication. I'll show you some examples of duplication in code and how you can go about refactoring it to make it DRY.

A good read to learn DRY is from the The Pragmatic Programmer. This book is a "must read" for any software engineer. We'll adapt some of the ideas from this book to this article.

How to make your code DRY

Let's say we're creating a User class for our application. We want to know if a user can perform certain actions in our app, such as liking a post or following a user.

In general, we don't want to allow a user in an "inactive" state to perform any action in the app.

Can you spot the duplicate code?

class User
  def can_bookmark_post?(post)
    true unless @disabled || @suspended || post.deleted?
  end

  def can_like_post?(post)
    true unless @disabled || @suspended || post.deleted? || post.author_id == @id
  end

  def can_follow_user?(user)
    true unless @disabled || @suspended || following?(user) || user.id == @id
  end
end

As you can see we're duplicating the logic for determining an inactive user in three places. If we ever want to change the criteria for what an inactive user means, we would need to change at least three methods with the new business rules.

We can make this code DRY by introducing a helper method to define what an inactive user is and carry it through the other methods that depend on knowing if a user is inactive.

class User
  def inactive?
    @disabled || @suspended
  end

  def can_bookmark_post?(post)
    true unless inactive? || post.deleted?
  end

  def can_like_post?(post)
    true unless inactive? || post.deleted? || post.author_id == @id
  end

  def can_follow_user?(user)
    true unless inactive? || following?(user) || user.id == @id
  end
end

Now we only have to change the rules for determining if the user is inactive in one place, and the change will apply to all the places that depend on it.

Example of DRY using React and Tailwind CSS

Let's see how we can apply DRY principles to frontend code as well. We'll build a NewsletterSignUp, Login, and SignUp component and style them Tailwind CSS class names.

Do you see an issue here?

export function NewsletterSignUp() {
  return (
    <div>
      <p>Sign up for the newsletter</p>
      <form method="POST" action="/newsletter/signup">
        <input type="email" name="email" />
        <button
          type="submit"
          className="bg-blue-500 px-2 py-1 font-bold text-white shadow hover:bg-blue-600"
        />
      </form>
    </div>
  );
}

export function Login() {
  return (
    <div>
      <p>Sign in</p>
      <form method="POST" action="/login">
        <input type="email" name="email" />
        <input type="password" name="password" />
        <button
          type="submit"
          className="bg-blue-500 px-2 py-1 font-bold text-white shadow hover:bg-blue-600"
        />
      </form>
    </div>
  );
}

export function SignUp() {
  return (
    <div>
      <p>Sign up</p>
      <form method="POST" action="/signup">
        <input type="email" name="email" />
        <input type="password" name="password" />
        <input type="password" name="password-confirmation" />
        <button
          type="submit"
          className="bg-blue-500 px-2 py-1 font-bold text-white shadow hover:bg-blue-600"
        />
      </form>
    </div>
  );
}

We're applying the same set of class names on each of the buttons! If we want to change the button's style consistently across all of these components, we'll need to update it in at least three places.

Since React is a component framework, we can easily extract the button into a separate component that encapsulates the styling information.

export function PrimaryButton(buttonProps: React.HTMLProps<HTMLButtonElement>) {
  return (
    <button
      className="bg-blue-500 px-2 py-1 font-bold text-white shadow hover:bg-blue-600"
      {...buttonProps}
    />
  );
}

export function NewsletterSignUp() {
  return (
    <div>
      <p>Sign up for the newsletter</p>
      <form method="POST" action="/newsletter/signup">
        <input type="email" name="email" />
        <PrimaryButton type="submit" />
      </form>
    </div>
  );
}

export function Login() {
  return (
    <div>
      <p>Sign in</p>
      <form method="POST" action="/login">
        <input type="email" name="email" />
        <input type="password" name="password" />
        <PrimaryButton type="submit" />
      </form>
    </div>
  );
}

export function SignUp() {
  return (
    <div>
      <p>Sign up</p>
      <form method="POST" action="/signup">
        <input type="email" name="email" />
        <input type="password" name="password" />
        <input type="password" name="password-confirmation" />
        <PrimaryButton type="submit" />
      </form>
    </div>
  );
}

Now if we want to update the style of the primary button, we only have to do it in one place.

Duplication in documentation

We've seen a couple of

A warning about DRY code

When first learning about DRY, it's tempting to extract code and make it "reusable" where it doesn't need to be. Despite our best intentions, this can inadvertently make the code more complex by adding more levels of indirection.

In my own work, I'll usually hold off on making my code DRY until I find a good reason to. Introducing abstractions too early can make the code rigid and inflexible.

Take the React example above. What if we notice that type="submit" is repeated several times for each usage of the <PrimaryButton> component. You might be tempted to think

That's not DRY! We can write type="submit" ONCE inside the <PrimaryButton> component.

This is a problem though. What if you want to use the <PrimaryButton> for something other than submit button? By reducing duplication, the component becomes less flexible. Judgement is needed when deciding the right time to introduce an abstraction and DRY up the code.

For more exploration on this topic, check out the following:

Summary

DRY is about creating abstractions reduce duplication to make code more maintainable. By making your code DRY, you can define business logic in one place and reuse it in other parts of the system.

However, abstractions come at a cost. Introducing abstractions could make code harder to follow and less flexible. Instead of trying to reduce duplication at all costs, think about what tradeoffs you're making when you complete disallow duplication in your codebase.

Hey, you! ๐Ÿซต

Did you know I created a YouTube channel? I'll be putting out a lot of new content on web development and software engineering so make sure to subscribe.

(clap if you liked the article)

You might also like