Post

Clean Code Architecture and Design Principles

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>();

Avoid Disinformation

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);
}

Extract Try/Catch Blocks

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))

Comments and Documentation

Comments Don’t Make Up for Bad Code

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)

Good Comments

Some comments are necessary and beneficial:

1
2
# Copyright (c) 2024 Company Name
# Licensed under the MIT License

Informative Comments

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

TODO Comments

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;
}

Bad Comments

Mumbling

1
2
3
4
# Bad - unclear what this means
def load_properties():
    # Now load properties
    properties = load_from_file()

Redundant Comments

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;

Misleading Comments

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!
}

Commented-Out Code

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

Documentation Comments

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

Extract Method

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
This post is licensed under CC BY 4.0 by the author.