TL;DR
- DRY = Don’t Repeat Yourself
- make sure you don’t duplicate code or knowledge
- if you identify duplicate code, try to abstract it and make it reusable
- keep your code modular (tackling one specific task) and decoupled (minimum number of dependencies)
"The code you write today will be somebody's legacy code tomorrow.” - The Pragmatic Programmer
Intro
Software engineering principles are guidelines that help software developers write good code.
Good code is efficient, maintainable, and scalable.
In this series of articles, we will cover some of the most important principles that we need to apply when writing code.
Today, we will be discussing the DRY (don’t repeat yourself) principle. Where does it come from? What does it mean? How can we apply it?
Origin
The DRY principle has been formulated by Andy Hunt and Dave Thomas in their book The Pragmatic Programmer, in 1991.
This book is a classic in the software development field and is a recommended-read for any developer.
Meaning
The DRY principle states that “Every piece of knowledge must have a single unambiguous, authoritative representation within a system”.
Unambiguous means that your code should be clear and not open to multiple interpretations.
Authoritative means that your code serves as the definitive reference for a particular functionality. In other words, you should not have 2 blocks of code doing the same thing.
By following the DRY principle, you ensure that your code is clear and free of duplication.
Application
There are a few rules you can follow to make sure your code is DRY:
1. Identify Code Duplications
When writing code, before creating a new function, a new component or any new piece of logic, make sure there is nothing similar already present in the codebase.
Now, imagine that in your codebase you need to transform milliseconds to minutes multiple times, at different places. You could have something like that:
// Component A
const functionA = () => {
// some logic here ...
const intervalInMilliseconds = 60_000;
const intervalInMinutes = Math.floor(intervalInMilliseconds / 60_000);
// some logic there ...
}:
// Component B
const functionB = () => {
// some logic here ...
const intervalInMilliseconds = 120_000;
const intervalInMinutes = Math.floor(intervalInMilliseconds / 60_000);
// some logic there ...
}:
You realise that you have duplicated logic. What can you do?
2. Make Your Code Reusable
If you identify duplicated logic, think about how you can make it generic and reusable in all the places it is needed. Create an abstraction (such as a function) that encapsulates that logic and reuse it wherever needed.
When you do that, you are creating a single authoritative representation of that logic.
Taking back our previous example, here’s what you can do:
// utils/millisecondsToMinutes.js
const millisecondsToMinutes = (ms) => {
const minutes = Math.floor(ms / 60_000);
return minutes;
}
export default millisToMinutes;
// utils/index.js
import millisToMinutes from './millisToMinutes';
// other utils functions here ...
export {
millisToMinutes,
// other functions exported here ...
};
// Component A
import millisecondsToMinutes from 'utils'
const functionA = () => {
// some logic here ...
const intervalInMilliseconds = 60_000;
const intervalInMinutes = millisecondsToMinutes(intervalInMilliseconds);
// some logic there ...
}:
// Component B
import millisecondsToMinutes from 'utils'
const functionB = () => {
// some logic here ...
const intervalInMilliseconds = 120_000;
const intervalInMinutes = millisecondsToMinutes(intervalInMilliseconds);
// some logic there ...
}:
3. Keep Your Code Modular And Decoupled
Modularity means that your code is reduced to small modules, components, or functionalities, responsible for a specific task, such as you can easily reuse it.
Decoupling means that you have reduced to a minimum the dependencies of your logic, making it easier to modify or replace one module without affecting others.
Here’s an example of a non-modular and tightly coupled piece of code:
// Non-modular code with tight coupling
const calculateNetSalary = (baseSalary) => {
const taxRate = 0.4; // Hardcoded tax rate
const deduction = 100; // Hardcoded fixed deduction
const taxAmount = baseSalary * taxRate;
const netSalary baseSalary - taxAmount - 100;
sendSalarySlipByEmail(netSalary);
return netSalary;
}
In the above example, the function directly uses hardcoded values for tax rate and deduction. This makes the function dependent on these specific values and difficult to modify without changing the function's implementation. If these values need to be changed, the function would need to be updated directly, which can lead to maintenance issues and make the code less flexible.
The function is also not quite modular as it does more than one thing. In addition to calculating the net salary, it sends the payslip by email with the sendSalarySlipByEmail
function integrated within it.
Here’s a more modular and less coupled version:
const calculateNetSalary = (baseSalary, taxRate, deduction) => {
const taxAmount = baseSalary * taxRate;
const netSalary = baseSalary - taxAmount - deduction;
return netSalary;
}
const netSalary = calculateNetSalary(5000, 0.4, 100);
sendSalarySlipByEmail(netSalary);
4. Do Code Reviews
If you work with other people on the same codebase, ask someone to review your code, and review other developer’s code. Fresh perspectives can often identify smelly code (such as duplication) that may not be apparent to the original author.
Conclusion
Applying the DRY principle involves identifying and eliminating code duplication, making your code reusable, abstracting common functionalities, and keeping your code modular and decoupled.
Regular refactoring and code reviews can help in maintaining adherence to the DRY principle in day-to-day coding practices.
By following the DRY principle, you will create more efficient, maintainable, and high-quality softwares.