Clean code principles are a set of guidelines and best practices for writing code that is easy to read, understand, and maintain. These principles were popularized by Robert C. Martin in his book "Clean Code: A Handbook of Agile Software Craftsmanship.
Let me explain Clean code principles in my way :D
Imagine that you are writing a recipe for a cake. You want the recipe to be easy to follow for anyone, even if they have never baked a cake before. You would start by writing down the ingredients in a clear and concise way. Then, you would explain the steps involved in baking the cake in a simple and straightforward way. You would also include any tips or tricks that you have learned along the way.
Clean code is like a well-written recipe. It is easy to read and understand, and it provides clear instructions for how to use the code. This makes it easier for other developers to work with the code and to make changes to it as needed.
This article is written based on referring Uncle Bob's books.
Though there are few variations I have added. Hope that makes sense.
Here are some of the key clean code principles:
1. Meaningful Names:
Use descriptive and meaningful names for variables, functions, classes, and other code entities.
Avoid cryptic or overly short names that require readers to guess their purpose. This helps us understand the code quickly without any confusion.
Imagine you're writing code for a simple program that calculates the area of a rectangle.
Here's some code without meaningful names:
int a = 5; // Width of the rectangle
int b = 10; // Height of the rectangle
int r = a * b; // Calculating the area
In this code, it's not clear what a, b, and r represent without reading comments or trying to guess. It's like using single-letter names for things you use every day, like having a "T" button on your TV remote that turns on the lights. It's confusing!
Now, let's apply the "Meaningful Names" principle:
int width = 5; // Width of the rectangle
int height = 10; // Height of the rectangle
int area = width * height; // Calculating the area
2. DRY (Don't Repeat Yourself):
Eliminate code duplication by creating reusable abstractions, functions, or classes.
Repeating code can lead to maintenance issues and make the codebase harder to understand. This helps keep your code clean, reduces errors, and makes it easier to maintain.
Let's see the example of "DRY" principle
Imagine you're building a program that calculates the area of different shapes, like rectangles and circles.
Now, without following the "DRY" principle, you might end up writing the same code for calculating areas in multiple places. Here's how it could look:
// Calculating the area of a rectangle
int width = 5;
int height = 10;
int rectangleArea = width * height;
// Calculating the area of a circle
double radius = 3.5;
double circleArea = 3.14 * radius * radius;
// Calculating the area of another rectangle
int anotherWidth = 8;
int anotherHeight = 6;
int anotherRectangleArea = anotherWidth * anotherHeight;
In this code, you're doing the same thing (calculating the area) in multiple places. If you ever need to change how you calculate the area, you'll have to make the same change in multiple spots, which can be error-prone and time-consuming.
Now, let's follow the "DRY" principle and reuse code:
// A function to calculate the area of a rectangle
int CalculateRectangleArea(int width, int height)
{
return width * height;
}
// A function to calculate the area of a circle
double CalculateCircleArea(double radius)
{
return 3.14 * radius * radius;
}
// Calculating the area of a rectangle
int rectangleArea = CalculateRectangleArea(5, 10);
// Calculating the area of a circle
double circleArea = CalculateCircleArea(3.5);
// Calculating the area of another rectangle
int anotherRectangleArea = CalculateRectangleArea(8, 6);
Now, you've defined separate functions (CalculateRectangleArea
and CalculateCircleArea
) for calculating the areas of rectangles and circles. You can reuse these functions whenever you need to calculate an area. If you need to change the area calculation logic, you only have to do it in one place, making your code more maintainable and less error-prone.
Following the "DRY" principle is like having a recipe book with all your favorite recipes in one place. You don't have to rewrite the same recipe every time you want to cook a meal; you just follow the instructions from the book. It saves time and helps you avoid mistakes.
3. Small Functions and Methods:
Keep functions and methods short and focused on a single responsibility.
Functions should ideally do one thing and do it well. This makes your code easier to understand and work with, like having puzzle pieces that fit together neatly.
Let's see an example:
Imagine you're creating a program that calculates the total price of items in a shopping cart.
Without following the "Small Functions and Methods" principle, you might end up with a single, big function that does everything:
double CalculateTotalPrice(List<Item> items)
{
double totalPrice = 0;
foreach (Item item in items)
{
double itemPrice = item.Price;
int quantity = item.Quantity;
// Calculate the item's subtotal
double itemSubtotal = itemPrice * quantity;
// Add the subtotal to the total price
totalPrice += itemSubtotal;
}
// Apply a 10% discount if the total price is over $100
if (totalPrice > 100)
{
totalPrice *= 0.9; // Apply 10% discount
}
return totalPrice;
}
In this code, the CalculateTotalPrice
function is quite long and does several things: it calculates the subtotal for each item, adds them up, and applies a discount if the total is over $100. It's like trying to solve a big, complex puzzle in one go, and it can be hard to understand.
Now, let's follow the "Small Functions and Methods" principle:
double CalculateSubtotal(Item item)
{
double itemPrice = item.Price;
int quantity = item.Quantity;
return itemPrice * quantity;
}
double CalculateTotalPrice(List<Item> items)
{
double totalPrice = 0;
foreach (Item item in items)
{
double itemSubtotal = CalculateSubtotal(item);
totalPrice += itemSubtotal;
}
if (totalPrice > 100)
{
totalPrice *= 0.9;
}
return totalPrice;
}
In this improved code, we've broken the task into smaller functions. CalculateSubtotal
is responsible for, well, calculating the subtotal of an item. CalculateTotalPrice
uses this smaller function to calculate the total price of all items in the cart. It's like solving the puzzle by putting together smaller, easier-to-handle pieces.
Following the "Small Functions and Methods" principle makes your code more readable, easier to test, and simpler to maintain. It's like having puzzle pieces that clearly show how they fit into the bigger picture, making it much easier to complete the puzzle (or understand your code)!
4. Single Responsibility Principle (SRP):
Each class or module should have a single responsibility or reason to change.
Avoid classes or functions that have too many responsibilities." Imagine you're building a car. You don't want your tires to also be responsible for playing music; that's not their job. In coding, it means that each class or function should focus on doing one thing and not try to do everything.
Let's see an example
Imagine you're creating a program to manage a library's book catalog.
Without following the Single Responsibility Principle, you might have a class that does too much:
class BookCatalog
{
List<Book> books;
public BookCatalog()
{
books = new List<Book>();
}
public void AddBook(Book book)
{
// Code to add a book to the catalog
}
public void RemoveBook(Book book)
{
// Code to remove a book from the catalog
}
public void SearchBook(string title)
{
// Code to search for a book by title
}
public void PrintCatalog()
{
// Code to print the entire catalog
}
}
In this code, the BookCatalog
class is responsible for adding, removing, searching for books, and even printing the catalog. It's like if your car's tires were also responsible for playing music, checking the engine, and steering the car. It's confusing and hard to maintain.
Now, let's follow the Single Responsibility Principle:
class BookCatalog
{
private List<Book> books;
public BookCatalog()
{
books = new List<Book>();
}
public void AddBook(Book book)
{
// Code to add a book to the catalog
}
public void RemoveBook(Book book)
{
// Code to remove a book from the catalog
}
public List<Book> SearchBooksByTitle(string title)
{
// Code to search for books by title and return a list
}
}
class CatalogPrinter
{
public void PrintCatalog(List<Book> books)
{
// Code to print the catalog
}
}
In this improved code, we've split the responsibilities. The BookCatalog
class now focuses solely on managing books (adding, removing, searching). The CatalogPrinter
class handles the responsibility of printing the catalog. It's like having a car where the tires do their job of moving the car, and there's a separate system for playing music. Everything has its own job, and it's easier to understand and maintain.
Following the Single Responsibility Principle makes your code more organized, easier to maintain, and less likely to introduce bugs when you make changes. It's like having a well-designed car, where each part does what it's supposed to do, making the whole vehicle work smoothly.
5. Comments and Documentation:
Use comments sparingly and focus on explaining "why" something is done, not "what" it does (code should be self-explanatory).
Maintain up-to-date documentation to help others understand your code.It's like having a cookbook with clear explanations for each recipe step.
Let's explain this principle with an example in C#:
Imagine you're working on a program that calculates the final price of products in a shopping cart.
Without following the Comments and Documentation principle, your code might look like this:
double CalculateFinalPrice(List<Product> cart, double taxRate)
{
double subtotal = 0;
foreach (Product item in cart)
{
double itemPrice = item.Price;
int quantity = item.Quantity;
// Calculate item subtotal
double itemSubtotal = itemPrice * quantity;
// Add item subtotal to the overall subtotal
subtotal += itemSubtotal;
}
// Calculate taxes
double taxAmount = subtotal * taxRate;
// Add taxes to the subtotal to get the final price
double finalPrice = subtotal + taxAmount;
return finalPrice;
}
In this code, you can understand what's happening because the variable names are somewhat descriptive. But there's no extra explanation about why certain calculations are being done or what each part of the code is for.
Now, let's follow the Comments and Documentation principle:
/// <summary>
/// Calculates the final price of products in a shopping cart, including taxes.
/// </summary>
/// <param name="cart">The list of products in the shopping cart.</param>
/// <param name="taxRate">The tax rate as a decimal (e.g., 0.08 for 8%).</param>
/// <returns>The final price, including taxes.</returns>
double CalculateFinalPrice(List<Product> cart, double taxRate)
{
double subtotal = 0;
foreach (Product item in cart)
{
double itemPrice = item.Price;
int quantity = item.Quantity;
// Calculate item subtotal
double itemSubtotal = itemPrice * quantity;
// Add item subtotal to the overall subtotal
subtotal += itemSubtotal;
}
// Calculate taxes
double taxAmount = subtotal * taxRate;
// Add taxes to the subtotal to get the final price
double finalPrice = subtotal + taxAmount;
return finalPrice;
}
In this improved code, we've added comments and documentation using /// comments. These comments explain what the function does, what its parameters mean, and what it returns. It's like having clear instructions in a recipe that tell you what each ingredient is for and how to combine them.
Following the Comments and Documentation principle makes your code much more understandable and user-friendly. It's like having a cookbook with not just a list of ingredients and steps but also explanations for why you're doing each step and what to expect at the end. This helps you, your teammates, and anyone who reads your code to quickly grasp what's going on and why.
6. Formatting and Consistency:
Adopt a consistent coding style and formatting throughout your codebase.
Use consistent indentation, spacing, and naming conventions. Imagine if in a cookbook, each recipe had a different style of writing, fonts, and spacing – it would be confusing. In coding, it means sticking to a consistent style and layout throughout your code.
Let's see an example
Imagine you're writing a program that manages a list of contacts.
Without following the Formatting and Consistency principle, your code might look like this:
class Contact
{
public string name;
public string emailAddress;
public void DisplayContact()
{
Console.WriteLine("Contact Information:");
Console.WriteLine("Name: " + name);
Console.WriteLine("Email: " + emailAddress);
}
}
public class Program
{
public static void Main()
{
Contact contact1 = new Contact();
contact1.name = "Alice";
contact1.emailAddress = "alice@email.com";
Contact contact2 = new Contact();
contact2.name = "Bob";
contact2.emailAddress = "bob@email.com";
contact1.DisplayContact();
contact2.DisplayContact();
}
}
In this code, there's inconsistent formatting: sometimes, variable names start with a lowercase letter, and sometimes they start with an uppercase letter. The spacing and indentation also vary. It's like having a recipe book where each recipe is written in a different font and with different spacing – hard to follow!
Now, let's follow the Formatting and Consistency principle:
class Contact
{
public string Name { get; set; }
public string EmailAddress { get; set; }
public void DisplayContact()
{
Console.WriteLine("Contact Information:");
Console.WriteLine("Name: " + Name);
Console.WriteLine("Email: " + EmailAddress);
}
}
public class Program
{
public static void Main()
{
Contact contact1 = new Contact
{
Name = "Alice",
EmailAddress = "alice@email.com"
};
Contact contact2 = new Contact
{
Name = "Bob",
EmailAddress = "bob@email.com"
};
contact1.DisplayContact();
contact2.DisplayContact();
}
}
In this improved code, we've made sure that the variable names have consistent formatting (using PascalCase for property names) and that there's a consistent level of indentation. It's like having a cookbook with a uniform font and spacing for all recipes – much easier to read and follow.
Following the Formatting and Consistency principle in your code helps you and other developers understand the code more quickly and reduces the chances of making errors due to inconsistent styles. It's like having a cookbook where all the recipes are written in the same style, making it easy for anyone to cook up a delicious program!
7. Avoid Magic Numbers and Strings:
Replace hardcoded values with named constants or variables to improve code readability.
Magic numbers and strings are unclear and error-prone.It's like having labels on items in your kitchen, so you don't have to guess what's in each container; you know exactly what's inside.
Let's see an example
Imagine you're building a simple program that calculates the price of a product with a discount.
Without following the "Avoid Magic Numbers and Strings" principle, your code might look like this:
double CalculateDiscountedPrice(double originalPrice)
{
// Applying a 10% discount
return originalPrice * 0.9;
}
In this code, the number 0.9 is used directly to represent the discount rate. It's a "magic number" because it's not clear why it's 0.9 or what it means. If you need to change the discount rate later, you'd have to search for all occurrences of 0.9 in your code and update them. It's like having unmarked containers in your kitchen – you'd have to open each one to figure out what's inside.
Now, let's follow the "Avoid Magic Numbers and Strings" principle:
const double DiscountRate = 0.9;
double CalculateDiscountedPrice(double originalPrice)
{
// Applying a 10% discount using the named constant
return originalPrice * DiscountRate;
}
In this improved code, we've defined a constant named DiscountRate and given it a clear, meaningful name. Now, it's easy to see that a 10% discount is applied because the code uses DiscountRate instead of the mysterious 0.9. If you ever need to change the discount rate, you only need to update it in one place (the constant). It's like having labels on containers in your kitchen, so you know exactly what's inside each one.
Following the "Avoid Magic Numbers and Strings" principle makes your code more understandable, maintainable, and less error-prone. It's like having a well-organized kitchen where everything is labeled, so you can quickly find what you need and avoid any cooking mishaps!
8. SOLID Principles:
Follow the SOLID principles (Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion) to create maintainable and extensible code.
(I will write the complete article for this soon)
9. Error Handling:
Implement proper error handling and avoid using exceptions for control flow.
Provide meaningful error messages and handle exceptions gracefully.Imagine you're cooking, and you accidentally spill some salt. You don't panic; instead, you have a plan to clean it up. In coding, it means handling unexpected situations or errors gracefully to prevent your program from crashing or behaving unpredictably.
Let's see an example
Imagine you're writing a program that divides two numbers provided by the user.
Without proper error handling, your code might look like this:
static void Main()
{
Console.WriteLine("Enter the numerator: ");
int numerator = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("Enter the denominator: ");
int denominator = Convert.ToInt32(Console.ReadLine());
int result = numerator / denominator;
Console.WriteLine("Result: " + result);
}
In this code, if the user enters 0
as the denominator, it will cause a "Divide by zero" error, and your program will crash. It's like not having any plan for handling a spilled salt shaker – things can get messy.
Now, let's follow the Error Handling principle:
static void Main()
{
Console.WriteLine("Enter the numerator: ");
int numerator;
if (int.TryParse(Console.ReadLine(), out numerator))
{
Console.WriteLine("Enter the denominator: ");
int denominator;
if (int.TryParse(Console.ReadLine(), out denominator) && denominator != 0)
{
int result = numerator / denominator;
Console.WriteLine("Result: " + result);
}
else
{
Console.WriteLine("Invalid denominator. Please enter a non-zero integer.");
}
}
else
{
Console.WriteLine("Invalid numerator. Please enter a valid integer.");
}
}
In this improved code, we've added error handling. We check if the user's input can be converted to integers (int.TryParse
). If the conversion fails or the denominator is zero, we display an error message instead of trying to perform a dangerous division. It's like having a plan to quickly clean up spilled salt, so it doesn't ruin your meal.
Following the Error Handling principle makes your code more robust and user-friendly. It ensures that your program doesn't crash or produce unexpected results when something goes wrong. It's like being prepared for accidents in the kitchen, so they don't ruin your cooking experience!
10. Testing:
Write unit tests for your code to ensure correctness and maintainability.
Use test-driven development (TDD) principles when appropriate.
11. Refactoring:
Continuously refactor your code to improve its structure and maintainability.
Refactoring should not introduce new functionality but should make the code cleaner.
12. Minimize Function/Method Arguments:
Avoid functions or methods with too many arguments. Aim for simplicity.
If a function requires many parameters, consider encapsulating them in an object.
13. Keep Code Modules Small:
Avoid creating excessively large classes or modules.
Break large codebases into smaller, manageable components.
14. Limit Nesting and Indentation:
Reduce deep nesting of conditional statements and loops.
Limit the level of indentation to improve code readability.
15. Code Reviews:
Collaborate with team members through code reviews to catch issues and ensure adherence to clean code principles.
16. Continuous Learning:
Stay updated with industry best practices and continue learning to improve your coding skills.
17. KISS (Keep It Simple, Stupid): Simplicity is key. Code should be as simple as possible while still achieving its intended purpose. Complex solutions can introduce unnecessary bugs and make code harder to maintain.Think of it as making a peanut butter and jelly sandwich – you don't need a 10-step recipe; you keep it simple and straightforward.
Let's see an example
Imagine you're building a program to check if a given number is even.
Here's a simple and straightforward approach following the KISS principle:
bool IsEven(int number)
{
return number % 2 == 0;
}
In this code, we use the modulo operator % to check if the number is divisible by 2. If the remainder is 0, it's even; otherwise, it's not. It's a straightforward and simple way to determine if a number is even.
Now, consider a more complex and unnecessary approach:
bool IsEven(int number)
{
if (number < 0)
{
throw new ArgumentException("Negative numbers are not supported.");
}
string numberAsString = number.ToString();
int lastDigit = int.Parse(numberAsString[numberAsString.Length - 1].ToString());
if (lastDigit == 0 || lastDigit == 2 || lastDigit == 4 || lastDigit == 6 || lastDigit == 8)
{
return true;
}
else
{
return false;
}
}
In this code, we're converting the number to a string, extracting the last digit, and checking if it's in the set {0, 2, 4, 6, 8}. This approach is unnecessarily complicated and less intuitive compared to the simple modulo-based check. It's like creating a Rube Goldberg machine to make a peanut butter and jelly sandwich when a knife and two slices of bread would do the job just fine.
Following the KISS principle ensures that your code is easier to understand, maintain, and less prone to bugs. It's like choosing the straightforward path to make your sandwich – it gets the job done without unnecessary complexity. In programming, simpler solutions are often more elegant and efficient.
18. YAGNI (You Ain't Gonna Need It): Don't add functionality or code that you anticipate needing in the future but don't currently need. This helps keep the codebase lean and focused on the present requirements. It's similar to not packing extra clothes for a weekend trip when you know you won't need them.
Let's see an example
Imagine you're building a simple to-do list application.
Following the YAGNI principle, you start with a basic class to represent a task:
public class Task
{
public string Description { get; set; }
public bool IsCompleted { get; set; }
public Task(string description)
{
Description = description;
IsCompleted = false;
}
}
In this example, you have a class to represent a task with a description and a completion status. It's simple and does exactly what you need for your to-do list application at the moment.
Now, let's say you start thinking about the future and decide to add a due date property to your task class, even though your current application doesn't use due dates:
public class Task
{
public string Description { get; set; }
public bool IsCompleted { get; set; }
public DateTime DueDate { get; set; }
public Task(string description)
{
Description = description;
IsCompleted = false;
DueDate = DateTime.Now; // Default to the current date and time
}
}
While adding a due date might seem like a good idea for future functionality, it violates the YAGNI principle because you don't have a specific requirement for it in your current to-do list application. It introduces unnecessary complexity and potential for bugs related to due dates that you don't currently need.
By following the YAGNI principle, you keep your code simple and focused on the immediate requirements of your project. If you do find a need for due dates in the future, you can add them when they become necessary. Until then, you avoid unnecessary work and complexity, making your code easier to maintain and understand. It's like packing only what you need for your trip, leaving the extra clothes at home until you have a specific reason to bring them.
19. Separation of Concerns (SoC): Divide your code into separate modules or classes, each with a specific responsibility. This makes the code easier to understand and maintain.
20. High Cohesion and Low Coupling: Aim for high cohesion within modules (i.e., the elements within a module should be closely related) and low coupling between modules (i.e., modules should have minimal dependencies on each other).
Let's break down each principle and provide an example
High Cohesion:
High Cohesion means that the elements (such as functions or classes) within a module or component should be closely related to each other and focused on a single responsibility or purpose.
A module with high cohesion has well-defined and clear functionality.
Example in C#:
Imagine you're building a class to represent a bank account. In a well-cohesive design, this class should contain methods and properties related to banking operations, such as deposit, withdraw, and checking the balance. Here's an example with high
cohesion:
public class BankAccount
{
public decimal Balance { get; private set; }
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= Balance)
{
Balance -= amount;
}
else
{
Console.WriteLine("Insufficient funds.");
}
}
}
In this example, the BankAccount class has high cohesion because it contains methods and properties related to managing a bank account. It doesn't have unrelated functionality, such as sending emails or performing complex mathematical calculations.
Low Coupling:
Low Coupling refers to the degree of dependence or connection between different modules or components within a system.
A design with low coupling ensures that changes in one module have minimal impact on other modules.
Example in C#:
Consider a scenario where your bank account class needs to send email notifications when certain events occur. In a design with low coupling, you would separate the email sending functionality into a separate class or module. Here's an example:
public class BankAccount
{
public decimal Balance { get; private set; }
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= Balance)
{
Balance -= amount;
}
else
{
Console.WriteLine("Insufficient funds.");
}
}
}
public class EmailNotifier
{
public void SendNotification(string message, string emailAddress)
{
// Code to send an email notification
Console.WriteLine($"Sending email to {emailAddress}: {message}");
}
}
By separating email notifications into the EmailNotifier
class, you achieve low coupling. Changes to the email notification logic won't affect the core functionality of the BankAccount
class, and vice versa.
In summary, high cohesion and low coupling are essential design principles in software development. High cohesion ensures that individual components have a clear and focused purpose, while low coupling reduces the interdependencies between components, making your code more modular and maintainable. These principles help create flexible and robust software systems.
Top comments (2)
@ramyatantry You will like it. Kind of version 2 of Anti-patterns
For me personally, the biggest polluters in my code are temporary and test methods. Often, I write a piece of code as a temporary function, but once it functions correctly, I tend to leave it as is and don't revisit it, even though I acknowledge that I should have rewritten it much more effectively.