Clean Code

Writing Clean Code: Principles and Practices

Clean code is code that is easy to read, understand, and modify. It is not about following a specific style guide or design pattern — it is about communicating intent clearly to your future self and your teammates. Robert C. Martin’s Clean Code and Martin Fowler’s Refactoring established many of the core principles. This article covers the most impactful practices: meaningful naming, small functions, DRY, and SOLID.

Meaningful Naming

Names are the most direct form of communication in code. A good name reveals intent without requiring a comment. Choose names that answer: why does this exist, what does it do, and how is it used? Avoid single-letter names (except for loop counters in very tight scopes), avoid abbreviations that are not universally understood, and use pronounceable names that can be discussed verbally during code review. Boolean variables should read like predicates: isActive, hasPermission, canDelete.

# Bad names — unclear intent
def d(a, b):
    return a * (1 - b)

# Good names — clear intent
def apply_discount(original_price: float, discount_rate: float) -> float:
    return original_price * (1 - discount_rate)

Small Functions That Do One Thing

A function should do one thing and do it well. If you can extract a meaningful section of a function into a separate function with a descriptive name, do it. Small functions (under 20 lines) are easier to test, easier to understand, and more reusable. The function name and the implementation should be at the same level of abstraction — if a function named calculateTotal also sends emails, it is doing too much. Each function should have one level of indentation and either return a value or produce a side effect, but not both.

DRY — Don’t Repeat Yourself

Duplicated code multiplies bugs — when you fix a bug in one copy, you must remember to fix it in all the other copies. Extract repeated logic into functions, classes, or modules. The rule of three is a useful guideline: if you write the same thing three times, extract it; for the first two occurrences, wait and see if a third appears before refactoring. Do not conflate coincidence with duplication — two pieces of code that happen to look the same but serve different purposes should not be unified prematurely.

SOLID Principles

The Single Responsibility Principle (SRP) states that a class should have only one reason to change — keep domain logic separate from infrastructure code. The Open/Closed Principle (OCP) says code should be open for extension but closed for modification. The Liskov Substitution Principle (LSP) requires that derived classes be substitutable for their base classes. Interface Segregation (ISP) means many specific interfaces are better than one general-purpose interface. Dependency Inversion (DIP) says depend on abstractions, not concretions — inject dependencies rather than instantiating them.

# SRP violation: OrderService handles business logic AND persistence
class OrderService:
    def process_order(self, order):
        self.apply_discounts(order)
        database.save(order)
        email.send(order)

# SRP compliance: separate concerns
class OrderProcessor:
    def process(self, order): ...
class OrderRepository:
    def save(self, order): ...
class NotificationService:
    def send_confirmation(self, order): ...

Clean code is not achieved in one pass. Write the first version to make it work, then refactor to make it clean. The refactoring step is critical — without it, code accumulates cruft over time until it becomes unmaintainable.

Code Smells and Refactoring Techniques

Code smells are surface-level indicators that suggest deeper problems. Common smells include: long methods (extract method until each fits on one screen), large classes (extract class for each responsibility), primitive obsession (create value objects for phone numbers, money, dates), long parameter lists (introduce parameter object), shotgun surgery (a change requires editing many files—consolidate related logic), feature envy (a method uses more features of another class than its own—move it), and switch statements (replace with polymorphism). Martin Fowler’s catalog of refactorings provides step-by-step transformations for each smell. Extract Method, Rename Variable, Move Field, Replace Conditional with Polymorphism, and Introduce Parameter Object are the most frequently applied refactorings. Modern IDEs automate many refactorings with keyboard shortcuts—learn them to make refactoring faster than the alternative of leaving messy code.

# Before: primitive obsession
def create_order(customer_name, customer_email, customer_phone):
    pass

# After: value objects
@dataclass
class Customer:
    name: str
    email: str
    phone: str

def create_order(customer: Customer):
    pass

Test-Driven Development and Clean Code

TDD and clean code reinforce each other. Writing tests first forces you to design testable interfaces: clear inputs/outputs, dependency injection, and single responsibilities—all hallmarks of clean code. The test provides immediate feedback on API design: if the test is hard to write, the interface is probably awkward. The refactoring step in TDD (red-green-refactor) is where clean code practices are applied: extract methods, rename variables, simplify conditionals, and remove duplication. Without tests, refactoring is risky. With a comprehensive test suite, you refactor aggressively, knowing the tests will catch regressions.

Leave a Reply

Your email address will not be published. Required fields are marked *