The DRY principle is an integral part of clean code, but what does it actually mean and what is it really good for?
Should you really care about it?
What does it actually mean?
Don't write code that does the same thing twice.
It doesn't mean that you must only ever write one loop within all of your code. But you shouldn't rewrite business or core logic multiple times by hand.
An Example
Let's take a look at a very basic example to give you an idea of what it actually looks like to unnecessarily repeat code.
async function getArticlesForUser(userId) {
const userResponse = await fetch(`/api/users/${userId}`, {
credentials: ‘include’,
redirect: ‘follow’
});
const user = userResponse.json();
const articleResponse = await fetch(`/api/articles/${user.authorId}`);
return articleResponse.json();
}
async function getCommentsForUser(userId) {
const userResponse = await fetch(`/api/users/${userId}`, {
credentials: ‘include’,
redirect: ‘follow’
});
const user = userResponse.json();
const commentResponse = await fetch(`/api/comments/${user.commentId}`);
return commentResponse.json();
}
Can you spot the issue here?
Both functions implement fetching a user, which requires at least some knowledge about the API endpoint, and what options have to be set to successfully retrieve a user from the API.
Why is it important?
As you have seen above, implementing basically the same logic twice also yields two places to check if something ever changes.
Take the user API for example: What if the endpoint changes? Right, you'd also have to change two occurrences in your code.
But what if you forget one occurrence? Well, hopefully, you have tests covering that.
But this leads to another issue. You also have to test the same thing twice for two different use cases.
The issue with multiple places were the same things happen
You're, however, still left with multiple places you could forget to change...
async function getArticlesForUser(userId) {
// we changed fetch to contact the new API endpoint here
const userResponse = await fetch(`/api/v2/user/${userId}`, {
credentials: ‘include’,
redirect: ‘follow’
});
const user = userResponse.json();
const articleResponse = await fetch(`/api/articles/${user.authorId}`);
return articleResponse.json();
}
async function getCommentsForUser(userId) {
// but we forgot to change this!
const userResponse = await fetch(`/api/users/${userId}`, {
credentials: ‘include’,
redirect: ‘follow’
});
const user = userResponse.json();
const commentResponse = await fetch(`/api/comments/${user.commentId}`);
return commentResponse.json();
}
And this is exactly one of the reasons you shouldn’t repeat yourself.
It isn’t easy to spot all occurrences of repeated code, and if that code has a special meaning, it’s a breeding ground for bugs.
Fixing the issue
To fix the issue you can abstract away how you fetch a user from your API by placing it within its own function.
Whenever fetching a user changes, you only have to adjust one specific piece of code and only one specific set of tests.
async function getUser(userId) {
const userResponse = await fetch(`/api/v2/user/${userId}`, {
credentials: ‘include’,
redirect: ‘follow’
});
return userResponse.json();
}
async function getArticlesForUser(userId) {
const user = await getUser(userId);
const articleResponse = await fetch(`/api/articles/${user.authorId}`);
return articleResponse.json();
}
async function getCommentsForUser(userId) {
const user = await getUser(userId);
const commentResponse = await fetch(`/api/comments/${user.commentId}`);
return commentResponse.json();
}
Can you see the advantage of the approach above?
One function which does exactly what it says it does, getting a user and returning it. Whenever you need a user within your codebase, import that function and use it.
When to abstract
You'll get a lot of different answers when you ask developers about when you should start abstracting code away because everyone does it differently.
If you want to be very strict: As soon as you see at least some potential to abstract something away, do it.
But in my humble opinion, that's too strict. Better wait for at least the first repetition before you abstract logic away.
Sometimes you'll find small nuances which you can also cover within your abstraction, which opens it up for a broader range of use cases.
This still leaves you with the problem of actually finding repetitions which is a problem other developers have as well.
Automated help
This is why nearly every modern IDE has at least one plugin that helps you detect duplicate code.
Those plugins are an awesome helper because they point you towards duplicate code, which reduces manual labor, which is always great.
There are also command-line tools that you can integrate within your build pipeline.
They'll detect duplicate code and warn you.
Conclusion
The DRY principle is a pretty great way to reduce repetition and also complexity within your code.
Take it this way: Better be lazy and write stuff once than to repeat yourself over and over again while wasting precious time.
Show Some Support
If you liked this article, visit me on Twitter, and it would mean the world to me if you also liked and shared the thread on Twitter.