The main reason to care about clean code is due to the developer team's speed to maintain and deliver more functionalities in an existing project. It's not just about aesthetics, the people involved in the project will have more ease and less stress working on it.
Roadmap to achieve a cleaner code:
The boy scout rule
Variables
Functions
Comments
Formatting
Unit Tests
Refactoring
Introduction
One thing all developers should perceive is that they are writing not only for machines to interpret their code but for humans either. It's important because not only others will need to understand the code, but himself too, because he may have started working on other projects or features in the future, and when he comes back, the developer will be out of context.
So, when coding, we should consider the order and "Human" meaning of the classes/functions/variables/components. It must not be just a procedural "cake recipe".
The boy scout rule
Leave your code better than you found it
That may seem like obvious advice, and it is! But it's not about when we leave our project we need to make it perfect. Instead, it advises us to gradually and frequently refactor our code. That way, it won't slow us down, it will just speed us up when we get back to that code, and since we are not trying to achieve the holy grail, the most unrealistic perfect code, it shouldn't take too long to do it.
Variables
Rules that can significantly improve the code:
Meaningful names
Pronounceable/Searchable names
Dirty Example
Note that "x", "y", "r", "special", "animals", "f" and "a" have names that don't mean anything to humans, they just store values that we need to look in the code for their meaning.
const x = 0.8
const y = 1500
const special = false
const r = calculateInterestAmount(x, y, special);
function calculateInterestAmount(x, y, special) {
return (special ? (x - 0.5) * y : x * y).toFixed(2)
}
const anmls = ["Dog", "Cat", "Bird"]
const f = anmls.find(a => a === "Cat")
Clean Example
Now we know what each variable does in its context. And we don't have the magic number "0.5" floating around, we can see why it is there.
const interestRate = 0.8
const totalAmount = 1500
const isSpecial = false
const interestAmount = calculateInterestAmount(interestRate, totalAmount, isSpecial);
function calculateInterestAmount(interestRate, totalAmount, isSpecial) {
const SPECIAL_INTEREST_RATE_DISCOUNT = 0.5;
let appliedInterest = isSpecial ?
(interestRate - SPECIAL_INTEREST_RATE_DISCOUNT) : interestRate;
return (appliedInterest * totalAmount).toFixed(2)
}
const pets = ["Dog", "Cat", "Bird"]
const catFound = pets.find(pet => pet === "Cat")
Functions
Rules that can significantly improve the code:
Function names (follow the same principle as variables)
Size of the function (Vertically and Horizontally)
Usually, when a function is too big, it is probably not following the principles of the “Single Responsibility Principle” (SRP) or “Don't Repeat Yourself” (DRY).It shouldn't have more than 3 parameter
This one is debatable, but one thing we can all agree on is that fewer parameters are better for testing (since there are fewer combinations of inputs and expected outputs), understanding, and use of the function.
Dirty Example
const firstLoan = {
interestRate: 0.8,
totalAmount: 1500,
isSpecial: false
};
const secondLoan = {
interestRate: 0.6,
totalAmount: 10000,
isSpecial: true
};
calculateTotalLoans(firstLoan, secondLoan);
function calculateTotalLoans(firstLoan, secondLoan) {
let totalLoans = 0
const SPECIAL_INTEREST_RATE_DISCOUNT = 0.5
let firstAppliedInterest = firstLoan.isSpecial ?
(firstLoan.interestRate - SPECIAL_INTEREST_RATE_DISCOUNT) : firstLoan.interestRate
const firstInterestAmount = (firstAppliedInterest * firstLoan.interestRate)
totalLoans += firstInterestAmount + firstLoan.totalAmount
let secondAppliedInterest = secondLoan.isSpecial ?
(secondLoan.interestRate - SPECIAL_INTEREST_RATE_DISCOUNT) : secondLoan.interestRate
const secondInterestAmount = (secondAppliedInterest * secondLoan.interestRate)
totalLoans += secondInterestAmount + secondLoan.totalAmount
return totalLoans.toFixed(2)
}
Here we can't scale the function if we had more loans and we have duplication of codes in lines 19–22 and 26–29.
Clean Example
const loans = [
{
interestRate: 0.8,
totalAmount: 1500,
isSpecial: false
},
{
interestRate: 0.6,
totalAmount: 10000,
isSpecial: true
}
];
function calculateTotalLoans(loans) {
let totalLoans = loans.reduce(calculateIndividualLoan, 0)
return totalLoans.toFixed(2)
}
const calculateIndividualLoan = (total, loan) => {
const individualLoan = calculateInterestAmount(
loan.interestRate,
loan.totalAmount,
loan.isSpecial
) + loan.totalAmount
return total + individualLoan
}
function calculateInterestAmount(interestRate, totalAmount, isSpecial) {
const SPECIAL_INTEREST_RATE_DISCOUNT = 0.5;
let appliedInterest = isSpecial ?
(interestRate - SPECIAL_INTEREST_RATE_DISCOUNT) : interestRate;
return (appliedInterest * totalAmount)
}
calculateTotalLoans(loans);
4. Indenting
Since the indenting guides us when we are reading, it should always make sense. The content of functions, conditions and classes should match their opening and closing brackets, giving a sense that the code inside the bracket "belongs" to that section.
5. Have no side effects
In this function, it says to us that it will "check the password passed", but if you look at line 3, you will see that it has a second responsibility to "startAdminSession" if the password is "Admin". That is certainly a side effect that this function should not be responsible for.
7. Guard clauses
I started really to notice this when I learned the swift language because it has a "guard (condition) else { return }" statement that started to make sense for code cleanness for me and check for null variables of course.
Dirty Example
function checkAdmin(user) {
if (user) {
if (user.isAuthenticated) {
if (user.isAdmin) {
return true
} else {
return false
}
} else {
return false
}
}
}
Clean example
function checkAdmin(user) {
if (!user) return false
if (!user.isAuthenticated) return false
if (!user.isAdmin) return false
return true
}
Comments
If you feel the need to write a comment, try to refactor your code first, in a way that will be self-explanatory. Some cases are really hard, but try to minimize the most you can. And there is always a risk that your comment will be left behind in terms of updated to your code, leading to confusion afterward.
Good comments
Warning of consequences: This one should be used moderately, but it could prevent unwanted consequences from happening.
TODO/FIXME: They can be used as reminders, but if written in the project must be prioritized to not accumulate all over the project. Usually, that is better controlled outside of the project.
JSDoc-like comment: They can be used to document more complex code.
Bad Comments
Redundant
// Check if a user is authenticated and is and Admin
function checkAdmin(user) {
if (!user) return false
if (!user.isAuthenticated) return false
if (!user.isAdmin) return false
return true
}
Comments vs Variable/Functions
Usually, well-named variables and functions give a good notion of what it does. Think about our previous example function "checkAdmin", is it a good name? it could be better, a clearer definition would be "IsAdminAuthenticated".Commented-out code
This one caught me falling into it. The problem is if you comment on a code and let it stay in your project, no one except you will know if they can delete that code, because it certainly has a reason to be there instead of deleted. If we keep doing that practice, we will overload our project with undesired pollution of commented code (of course, there are exceptions for this).
Unit Tests
The best practices of TDD always tell us to write our code first and then our production code. There are four things you should consider when dealing with tests:
The three Laws of TDD
1. You may not write production code until you have written a failed unit test.
2. You may not write more of a unit test than is sufficient to fail and not compiling is failing.
3. You may not write more production code than is sufficient to pass the currently failing test.One assert per test (similar to the DRY principle, don't do too much in a single test)
F.I.R.S.T
FAST: tests should be fast because slow tests will discourage us to run them frequently.
INDEPENDENT: tests should not be dependent because they can cause a chain effect of failures that is hard to deal with.
REPEATABLE: tests should be able to run in any environment, develop, homolog, or production, it doesn't matter which environment it is running.
SELF-VALIDATING: tests should always output a boolean because it will speed up the reading of the result of the tests.
Restructuring vs Refactoring
Restructuring is any rearrangement of parts of a whole. It’s a very general term that doesn’t imply any particular way of doing the restructuring.
Refactoring is a very specific technique, founded on using small behavior-preserving transformations (themselves called refactorings)
— Martin Fowler
Why refactoring is essential?
Improves the Design of the software
Boosts programming speed
It makes code easier to read
It helps find bugs
It helps to understand better the code you are working on
I won't explore in this article refactoring techniques, because it would get too long for that. But I just wanted to mention it because it helps us as developers to make a more maintainable project and grow as professionals.
If you want to know more about this kind of content, try reading the books that are in the "References" section, I used them to learn and write this article. Since there is a lot of content there, I just made a synthesis of the things that impacted me the most.
References:
Robert Cecil Martin, Clean Code (Prentice Hall, 2008).
Kent Beck and Martin Fowler, Refactoring: Impr