Make Your Code DRY: Don't Repeat Yourself!
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:
- Dan Abramov's talk on WET (Write Everything Twice) codebases
- Kent C. Dodds's article on AHA (Avoid Hasty Abstractions) programming
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.