Over reusability
_Thinking of implementing your next project as general and reusable as possible? Think again!
One of the first and most important principles I have learned as a software engineer is DRY (Don’t Repeat Yourself). In the beginning, I thought about it on every line of code. Sometimes, I tried so hard to make my code reusable, even if it wasn’t expected to be used anywhere else. And it just made my code to be hard to maintain and read.
Today, after some miles as a software engineer, I wanted to share my thoughts on what to consider before following the DRY principle.
Writing a reusable code means writing a general enough code that will be useful for multiple (present or future) requirements.
The main advantages of having a reusable code are:
- When implementing a new feature, reusing an already proved and well-maintained solution saves time and ensures good quality.
- Improving a reusable code improves all of its users at once.
- The code of the user is easier to read because it has fewer implementation details.
The main disadvantage is that general solutions are not likely to cover all of the requirements of the users. Requirements that the “general” solution will not initially cover, would make the code harder to read, maintain, and expand. Which paradoxically makes it harder to reuse.
I really liked what Bruce F. Webster wrote about it:
“The more general (and thus reusable) a software component is, the less likely it is to be useful without modification for a given application.
The more suited a software component is for a given application, the less likely it is to be useful elsewhere.”
Sometimes, when all the stars are aligned, you find a general solution that covers an entire domain of requirements (present or future). If implemented correctly, it will bound the context of the problem in a single implementation, and you will benefit from the advantages of having a reusable code.
That’s not always the case, so before applying the DRY rule, let’s consider some factors…
Agility
Whether you have just started a new project, or your project is already widely used, think about how easy it is to change or add features. Reusability can take you both ways:
- Reusing can save a lot of time because you have less to implement and maintain.
- If the code you reuse does not 100% fit your needs, you will have to modify a code that other features use, which takes more time.
Uncertainty & Flexibility
Keep in mind that reusability couples together multiple pieces that can change independently. It reduces flexibility, which is good to have when the amount of uncertainty is large.
Reusing makes every modification impact other requirements that are already satisfied. At some point, over-reusability will make your code a legacy, because the cost of changing it will be so high that it will not be worth the value of adding or changing it.
The further the future is, the harder it is to predict. Consider the impact of modifying the code in the future.
It is OK to have code duplication
Yes, I said it! When you write abstraction and generalize your code wrongly, you make all of the reusers get coupled with a code that is hard to modify, so hard that you would avoid touching and breaking requirements that were already satisfied (remember uncertainty and flexibility?).
Sandi Metz has a blog post that explains why you should avoid such abstractions and how to get rid of them. Here is a quote that I really liked from there:
“prefer duplication over the wrong abstraction”
You can also read more about it in Kent C. Dodds’s blog post — he named it: AHA (Avoid Hasty Abstraction).
If it looks like the code you need, it doesn’t mean you should reuse it
Let’s say you have an API to list resources based on some filter parameters. And you are required implement an API to request a single resource based on a unique identifier. You can reuse some parts of the previous API if the code is modular enough, but don’t write workarounds and patches only for you be able to fit the existing code with the requirement.
We have a function for retrieving the items and the count:
async getItems(filter) {
const data = await db.findMany(filter);
const count = await db.count(filter);
return { data, count };
}
And we want to add a function for retrieving a single item.
One way of doing it is to generalize the existing code. You may also need to make it backward compatible.
// Avoid ❌
async getItems(filter, withCount) {
const data = await db.findMany({ where: filter });
const count = (withCount ?? true) ? await db.count(filter) : undefined;
return { data, count };
}
async getItem(id) {
const { data } = getItems({ id }, false);
if(data.length === 0) throw new NotFoundException(id);
return data[0];
}
Another way to do it, is to make the new function to reuse a lower level code.
// Prefer ✅
async getItems(filter) { // Nothing changes here 👍
const data = await db.findMany(filter);
const count = await db.count(filter);
return { data, count };
}
async getItem(id) {
const data = await db.findFirst({ where: { id } });
if(!data) throw new NotFoundException(id);
return data;
}
We can see here how generalizing your code can cause coupling.
Clean as you go
Always leave the code cleaner than it was. Especially when it is widely used!
When changing a common code, spend time on doing it right and understand the entire picture. Often, it is a bit scary to change such code because of the impact it has on components that you are not familiar with, so the solution becomes to patch it. And when that happens, eventually- the code will look like a spaghetti of patches. 🍝
When estimating a task related to a widely used component, calculate the time it takes to clean it and ensure consumers don't break. Of course, timelines don’t always allow it, but cleaning is necessary to keep the code of high enough quality.
Avoid reusing legacy code
Is the code you are about to reuse well maintained? If not, it will save time to reuse it, but in the future, it may be deprecated or not fit the requirements. Make sure you understand the limits of the code and that it can be modified in the future.
Prepare for un-using
Think about how easy it is to divert your code from using a concrete implementation A to a concrete implementation B. If the code you are reusing is not relevant anymore for your needs, it should be relatively easy to replace it with something different.
For example, if you have code that is responsible for using common AWS SQS functionality, it should be relatively easy to refactor the code to use Kafka instead or to use a mock for running tests in isolation.
Read more: Dependency Inversion, Portability.
Conclusion
If your code is reusable, then it has more impact on the system- so implement it thoughtfully. Add tests and strive for high coverage.
Plan your changes and understand the impact in case of modification. Make sure that you spend more time on the quality, that it is refactorable, and that it can be easily reused. Keep in mind that someone is going to read and modify this code in the future, so make it readable.
There are a lot of advantages to following DRY principle and making the code reusable. But always be aware of the drawbacks of overusing it. Find the balance and understand what would be most beneficial for your system overall.