Introduction
Clean code is code that is easy to read, understand, and maintain. It’s not just about making code work; it’s about crafting code that other developers (including your future self) can easily comprehend and modify. Writing clean code is a discipline that requires constant practice and attention to detail.
Robert C. Martin (Uncle Bob) popularized the concept of clean code through his influential book “Clean Code: A Handbook of Agile Software Craftsmanship”. The principles outlined in his work have become fundamental guidelines for professional software development.
This comprehensive guide explores clean code principles, naming conventions, function design, error handling, class design, and how these concepts integrate with SOLID principles. We’ll examine practical examples in multiple programming languages to demonstrate how to write maintainable, high-quality code.
Core Principles of Clean Code
The Boy Scout Rule
Leave the code cleaner than you found it. Every time you touch a file, make it slightly better. Fix a name, break up a long function, eliminate a small bit of duplication.
The Principle of Least Surprise
Code should do what you expect it to do. If a function is named calculateTotal(), it should calculate a total, not also save it to a database or send an email.
YAGNI (You Aren’t Gonna Need It)
Don’t implement functionality until you actually need it. Avoid speculative coding that adds complexity without immediate value.
DRY (Don’t Repeat Yourself)
Every piece of knowledge should have a single, unambiguous representation in the system. Duplication is waste and increases maintenance burden.
KISS (Keep It Simple, Stupid)
Simplicity should be a key goal in design. Avoid unnecessary complexity. The simpler the solution, the easier it is to understand and maintain.
Meaningful Names
Use Intention-Revealing Names
Names should reveal intent without requiring comments:
1
2
3
4
5
| # Bad
d = 86400 # seconds in a day
# Good
SECONDS_PER_DAY = 86400
|
1
2
3
4
5
6
7
8
9
| // Bad
function getData() {
// ...
}
// Good
function fetchActiveUserProfiles() {
// ...
}
|
1
2
3
4
5
| // Bad
var list = new List<int>();
// Good
var customerIds = new List<int>();
|
Don’t use names that mislead or provide false clues:
1
2
3
4
5
6
7
8
9
10
11
| # Bad - accounts_list might not actually be a list
accounts_list = {
'user1': Account(),
'user2': Account()
}
# Good
accounts_by_username = {
'user1': Account(),
'user2': Account()
}
|
1
2
3
4
5
6
7
8
9
| // Bad - suggests it returns multiple items
function getUsers() {
return database.findOne({ id: userId });
}
// Good
function getUser(userId) {
return database.findOne({ id: userId });
}
|
Make Meaningful Distinctions
Don’t add noise words or use arbitrary distinctions:
1
2
3
4
5
6
7
| // Bad - What's the difference?
public class ProductInfo { }
public class ProductData { }
// Good - Clear distinction
public class Product { }
public class ProductRepository { }
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Bad
public void CopyChars(char[] a1, char[] a2)
{
for (int i = 0; i < a1.Length; i++)
{
a2[i] = a1[i];
}
}
// Good
public void CopyChars(char[] source, char[] destination)
{
for (int i = 0; i < source.Length; i++)
{
destination[i] = source[i];
}
}
|
Use Pronounceable Names
If you can’t pronounce it, you can’t discuss it without sounding like an idiot:
1
2
3
4
5
| # Bad
genymdhms = datetime.now()
# Good
generation_timestamp = datetime.now()
|
1
2
3
4
5
6
7
8
9
| // Bad
const DtaRcrd102 = {
pszqint: '102'
};
// Good
const customerRecord = {
customerId: '102'
};
|
Use Searchable Names
Single-letter names and numeric constants are hard to locate across a codebase:
1
2
3
4
5
6
7
8
9
10
11
| # Bad
for i in range(34):
s += (t[i] * 4) / 5
# Good
WORK_DAYS_PER_WEEK = 5
HOURS_PER_DAY = 8
for day_index in range(DAYS_IN_PERIOD):
day_total = working_hours[day_index] * HOURS_PER_DAY
sum_of_hours += day_total / WORK_DAYS_PER_WEEK
|
Class Names
Classes and objects should have noun or noun phrase names:
1
2
3
4
5
6
7
8
9
| // Good
public class Customer { }
public class Account { }
public class AddressParser { }
// Bad
public class Manager { } // Too generic
public class Process { } // Verb, not noun
public class Data { } // Meaningless
|
Method Names
Methods should have verb or verb phrase names:
1
2
3
4
5
6
7
8
9
| // Good
function saveCustomer() { }
function deleteAccount() { }
function isValidEmail() { }
// Good - accessors, mutators, predicates
function getCustomerName() { }
function setCustomerName(name) { }
function isActive() { }
|
Avoid Mental Mapping
Readers shouldn’t have to mentally translate your names:
1
2
3
4
5
6
7
8
9
10
11
| # Bad
for i in customer_list:
for j in i.orders:
for k in j.items:
total += k.price
# Good
for customer in customer_list:
for order in customer.orders:
for item in order.items:
total += item.price
|
Domain-Specific Names
Use names from the solution or problem domain:
1
2
3
4
5
6
7
| // Good - uses domain terminology
public class ShoppingCart
{
public void AddItem(Product product, int quantity) { }
public decimal CalculateTotal() { }
public void ApplyCoupon(Coupon coupon) { }
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Good - uses mathematical/algorithm terminology
def binary_search(sorted_array, target):
left = 0
right = len(sorted_array) - 1
while left <= right:
mid = (left + right) // 2
if sorted_array[mid] == target:
return mid
elif sorted_array[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
|
Functions and Methods
Small Functions
Functions should be small. Really small. Ideally, they should do one thing and do it well:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| # Bad - doing too much
def process_order(order):
# Validate order
if not order.items:
raise ValueError("Order must have items")
# Calculate total
total = sum(item.price * item.quantity for item in order.items)
# Apply discount
if order.customer.is_premium:
total *= 0.9
# Update inventory
for item in order.items:
product = Product.get(item.product_id)
product.stock -= item.quantity
product.save()
# Save order
order.total = total
order.status = "processed"
order.save()
# Send confirmation
send_email(order.customer.email, "Order Confirmation", f"Total: ${total}")
return order
# Good - single responsibility per function
def process_order(order):
validate_order(order)
total = calculate_order_total(order)
update_inventory(order)
finalize_order(order, total)
send_order_confirmation(order)
return order
def validate_order(order):
if not order.items:
raise ValueError("Order must have items")
def calculate_order_total(order):
subtotal = sum(item.price * item.quantity for item in order.items)
return apply_customer_discount(subtotal, order.customer)
def apply_customer_discount(amount, customer):
if customer.is_premium:
return amount * 0.9
return amount
def update_inventory(order):
for item in order.items:
decrease_product_stock(item.product_id, item.quantity)
def finalize_order(order, total):
order.total = total
order.status = "processed"
order.save()
def send_order_confirmation(order):
message = f"Your order #{order.id} totaling ${order.total} has been processed"
send_email(order.customer.email, "Order Confirmation", message)
|
Do One Thing
Functions should do one thing, do it well, and do it only:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // Bad - multiple responsibilities
function getUserData(userId) {
const user = database.findUser(userId);
// Log the access
logger.log(`User ${userId} accessed at ${new Date()}`);
// Update last access time
user.lastAccessTime = new Date();
database.save(user);
// Format for display
return {
displayName: `${user.firstName} ${user.lastName}`,
email: user.email,
formattedDate: formatDate(user.createdAt)
};
}
// Good - separated concerns
function getUser(userId) {
return database.findUser(userId);
}
function logUserAccess(userId) {
logger.log(`User ${userId} accessed at ${new Date()}`);
}
function updateLastAccessTime(userId) {
const user = getUser(userId);
user.lastAccessTime = new Date();
database.save(user);
}
function formatUserForDisplay(user) {
return {
displayName: `${user.firstName} ${user.lastName}`,
email: user.email,
formattedDate: formatDate(user.createdAt)
};
}
|
One Level of Abstraction
All statements in a function should be at the same level of abstraction:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Bad - mixing abstraction levels
public void ProcessPayment(Order order)
{
// High level
ValidateOrder(order);
// Low level - direct SQL
var connection = new SqlConnection("connectionString");
connection.Open();
var command = new SqlCommand("UPDATE Inventory SET Stock = Stock - @quantity", connection);
command.ExecuteNonQuery();
// High level
ChargeCustomer(order);
}
// Good - consistent abstraction level
public void ProcessPayment(Order order)
{
ValidateOrder(order);
UpdateInventory(order);
ChargeCustomer(order);
SendConfirmation(order);
}
|
Function Arguments
The ideal number of arguments is zero. Next is one, followed closely by two. Three should be avoided where possible. More than three requires very special justification:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # Bad - too many arguments
def create_user(username, email, password, first_name, last_name,
age, country, city, postal_code, phone):
pass
# Good - use an object/dict
def create_user(user_data):
pass
# Or use a data class
from dataclasses import dataclass
@dataclass
class UserRegistrationData:
username: str
email: str
password: str
first_name: str
last_name: str
age: int
country: str
city: str
postal_code: str
phone: str
def create_user(registration_data: UserRegistrationData):
pass
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Bad - boolean flags indicate doing multiple things
function bookHotel(hotelId, checkIn, checkOut, isVIP, sendEmail, updateCalendar) {
// Different logic based on flags
}
// Good - split into separate functions
function bookHotelForRegularCustomer(hotelId, checkIn, checkOut) {
const booking = createBooking(hotelId, checkIn, checkOut);
return booking;
}
function bookHotelForVIPCustomer(hotelId, checkIn, checkOut) {
const booking = createBooking(hotelId, checkIn, checkOut);
applyVIPBenefits(booking);
return booking;
}
|
No Side Effects
Functions should not have hidden side effects. If a function changes state, it should be clear from the name:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # Bad - hidden side effect
def check_password(username, password):
user = User.find_by_username(username)
if user and user.password == hash_password(password):
# Hidden side effect - creates session
Session.initialize(user)
return True
return False
# Good - explicit about side effects
def authenticate_user(username, password):
user = User.find_by_username(username)
if user and user.password == hash_password(password):
Session.initialize(user)
return True
return False
# Or separate concerns
def check_password(username, password):
user = User.find_by_username(username)
return user and user.password == hash_password(password)
def login_user(username, password):
if check_password(username, password):
user = User.find_by_username(username)
Session.initialize(user)
return True
return False
|
Command Query Separation
Functions should either do something or answer something, not both:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // Bad - does and queries
public bool SetAttribute(string name, string value)
{
if (attributeExists(name))
{
setAttribute(name, value);
return true;
}
return false;
}
// Usage is confusing
if (SetAttribute("username", "john")) // Setting or checking?
{
// ...
}
// Good - separate command and query
public bool AttributeExists(string name)
{
return attributes.ContainsKey(name);
}
public void SetAttribute(string name, string value)
{
if (!AttributeExists(name))
{
throw new ArgumentException($"Attribute {name} does not exist");
}
attributes[name] = value;
}
// Clear usage
if (AttributeExists("username"))
{
SetAttribute("username", "john");
}
|
Prefer Exceptions to Error Codes
Using exceptions instead of error codes separates error handling from the main logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // Bad - error codes clutter the logic
function deleteUser(userId) {
const user = getUser(userId);
if (user === null) {
return ERROR_USER_NOT_FOUND;
}
const result = database.delete(user);
if (result === ERROR_DATABASE_FAILURE) {
return ERROR_DATABASE_FAILURE;
}
const logResult = logger.log(`Deleted user ${userId}`);
if (logResult === ERROR_LOG_FAILURE) {
return ERROR_LOG_FAILURE;
}
return SUCCESS;
}
// Usage is messy
const result = deleteUser(userId);
if (result === ERROR_USER_NOT_FOUND) {
// handle
} else if (result === ERROR_DATABASE_FAILURE) {
// handle
}
// Good - exceptions separate error handling
function deleteUser(userId) {
const user = getUser(userId); // throws UserNotFoundError
database.delete(user); // throws DatabaseError
logger.log(`Deleted user ${userId}`); // throws LogError
}
// Clean usage
try {
deleteUser(userId);
console.log('User deleted successfully');
} catch (error) {
handleError(error);
}
|
Try/catch blocks are ugly. Extract the bodies of try and catch blocks into functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| # Bad - logic mixed with error handling
def delete_page(page):
try:
registry = get_registry()
config_keys = registry.get_config_keys()
for key in config_keys:
if key.matches(page.name):
registry.delete_reference(key)
page.delete()
except Exception as e:
logger.log(str(e))
# Good - separated concerns
def delete_page(page):
try:
delete_page_and_references(page)
except Exception as e:
log_error(e)
def delete_page_and_references(page):
delete_config_references(page)
page.delete()
def delete_config_references(page):
registry = get_registry()
config_keys = registry.get_config_keys()
for key in config_keys:
if key.matches(page.name):
registry.delete_reference(key)
def log_error(error):
logger.log(str(error))
|
Don’t comment bad code—rewrite it:
1
2
3
4
5
6
| // Bad - comment explaining messy code
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
// Good - self-explanatory code
if (employee.isEligibleForFullBenefits())
|
Explain Yourself in Code
1
2
3
4
5
6
| // Bad - comment needed to explain
// Check if user has administrative privileges
if (user.Role == 1 && user.Status == 2)
// Good - no comment needed
if (user.IsAdministrator && user.IsActive)
|
Some comments are necessary and beneficial:
1
2
| # Copyright (c) 2024 Company Name
# Licensed under the MIT License
|
1
2
3
4
5
6
7
| // Returns an instance of the Responder being tested
function responderInstance() {
return new Responder();
}
// Format: kk:mm:ss EEE, MMM dd, yyyy
const timeMatcher = /\d{2}:\d{2}:\d{2} \w{3}, \w{3} \d{2}, \d{4}/;
|
Explanation of Intent
1
2
3
| // We decided to use a thread pool here because the number of
// concurrent requests can spike significantly during peak hours
var threadPool = new ThreadPool(maxThreads: 100);
|
Warning of Consequences
1
2
3
4
5
6
7
| # Don't run this test unless you have at least 2GB of free memory
def test_large_dataset_processing():
pass
# This will take approximately 10 minutes to complete
def rebuild_search_index():
pass
|
1
2
3
4
5
| // TODO: Implement caching layer to improve performance
// TODO: Add validation for negative values
function calculateDiscount(amount) {
return amount * 0.1;
}
|
Mumbling
1
2
3
4
| # Bad - unclear what this means
def load_properties():
# Now load properties
properties = load_from_file()
|
1
2
3
4
5
6
7
8
| // Bad - comment adds no value
// Returns the day of the month
public int getDayOfMonth() {
return dayOfMonth;
}
// The customer object
private Customer customer;
|
1
2
3
4
5
6
7
8
| // Bad - comment is incorrect
// This method processes the order and returns true if successful
public bool ProcessOrder(Order order)
{
ProcessPayment(order);
UpdateInventory(order);
// Actually doesn't return anything about success!
}
|
1
2
3
4
5
6
| # Bad - delete it, version control remembers
def calculate_total(items):
total = sum(item.price for item in items)
# tax = total * 0.08
# total = total + tax
return total
|
For public APIs, use proper documentation comments:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| def calculate_shipping_cost(weight, distance, priority):
"""
Calculate shipping cost based on package weight and delivery distance.
Args:
weight (float): Package weight in kilograms
distance (float): Delivery distance in kilometers
priority (str): Shipping priority ('standard', 'express', 'overnight')
Returns:
float: Calculated shipping cost in dollars
Raises:
ValueError: If weight or distance is negative
InvalidPriorityError: If priority is not recognized
Examples:
>>> calculate_shipping_cost(2.5, 100, 'standard')
15.50
"""
if weight < 0 or distance < 0:
raise ValueError("Weight and distance must be positive")
base_cost = weight * 2 + distance * 0.1
multipliers = {
'standard': 1.0,
'express': 1.5,
'overnight': 2.0
}
if priority not in multipliers:
raise InvalidPriorityError(f"Unknown priority: {priority}")
return base_cost * multipliers[priority]
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| /**
* Validates an email address format
*
* @param {string} email - The email address to validate
* @returns {boolean} True if email format is valid, false otherwise
*
* @example
* isValidEmail('user@example.com') // returns true
* isValidEmail('invalid-email') // returns false
*/
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| /// <summary>
/// Processes a refund for a given order
/// </summary>
/// <param name="orderId">The unique identifier of the order</param>
/// <param name="amount">The refund amount (must not exceed order total)</param>
/// <param name="reason">The reason for the refund</param>
/// <returns>A RefundResult containing transaction details</returns>
/// <exception cref="OrderNotFoundException">Thrown when order is not found</exception>
/// <exception cref="InvalidRefundAmountException">Thrown when amount exceeds order total</exception>
public RefundResult ProcessRefund(string orderId, decimal amount, string reason)
{
// Implementation
}
|
Error Handling
Use Exceptions Rather Than Return Codes
Exceptions separate error handling from the main algorithm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| # Bad - error codes mixed with logic
def withdraw_money(account, amount):
if account.balance < amount:
return ERROR_INSUFFICIENT_FUNDS
if amount < 0:
return ERROR_INVALID_AMOUNT
account.balance -= amount
return SUCCESS
# Usage is messy
result = withdraw_money(account, 100)
if result == ERROR_INSUFFICIENT_FUNDS:
print("Insufficient funds")
elif result == ERROR_INVALID_AMOUNT:
print("Invalid amount")
# Good - exceptions for errors
def withdraw_money(account, amount):
if amount < 0:
raise ValueError("Amount must be positive")
if account.balance < amount:
raise InsufficientFundsError("Account balance too low")
account.balance -= amount
# Clean usage
try:
withdraw_money(account, 100)
except ValueError as e:
print(f"Invalid input: {e}")
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")
|
Write Your Try-Catch-Finally Statement First
Define the scope of error handling early:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Good - try-catch defines transaction scope
function processPayment(paymentData) {
let transaction;
try {
transaction = beginTransaction();
validatePaymentData(paymentData);
const charge = processCharge(paymentData);
updateAccount(charge);
commitTransaction(transaction);
return charge;
} catch (error) {
if (transaction) {
rollbackTransaction(transaction);
}
throw new PaymentProcessingError('Payment failed', error);
} finally {
if (transaction) {
closeTransaction(transaction);
}
}
}
|
Use Custom Exception Classes
Create meaningful exception hierarchies:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| # Good - custom exception hierarchy
class ApplicationError(Exception):
"""Base exception for application errors"""
pass
class ValidationError(ApplicationError):
"""Raised when validation fails"""
pass
class AuthenticationError(ApplicationError):
"""Raised when authentication fails"""
pass
class AuthorizationError(ApplicationError):
"""Raised when user lacks permissions"""
pass
class ResourceNotFoundError(ApplicationError):
"""Raised when a requested resource is not found"""
def __init__(self, resource_type, resource_id):
self.resource_type = resource_type
self.resource_id = resource_id
super().__init__(f"{resource_type} with id {resource_id} not found")
# Usage
def get_user(user_id):
user = database.find_user(user_id)
if not user:
raise ResourceNotFoundError("User", user_id)
return user
try:
user = get_user(123)
except ResourceNotFoundError as e:
logger.error(f"Resource error: {e.resource_type} {e.resource_id}")
except AuthenticationError:
redirect_to_login()
except ApplicationError as e:
logger.error(f"Application error: {e}")
|
Provide Context with Exceptions
Include enough information to diagnose the problem:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Bad - minimal context
throw new Exception("Failed");
// Good - rich context
throw new OrderProcessingException(
$"Failed to process order {order.Id} for customer {order.CustomerId}",
order,
innerException
)
{
OrderId = order.Id,
CustomerId = order.CustomerId,
ErrorCode = "ORD_001",
Timestamp = DateTime.UtcNow
};
|
Don’t Return Null
Returning null creates unnecessary checks:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Bad - requires null checks everywhere
function getUsers() {
const users = database.query('SELECT * FROM users');
return users.length > 0 ? users : null;
}
const users = getUsers();
if (users !== null) { // Always need to check
users.forEach(user => console.log(user.name));
}
// Good - return empty collection
function getUsers() {
return database.query('SELECT * FROM users') || [];
}
const users = getUsers();
users.forEach(user => console.log(user.name)); // No null check needed
// Or use Optional/Maybe pattern
function getUser(userId) {
const user = database.findOne(userId);
return Optional.ofNullable(user);
}
const user = getUser(123);
user.ifPresent(u => console.log(u.name));
|
Don’t Pass Null
Avoid passing null as arguments:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| # Bad - accepts null
def calculate_discount(order, coupon):
if coupon is None:
return 0
return order.total * coupon.discount_rate
# Usage requires None checks
discount = calculate_discount(order, None)
# Good - explicit methods
def calculate_discount(order, coupon):
return order.total * coupon.discount_rate
def calculate_discount_without_coupon(order):
return 0
# Or use optional parameters with defaults
def calculate_discount(order, coupon=None):
if coupon is None:
return 0
return order.total * coupon.discount_rate
# Or better - use Null Object pattern
class NoCoupon:
discount_rate = 0
def calculate_discount(order, coupon):
return order.total * coupon.discount_rate
# Usage doesn't need None checks
discount = calculate_discount(order, NoCoupon())
|
Class Design
Classes Should Be Small
Like functions, classes should be small. We measure class size by responsibilities:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // Bad - too many responsibilities
public class UserManager
{
public void CreateUser(User user) { }
public void DeleteUser(int userId) { }
public void SendEmail(string to, string message) { }
public void LogActivity(string activity) { }
public void ValidatePassword(string password) { }
public void EncryptData(string data) { }
public void GenerateReport() { }
public void BackupDatabase() { }
}
// Good - single responsibility
public class UserRepository
{
public void Create(User user) { }
public void Delete(int userId) { }
public User GetById(int userId) { }
}
public class EmailService
{
public void Send(string to, string message) { }
}
public class ActivityLogger
{
public void Log(string activity) { }
}
public class PasswordValidator
{
public bool IsValid(string password) { }
}
|
Single Responsibility Principle
A class should have one and only one reason to change:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| # Bad - multiple responsibilities
class Employee:
def __init__(self, name, role, salary):
self.name = name
self.role = role
self.salary = salary
def calculate_pay(self):
# Accounting responsibility
return self.salary + self.calculate_bonus()
def save(self):
# Database responsibility
database.save(self)
def generate_report(self):
# Reporting responsibility
return f"Employee Report: {self.name}, {self.role}"
# Good - separated responsibilities
class Employee:
def __init__(self, name, role, salary):
self.name = name
self.role = role
self.salary = salary
class PayrollCalculator:
def calculate_pay(self, employee):
return employee.salary + self.calculate_bonus(employee)
def calculate_bonus(self, employee):
# Bonus calculation logic
pass
class EmployeeRepository:
def save(self, employee):
database.save(employee)
def get_by_id(self, employee_id):
return database.find(employee_id)
class EmployeeReportGenerator:
def generate_report(self, employee):
return f"Employee Report: {employee.name}, {employee.role}"
|
Cohesion
Classes should have high cohesion—their methods and fields should be related:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| // Bad - low cohesion
class EmployeeReporter {
constructor() {
this.employee = null;
this.database = null;
this.emailService = null;
this.logger = null;
}
generateReport(employeeId) {
// Only uses employee
this.employee = this.database.findEmployee(employeeId);
return `Report for ${this.employee.name}`;
}
sendEmail(to, message) {
// Only uses emailService
this.emailService.send(to, message);
}
logActivity(message) {
// Only uses logger
this.logger.log(message);
}
}
// Good - high cohesion
class EmployeeReporter {
constructor(employee) {
this.employee = employee;
}
generateReport() {
return {
name: this.employee.name,
role: this.employee.role,
salary: this.employee.salary,
performanceRating: this.calculatePerformanceRating(),
tenure: this.calculateTenure()
};
}
calculatePerformanceRating() {
// Uses employee data
return this.employee.performanceScore / 10;
}
calculateTenure() {
// Uses employee data
const startDate = new Date(this.employee.hireDate);
const now = new Date();
return (now - startDate) / (1000 * 60 * 60 * 24 * 365);
}
}
|
Organizing for Change
Structure classes to minimize the impact of changes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| # Bad - hard to extend
class ShippingCalculator:
def calculate(self, order, shipping_type):
if shipping_type == "standard":
return order.weight * 1.5
elif shipping_type == "express":
return order.weight * 3.0
elif shipping_type == "overnight":
return order.weight * 5.0
else:
raise ValueError("Unknown shipping type")
# Good - open for extension, closed for modification
from abc import ABC, abstractmethod
class ShippingStrategy(ABC):
@abstractmethod
def calculate(self, order):
pass
class StandardShipping(ShippingStrategy):
def calculate(self, order):
return order.weight * 1.5
class ExpressShipping(ShippingStrategy):
def calculate(self, order):
return order.weight * 3.0
class OvernightShipping(ShippingStrategy):
def calculate(self, order):
return order.weight * 5.0
class ShippingCalculator:
def __init__(self, strategy: ShippingStrategy):
self.strategy = strategy
def calculate(self, order):
return self.strategy.calculate(order)
# Easy to add new shipping types without modifying existing code
class SameDayShipping(ShippingStrategy):
def calculate(self, order):
return order.weight * 7.0
|
Refactoring Techniques
Break long methods into smaller, well-named ones:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
| // Before
public void PrintInvoice(Order order)
{
Console.WriteLine("Invoice");
Console.WriteLine("--------");
// Print customer details
Console.WriteLine($"Customer: {order.Customer.Name}");
Console.WriteLine($"Address: {order.Customer.Address}");
Console.WriteLine($"Phone: {order.Customer.Phone}");
Console.WriteLine();
// Print items
foreach (var item in order.Items)
{
Console.WriteLine($"{item.Name} x {item.Quantity} = ${item.Price * item.Quantity}");
}
Console.WriteLine();
// Calculate totals
decimal subtotal = 0;
foreach (var item in order.Items)
{
subtotal += item.Price * item.Quantity;
}
decimal tax = subtotal * 0.08m;
decimal total = subtotal + tax;
Console.WriteLine($"Subtotal: ${subtotal}");
Console.WriteLine($"Tax: ${tax}");
Console.WriteLine($"Total: ${total}");
}
// After
public void PrintInvoice(Order order)
{
PrintHeader();
PrintCustomerDetails(order.Customer);
PrintItems(order.Items);
PrintTotals(order);
}
private void PrintHeader()
{
Console.WriteLine("Invoice");
Console.WriteLine("--------");
}
private void PrintCustomerDetails(Customer customer)
{
Console.WriteLine($"Customer: {customer.Name}");
Console.WriteLine($"Address: {customer.Address}");
Console.WriteLine($"Phone: {customer.Phone}");
Console.WriteLine();
}
private void PrintItems(List<OrderItem> items)
{
foreach (var item in items)
{
Console.WriteLine($"{item.Name} x {item.Quantity} = ${item.Price * item.Quantity}");
}
Console.WriteLine();
}
private void PrintTotals(Order order)
{
decimal subtotal = CalculateSubtotal(order.Items);
decimal tax = CalculateTax(subtotal);
decimal total = subtotal + tax;
Console.WriteLine($"Subtotal: ${subtotal}");
Console.WriteLine($"Tax: ${tax}");
Console.WriteLine($"Total: ${total}");
}
private decimal CalculateSubtotal(List<OrderItem> items)
{
return items.Sum(item => item.Price * item.Quantity);
}
private decimal CalculateTax(decimal amount)
{
return amount * 0.08m;
}
|
Introduce Parameter Object
Replace long parameter lists with objects:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // Before
function createUser(firstName, lastName, email, phone, address, city, state, zipCode, country) {
// ...
}
createUser('John', 'Doe', 'john@example.com', '555-1234', '123 Main St', 'Springfield', 'IL', '62701', 'USA');
// After
class UserInfo {
constructor({ firstName, lastName, email, phone, address, city, state, zipCode, country }) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.address = address;
this.city = city;
this.state = state;
this.zipCode = zipCode;
this.country = country;
}
}
function createUser(userInfo) {
// ...
}
const userInfo = new UserInfo({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
phone: '555-1234',
address: '123 Main St',
city: 'Springfield',
state: 'IL',
zipCode: '62701',
country: 'USA'
});
createUser(userInfo);
|
Replace Conditional with Polymorphism
Use polymorphism instead of switch statements:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| # Before
class Bird:
def __init__(self, bird_type):
self.type = bird_type
def get_speed(self):
if self.type == "european_swallow":
return 35
elif self.type == "african_swallow":
return 40
elif self.type == "norwegian_blue":
return 0
else:
raise ValueError("Unknown bird type")
def can_fly(self):
if self.type == "european_swallow":
return True
elif self.type == "african_swallow":
return True
elif self.type == "norwegian_blue":
return False
else:
raise ValueError("Unknown bird type")
# After
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def get_speed(self):
pass
@abstractmethod
def can_fly(self):
pass
class EuropeanSwallow(Bird):
def get_speed(self):
return 35
def can_fly(self):
return True
class AfricanSwallow(Bird):
def get_speed(self):
return 40
def can_fly(self):
return True
class NorwegianBlue(Bird):
def get_speed(self):
return 0
def can_fly(self):
return False
|
Integration with SOLID Principles
Clean code principles naturally align with SOLID:
Single Responsibility + Clean Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // Clean code + SRP
public class OrderProcessor
{
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
private readonly INotificationService _notification;
public OrderProcessor(
IInventoryService inventory,
IPaymentService payment,
INotificationService notification)
{
_inventory = inventory;
_payment = payment;
_notification = notification;
}
public void ProcessOrder(Order order)
{
ValidateOrder(order);
ReserveInventory(order);
ProcessPayment(order);
SendConfirmation(order);
}
private void ValidateOrder(Order order)
{
if (order.Items.Count == 0)
throw new InvalidOrderException("Order must contain items");
}
private void ReserveInventory(Order order)
{
foreach (var item in order.Items)
{
_inventory.Reserve(item.ProductId, item.Quantity);
}
}
private void ProcessPayment(Order order)
{
_payment.Charge(order.Customer, order.Total);
}
private void SendConfirmation(Order order)
{
_notification.SendOrderConfirmation(order);
}
}
|
Open/Closed + Clean Classes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // Clean code + OCP
class DiscountCalculator {
constructor(discountStrategy) {
this.strategy = discountStrategy;
}
calculateDiscount(order) {
return this.strategy.calculate(order);
}
}
class NoDiscount {
calculate(order) {
return 0;
}
}
class PercentageDiscount {
constructor(percentage) {
this.percentage = percentage;
}
calculate(order) {
return order.total * (this.percentage / 100);
}
}
class FixedAmountDiscount {
constructor(amount) {
this.amount = amount;
}
calculate(order) {
return Math.min(this.amount, order.total);
}
}
// Usage
const calculator = new DiscountCalculator(new PercentageDiscount(10));
const discount = calculator.calculateDiscount(order);
|
Conclusion
Writing clean code is a discipline that requires continuous practice and attention. It’s not enough to make code work; we must also make it clean. Clean code is easier to read, understand, modify, and maintain. It reduces technical debt and improves team productivity.
Key principles to remember:
- Choose meaningful, intention-revealing names
- Keep functions small and focused on one thing
- Minimize function arguments
- Write code at consistent levels of abstraction
- Use exceptions for error handling
- Comment only when necessary, prefer self-documenting code
- Design classes with single responsibilities and high cohesion
- Refactor continuously to improve design
- Integrate clean code principles with SOLID for robust architecture
The Boy Scout Rule applies to all code: leave it cleaner than you found it. Every small improvement compounds over time into a significantly better codebase.
References
- Robert C. Martin - Clean Code: A Handbook of Agile Software Craftsmanship
- Martin Fowler - Refactoring: Improving the Design of Existing Code
- Steve McConnell - Code Complete: A Practical Handbook of Software Construction
- Kent Beck - Implementation Patterns
- Robert C. Martin - Clean Architecture: A Craftsman’s Guide to Software Structure and Design
- The Pragmatic Programmer by David Thomas and Andrew Hunt
- Effective Java by Joshua Bloch