These notes provide a detailed summary of the “Clean Code” book, along with examples to help you understand and apply its principles effectively.


General Principles of Clean Code

  1. Readability:

    • Code is written for humans to read and understand.
    • Prioritize clarity over clever tricks or brevity.

    Example:

    # Clear and readable code
    def calculate_total_price(prices, tax_rate):
        subtotal = sum(prices)
        tax = subtotal * tax_rate
        return subtotal + tax
    
    # Avoid unclear code
    def calc(p, t):
        return sum(p) + sum(p) * t
    
  2. Simplicity:

    • Strive for simplicity by breaking down complex logic.
    • Avoid overengineering solutions to problems.

    Example:

    # Simple and clear
    def is_eligible_for_discount(user):
        return user.age >= 18 and user.is_member
    
  3. Avoid Duplication (DRY Principle):

    • Extract common functionality to avoid repetitive code.

    Example:

    # Before
    def calculate_area_of_circle(radius):
        return 3.14 * radius * radius
    
    def calculate_area_of_square(side):
        return side * side
    
    # After
    def calculate_area(shape, dimension):
        if shape == 'circle':
            return 3.14 * dimension * dimension
        elif shape == 'square':
            return dimension * dimension
    
  4. Testability:

    • Write modular and decoupled code to make testing easier.

    Example:

    # Decoupled code
    def send_email(smtp_client, recipient, message):
        smtp_client.connect()
        smtp_client.send(recipient, message)
    
    # Easily testable by mocking smtp_client.
    
  5. Small and Focused:

    • Follow the Single Responsibility Principle (SRP).
    • Each class or function should have only one reason to change.

Naming Conventions

  • Descriptive Names:

    • Use clear, descriptive, and unambiguous names.

    Example:

    # Good naming
    def calculate_interest(principal, rate, time):
        pass
    
    # Poor naming
    def calc(p, r, t):
        pass
    
  • Consistent Patterns:

    • Use camelCase for variables and functions, PascalCase for classes.
  • Avoid Abbreviations:

    • Write full names (e.g., totalAmount instead of totAmt).

Functions

  1. Small and Focused:

    • Functions should ideally have 5-20 lines of code and do one thing only.

    Example:

    # Before
    def process_order(order):
        # validate order
        if not order.is_valid():
            return "Invalid order"
        # calculate total
        total = sum(order.items)
        # send confirmation
        send_confirmation(order.user)
        return total
    
    # After (refactored into smaller functions)
    def validate_order(order):
        return order.is_valid()
    
    def calculate_total(order):
        return sum(order.items)
    
    def process_order(order):
        if not validate_order(order):
            return "Invalid order"
        total = calculate_total(order)
        send_confirmation(order.user)
        return total
    
  2. Few Arguments:

    • Limit arguments to three or fewer. Use parameter objects if needed.

    Example:

    # Use parameter object
    class OrderDetails:
        def __init__(self, items, user, tax_rate):
            self.items = items
            self.user = user
            self.tax_rate = tax_rate
    
    def calculate_total(details):
        subtotal = sum(details.items)
        tax = subtotal * details.tax_rate
        return subtotal + tax
    
  3. Avoid Side Effects:

    • A function should not modify external variables or objects unexpectedly.
  4. Use Meaningful Error Handling:

    • Use exceptions and meaningful error messages.

    Example:

    # Good
    def divide(a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b
    
    # Poor
    def divide(a, b):
        return a / b  # Risk of ZeroDivisionError
    

Comments

  1. Minimize Comments:

    • Let the code explain itself as much as possible.

    Example:

    # Good
    def get_user_age(user):
        return user.age
    
    # Poor
    # This function gets the age of the user
    def get_user_age(user):
        return user.age
    
  2. Use Comments for Clarification:

    • Write comments to explain why something is done, not what the code does.

    Example:

    # Why we use this formula: it follows the standard tax calculation.
    tax = subtotal * 0.05
    

Classes

  1. Small and Focused Classes:

    • Each class should represent a single concept or responsibility.
  2. Encapsulation:

    • Keep internal details private and provide controlled access through public methods.

    Example:

    class BankAccount:
        def __init__(self, balance):
            self.__balance = balance  # private attribute
    
        def deposit(self, amount):
            self.__balance += amount
    
        def get_balance(self):
            return self.__balance
    
  3. Dependency Injection:

    • Pass dependencies as parameters instead of creating them internally.

    Example:

    class ReportGenerator:
        def __init__(self, data_fetcher):
            self.data_fetcher = data_fetcher  # dependency injected
    
        def generate(self):
            data = self.data_fetcher.fetch()
            return f"Report: {data}"
    

Error Handling

  1. Use Exceptions Instead of Error Codes:

    • Avoid returning error codes from functions.

    Example:

    # Good
    def open_file(filename):
        if not os.path.exists(filename):
            raise FileNotFoundError(f"{filename} does not exist")
        return open(filename)
    
    # Poor
    def open_file(filename):
        if not os.path.exists(filename):
            return -1  # Error code
        return open(filename)
    
  2. Handle Exceptions Gracefully:

    • Don’t let exceptions crash your program unnecessarily.

    Example:

    try:
        result = divide(10, 0)
    except ValueError as e:
        print(f"Error: {e}")
    

Code Smells and Refactoring

Common Code Smells

  1. Long Methods: Break them into smaller, focused methods.
  2. Duplicated Code: Extract reusable methods or functions.
  3. Large Classes: Split into smaller, cohesive classes.
  4. Too Many Arguments: Use parameter objects or simplify logic.
  5. Inconsistent Naming: Use a uniform naming style.

Refactoring Techniques

  1. Extract Method: Move chunks of code into new functions.

  2. Rename Variable/Method: Use meaningful names to improve clarity.

  3. Replace Conditional with Polymorphism:

    • Replace long if-else or switch blocks with object-oriented design.

    Example:

    # Before
    def get_shipping_cost(country):
        if country == 'US':
            return 5
        elif country == 'Canada':
            return 10
        else:
            return 20
    
    # After
    class ShippingCost:
        def get_cost(self):
            pass
    
    class USShipping(ShippingCost):
        def get_cost(self):
            return 5
    
    class CanadaShipping(ShippingCost):
        def get_cost(self):
            return 10
    
    shipping = USShipping()
    cost = shipping.get_cost()