Introduction
Writing clean code is a fundamental skill for every software developer. Clean code not only makes your codebase more maintainable and easier to understand but also fosters collaboration among team members. In this comprehensive article, we will explore what clean code is, why it's important, and provide you with a set of best practices and principles to help you write clean and maintainable code.
What is Clean Code?
Clean code is code that is easy to read, easy to understand, and easy to modify. It is code that is devoid of unnecessary complexity, redundancy, and confusion. Clean code follows a set of conventions and best practices that make it more consistent, making it easier for multiple developers to work on the same project seamlessly.
Why is Clean Code Important?
Readability: Clean code is easy to read, which means that anyone - including your future self - can understand it quickly. This reduces the time required to grasp the code's functionality, leading to faster development and debugging.
Maintainability: Code is read more often than it is written. When you write clean code, it becomes easier to maintain and extend the application over time. This is crucial in the software development lifecycle, where projects often evolve and grow.
Collaboration: Clean code encourages collaboration. When your code is clean and well-organized, other team members can work on it effectively. This makes it easier to divide tasks and work on different parts of the codebase simultaneously.
Bug Reduction: Clean code reduces the likelihood of introducing bugs. Code that is difficult to understand is more prone to errors during modifications or enhancements.
Efficiency: Clean code is efficient code. It typically runs faster and uses fewer resources because it avoids unnecessary operations and complexity.
Now that we understand why clean code is important, let's delve into some best practices and principles to help you write clean code.
Best Practices and Principles for Writing Clean Code
1 . Meaningful Variable and Function Names
Use descriptive names for variables, functions, classes, and other identifiers. A well-chosen name can convey the purpose of the entity, making the code more understandable. Avoid single-letter variable names or cryptic abbreviations.
# Bad variable name
x = 5
# Good variable name
total_score = 5
2 . Keep Functions and Methods Short
Functions and methods should be concise and focused on a single task. The Single Responsibility Principle (SRP) states that a function should do one thing and do it well. Shorter functions are easier to understand, test, and maintain. If a function becomes too long or complex, consider breaking it down into smaller, more manageable functions.
// Long and complex function
function processUserData(user) {
// Many lines of code...
}
// Refactored into smaller functions
function validateUserInput(userInput) {
// Validation logic...
}
function saveUserToDatabase(user) {
// Database operation...
}
3 . Comments and Documentation
Use comments sparingly, and when you do, make them meaningful. Code should be self-explanatory whenever possible. Documentation, such as inline comments and README files, helps other developers understand your code's purpose and usage. Document complex algorithms, non-trivial decisions, and public APIs.
# Bad comment
x = x + 1 # Increment x
# Good comment
# Calculate the total score by incrementing x
total_score = x + 1
4 . Consistent Formatting and Indentation
Adhere to a consistent coding style and indentation. This makes the codebase look clean and organized. Most programming languages have community-accepted coding standards (e.g., PEP 8 for Python, eslint for JavaScript) that you should follow. Consistency also applies to naming conventions, spacing, and code structure.
// Inconsistent formatting
if(condition){
doSomething();
} else {
doSomethingElse();
}
// Consistent formatting
if (condition) {
doSomething();
} else {
doSomethingElse();
}
5 . DRY (Don't Repeat Yourself) Principle
Avoid duplicating code. Repeated code is harder to maintain and increases the risk of inconsistencies. Extract common functionality into functions, methods, or classes to promote code reusability. When you need to make a change, you'll only need to do it in one place.
Suppose you're working on a JavaScript application that calculates the total price of items in a shopping cart. Initially, you have two separate functions for calculating the price of each item type: one for calculating the price of a book and another for calculating the price of a laptop. Here's the initial code:
function calculateBookPrice(quantity, price) {
return quantity * price;
}
function calculateLaptopPrice(quantity, price) {
return quantity * price;
}
While these functions work, they violate the DRY principle because the logic for calculating the total price is repeated for different item types. If you have more item types to calculate, you'll end up duplicating this logic. To follow the DRY principle and improve code maintainability, you can refactor the code as follows:
function calculateItemPrice(quantity, price) {
return quantity * price;
}
const bookQuantity = 3;
const bookPrice = 25;
const laptopQuantity = 2;
const laptopPrice = 800;
const bookTotalPrice = calculateItemPrice(bookQuantity, bookPrice);
const laptopTotalPrice = calculateItemPrice(laptopQuantity, laptopPrice);
In this refactored code, we have a single calculateItemPrice function that calculates the total price for any item type based on the quantity and price provided as arguments. This adheres to the DRY principle because the calculation logic is no longer duplicated.
Now, you can easily calculate the total price for books, laptops, or any other item type by calling calculateItemPrice with the appropriate quantity and price values. This approach promotes code reusability, readability, and maintainability while reducing the risk of errors caused by duplicated code.
6 . Use Meaningful Whitespace
Properly format your code with spaces and line breaks. This enhances readability. Use whitespace to separate logical sections of your code. Well-formatted code is easier to scan, reducing the cognitive load on readers.
// Poor use of whitespace
const sum=function(a,b){return a+b;}
// Improved use of whitespace
const sum = function (a, b) {
return a + b;
}
7 . Error Handling
Handle errors gracefully. Use appropriate try-catch blocks or error-handling mechanisms in your code. This prevents unexpected crashes and provides valuable information for debugging. Don't suppress errors or simply log them without a proper response.
// Inadequate error handling
try {
result = divide(x, y);
} catch (error) {
console.error("An error occurred");
}
// Proper error handling
try {
result = divide(x, y);
} catch (error) {
if (error instanceof ZeroDivisionError) {
console.error("Division by zero error:", error.message);
} else if (error instanceof ValueError) {
console.error("Invalid input:", error.message);
} else {
console.error("An unexpected error occurred:", error.message);
}
}
8 . Testing
Write unit tests to verify your code's correctness. Test-driven development (TDD) can help you write cleaner code by forcing you to consider edge cases and expected behavior upfront. Well-tested code is more reliable and easier to refactor.
// Example using JavaScript and the Jest testing framework
test('addition works correctly', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
9 . Refactoring
Refactor your code regularly. As requirements change and your understanding of the problem domain deepens, adjust your code accordingly. Refactoring helps maintain clean code as the project evolves. Don't be afraid to revisit and improve existing code when necessary.
Suppose you have a function that calculates the total price of items in a shopping cart with a fixed discount percentage:
function calculateTotalPrice(cartItems) {
let totalPrice = 0;
for (const item of cartItems) {
totalPrice += item.price;
}
return totalPrice - (totalPrice * 0.1); // Apply a 10% discount
}
Initially, this function calculates the total price and applies a fixed discount of 10%. However, as the project evolves, you realize that you need to support variable discounts. To refactor the code to make it more flexible, you can introduce a discount parameter:
function calculateTotalPrice(cartItems, discountPercentage) {
if (discountPercentage < 0 || discountPercentage > 100) {
throw new Error("Discount percentage must be between 0 and 100.");
}
let totalPrice = 0;
for (const item of cartItems) {
totalPrice += item.price;
}
const discountAmount = (totalPrice * discountPercentage) / 100;
return totalPrice - discountAmount;
}
In this refactored code:
We have added a discountPercentage parameter to the calculateTotalPrice function, allowing you to specify the discount percentage when calling the function.
We perform validation on the discountPercentage parameter to ensure it falls within a valid range (0 to 100%). If it's not within the range, we throw an error.
The discount calculation is now based on the provided discountPercentage, making the function more flexible and adaptable to changing requirements.
By refactoring the code in this way, you have improved its flexibility and maintainability. You can easily adapt the function to handle different discount scenarios without having to rewrite the entire logic. This demonstrates the importance of regular code refactoring as your project evolves and requirements change.
10 . Version Control
Use version control systems like Git to track changes to your code. This allows you to collaborate effectively with team members, revert to previous versions if necessary, and maintain a clean history of your project's development. Git provides tools for code review, branching, and merging, facilitating collaboration and code cleanliness.
Conclusion
Writing clean code is not just a set of rules but a mindset and a discipline. It's about creating software that is easy to read, maintain, and extend. By following these best practices and principles, you can become a more proficient developer who produces high-quality code.Investing time in meticulously examining fellow engineers' codebases, particularly in open-source projects, can be an enlightening experience. Through this exploration, you gain invaluable insights into diverse coding styles and strategies. This exposure enables you to distill the essence of writing pristine, sustainable codebases.. Remember that clean code is a continuous journey, and with practice, it becomes second nature, leading to more efficient and enjoyable software development.
Top comments (40)
I am not sure what's good in that example, as the code is self-explanatory. :-)
I could see a good example like that:
The comment explains why value needs to be converted and prevents questions like "We don't need to convert value to number here".
Even then I'd refactor it into its own function and call it something like
fixBrokenApiReturnType
.The function name
fixBrokenApiReturnType
does not explain why it needs to be fixed. I'd eventually add a comment with the JIRA (or the like) ticket ID.Don't forget to JSDoc the living **** out that function as well xD
@barrymichaeldoyle Roger that xD
Passive-aggressive function names, love it π (I'm not being sarcastic, seeing people's exasperation come through in their comments/naming always gives me a good chuckle, and I've been known to do the same myself on occasion)
Even in your example, the comment isn't totally necessary. If that's JavaScript, it's clear what's happening. If it's another language, we could just cast to string, and that would be clear from the code. For instance:
int totalUsers = (int) users;
My example code is clear, but only from the code perspective. The question is, why would you convert an incoming value to a number here? And to clarify, an additional comment would help here.
Please give the credit to the author of this content...
As I was reading this, I knew it was directly from Clean Code: A Handbook of Agile Software Craftsmanship... So I pulled the book from my desk and confirmed it
en.m.wikipedia.org/wiki/Robert_C._...
Hi @camco
I appreciate your comment. However, I would like to clarify that the content I provided is a reflection of general knowledge and widely accepted industry practices. It has not been directly copied from any specific source, including 'Clean Code: A Handbook of Agile Software Craftsmanship.'
Now this is a ChatGPT written comment if I ever seen one...
So true π€£
I'd say this is not always true. Just spitting everything into smaller functions can lead to way too much fragmentation and it requires you to jump across several functions and files to grasp the full picture. The same can happen because of the DRY principle.
Sometimes it makes sense to duplicate the same code for a) readability reasons and/or b) because it serves a different purpose and just happen to look the same.
In short: those things lead to premature abstraction (YAGNI is often the better approach)
I conceptually divide the classes into two categories: master and utility.
The master code looks like this:
In this way, just reading the master code you can have a full picture of the flow and what it does.
On the other hand, often I see code like this:
This is a code impossible to read and to maintain
Thank you for your input. You've highlighted a valid point about the potential drawbacks of excessive code fragmentation and strict adherence to DRY. Balancing clean code with readability and avoiding premature abstraction is indeed a nuanced challenge. Context and project requirements often dictate the best approach. Your insights contribute to a valuable discussion on software development principles
Good artical, but your examples are off.
Good comments explain "why" not how or what. Method names explain what. Method bodies explain how. It's often not taught like this as introductory programming books often use comments to explain what some piece of code is doing. Once you can read code you should not need that help.
Changing a method to have a discount argument is not an example of refactoring as it changes behaviour. In TDD, the red, green, refactor cycle, relies on the tests written in red, that you made pass in green. If you add behaviour in refactoring, you won't have a test for it. Refactoring is defined as changing the design or structure without changing the behaviour.
Keep up the work.
Thanks for the feedback, Your insights are valuable.
catch ( ex ) {
console.error(...)
}
This should only be done at the top level of your application, and in most cases it is already being done, eg, by your web container or CLI framework.
If the only thing you can do with an exception is reporting it, then IGNORE it and let the top level deal with reporting. This will do it in the way that was configured for the entire application (eg, it sends it to the app logger).
If you can detect a more specific anomaly, eg, your case where you have something like "Invalid input %s for ", then RETHROW ANOTHER exception, ATTACHING the original one (the stacktrace is very valuable for debugging).
If you can recover from an exception, then do it in the catch and log the workaround as a warning (eg, "Config file not found, using default config"). If you need to interrupt the normal flow, then catch and rethrow or return, DO NOT catch and let your regular code to continue (with bad data, inconsistent/insecure state, etc).
I see wrong ways of dealing with exceptions (empty catch, console.error(), out.println () ) all the time and it's so time-wasting and frustrating.
I have been doing this for 47 years, and clean code has seldom been a fundamental skill.
I submit that if you're properly naming your variables and other structures, that constitutes good commenting. Comments should be rare, because your code should be so readable, you don't need them.
I know you aren't invoking Uncle Bob, here, but I'm going to. He covers that concept right in preface of the book, before he ever digs into the subject.
Thank you Mark,
Very Insightful π₯ππ
Thank you Daniel
well explained article π
Good