Clean Code: Chapter 3 Notes
Table of Contents
Link to Chapter 2 | Link to Chapter 4
Chapter 3: Functions
Functions should be small
Functions should be extremely short—ideally just a few lines, so they remain easy to understand and maintain.
Avoid deeply nested blocks; keep indentation shallow (1–2 levels), often replacing blocks with descriptive function calls.
A small function tells a concise, self-contained story, making it easier for readers to follow the program’s intent.
The smaller the function, the more descriptive and accurate its name can be, improving self-documentation.
Large functions hide complexity and mix abstraction levels, making errors and duplication more likely.
Do One Thing & One Level of Abstraction
A function should do exactly one conceptual task, and all its statements should exist at the same abstraction level.
Mixing details (like string concatenation) with high-level actions (like rendering a page) causes confusion.
The Stepdown Rule: organise functions so they read like a top down narrative, each calling the next abstraction level.
If you can extract a subfunction with a name that isn’t a restatement, the original function is doing too much.
Functions that “do one thing” cannot be logically split into sections such as “initialize,” “process,” “finalize.”
Switch Statements
Switch statements naturally violate “do one thing” by handling multiple cases; they also grow in size over time.
They break the Single Responsibility Principle (multiple reasons to change) and Open-Closed Principle (must change for new cases).
Preferred approach: hide switch statements inside a factory and dispatch behavior polymorphically through an interface.
Allow only one visible switch in your system, used solely for object creation, then encapsulate it.
This removes duplication and keeps high-level code unaware of concrete type distinctions.
Example:
public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } ----------------- public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } ----------------- public class EmployeeFactoryImpl implements EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r) ; case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmploye(r); default: throw new InvalidEmployeeType(r.type); } } }
Use Descriptive Names
A function’s name should clearly state its purpose. Long, descriptive names beat short, cryptic ones.
Consistent naming patterns (shared verbs/nouns) help code read like a coherent story and aid predictability.
Descriptive names reduce the need for comments and improve comprehension without external documentation.
Renaming functions can reveal design improvements, so try multiple options until the best emerges.
IDE refactoring tools make renaming safe, encouraging experimentation.
Function Arguments
The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn’t be used anyway.
Fewer arguments = better; aim for 0–2, avoid more than 3 unless absolutely necessary.
Flag arguments (booleans) are a red flag—they imply the function does multiple things.
Group related parameters into objects (e.g., Point
for x
and y
) to reduce argument count and improve clarity.
Output arguments are confusing—prefer returning values or mutating the owning object’s state.
Match function/argument names in verb–noun or keyword style (e.g., writeField(name)
, assertExpectedEqualsActual
).
Have No Side Effects
A function should do only what its name promises. Hidden state changes are misleading and dangerous.
Side effects create temporal coupling, meaning the function must be called in a certain sequence to be safe.
If unavoidable, make side effects explicit in the name (e.g., checkPasswordAndInitializeSession
).
Clear separation of command and query functions avoids ambiguity in meaning and intent.
Functions that modify state and return information often cause confusion and should be split.
Error Handling
Error handling is a single responsibility—separate it from normal logic to keep both paths clear.
Prefer exceptions over error codes to avoid cluttering the happy path and to reduce dependency magnets.
Extract try/catch bodies into their own functions for cleaner structure.
See below:
public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) throws Exception { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); }
Keep functions small enough that occasional multiple return or break statements are acceptable.
Avoid duplication in error handling, and follow the DRY principle to ensure changes occur in one place.