What does "self-documenting code" mean in practice, which specific refactorings let you replace a comment with code, and where does that technique stop working?
answer
- extract function = the comment becomes the name
- rename over annotate
- explaining variable for dense conditions
- magic number → named constant; bool param → enum
- names give what/which, never why
basics
~20 sIt means naming and structuring code so it explains itself: rename vague identifiers, extract a commented block into a well-named function, and put a complex condition into a named variable. It fails for reasons outside the code — external constraints, hazards, and decisions.
solid answer
~50 sSelf-documenting code shifts explanation from unverified prose into structure the compiler and tools maintain. The main moves: **Extract Function** — a commented block becomes a call whose name is the comment; **Rename Variable/Function** — `d` becomes `elapsedDays`, `process()` becomes `settleInvoice()`; **Introduce Explaining Variable** — a dense boolean becomes `isEligibleForRefund`; **Replace Magic Number with Named Constant**; **Replace Comment-Documented Flag with a type or enum**; **Decompose Conditional**. Each replaces a comment with something a rename tool updates and a test exercises, so it cannot rot. The limits are real. Names can express *what* and *which*, never *why*: you cannot name a function "because the vendor returns 200 on failure". Also outside its reach are legal headers, hazard warnings, performance rationale, TODOs, references to decisions and tickets, and public API contracts (parameters, errors, thread-safety). Treating "every comment is a failure" as absolute deletes exactly the comments worth keeping.
code
pseudocode · 15 lines// BEFORE: comments carrying the meaning
if (u.a > 18 && u.s == 2 && !u.b) { // adult, active, not banned
// send the welcome packet
m.send(u.e, T1); log(u.id);
}
// AFTER: structure carries the meaning; only the WHY remains a comment
val isEligibleForOnboarding = user.isAdult && user.isActive && !user.isBanned
if (isEligibleForOnboarding) {
sendWelcomePacket(user) // extracted: name replaces the old comment
}
// Irreducible - no name can say this:
// Packet must go out before the CRM sync at 02:00 UTC, or the vendor
// dedupes it away (vendor ticket #8812).go deeper
Give the idea plus two moves: rename unclear variables, and extract a commented block into a function named after the comment.
List the refactorings by name (extract function, rename, explaining variable, named constant, enum instead of boolean flag) and note that names cannot express why.
Frame it as moving explanation from unverified prose into tool-maintained structure, enumerate the irreducible comment categories, and discuss over-extraction and name-inflation failure modes.
Position it inside a knowledge strategy: types and tests as enforced documentation, doc comments as published contracts, ADRs for decisions, and review norms that turn 'what' comments into refactoring tasks without banning comments outright.
## The claim and the useful version of it The strong slogan is *"comments are a failure to express yourself in code."* The defensible version is narrower and more useful: > Any comment that explains **what the code does** is a hint that the code should be renamed or restructured. Comments that explain **why**, or that state facts originating outside the code, cannot be refactored away and should be written. The motivation is verification. Names, types and structure are checked and maintained by tools — the compiler rejects a misspelled name, a rename refactoring updates every reference, tests exercise the extracted function. Prose is checked by nobody. So every explanation you can move from prose into structure becomes permanently correct instead of eventually wrong. ## The concrete refactorings **1. Extract Function (Extract Method).** The single highest-value move. A block preceded by `// validate the request` becomes `validateRequest(request)`. Now the explanation is the function name; it appears in stack traces, is searchable, can be unit-tested, and cannot drift from the block because it *is* the block. Rule of thumb: if you can write a comment summarising a block, that comment is the function's name. **2. Rename Variable / Function / Class.** `d` → `elapsedDays`; `flag` → `hasVerifiedEmail`; `handle()` → `retryFailedPayment()`. Modern IDEs make renaming safe and mechanical, so there is rarely an excuse to annotate a bad name instead of fixing it. Good names encode units (`timeoutMs`), domain terms from the ubiquitous language, and intent. **3. Introduce Explaining Variable.** Replace a dense expression with a named intermediate: `if (order.total > 100 && customer.tier != BASIC && !order.refunded)` becomes `val qualifiesForFreeShipping = ...; if (qualifiesForFreeShipping)`. The variable name is the comment, and it is reusable in tests and log messages. **4. Replace Magic Number with Named Constant.** `86400` with `// seconds in a day` becomes `SECONDS_PER_DAY`. Same for magic strings and status codes. **5. Replace boolean parameters with an enum or two functions.** `render(true)` needs a comment to say what `true` means; `render(Mode.PREVIEW)` or `renderPreview()` does not. This kills a whole class of comment-at-the-call-site. **6. Decompose Conditional.** Turn `if (...) { ... } else { ... }` with commented branches into `if (isSummerRate(date)) summerCharge(quantity) else winterCharge(quantity)`. **7. Encode invariants in types.** A comment `// must be non-null and non-empty` is weaker than a non-nullable type plus a validated value object `EmailAddress`. Types are enforced; comments are hoped for. **8. Express contracts as tests.** A comment describing edge-case behaviour is better written as a named test — `refundsAreRejectedAfterNinetyDays()` — which fails when the behaviour changes. Tests are documentation with a build-time correctness check. ## Where the technique stops working Names answer *what* and *which*. They cannot answer *why*, and they cannot carry facts that do not originate in the code. Irreducible comment categories: - **Rationale / trade-off:** "chose the two-pass algorithm because the single-pass version doubled p99 latency." No identifier expresses a benchmark result. - **External constraints:** "the vendor returns HTTP 200 with an error body"; "spec §4.2 requires the exclusive end date." - **Hazard warnings:** "not thread-safe", "O(n²) — do not call inside the request loop", "AUTO-GENERATED, DO NOT EDIT." - **Legal / licence headers** — mandated text with a non-human audience. - **TODO/FIXME** with an issue link — a marker for future work, not a description of present code. - **Public API documentation** — callers should not have to read the body, so parameters, return values, thrown errors, side effects and threading contracts belong in doc comments. - **Genuinely dense code** — a hand-optimised numeric kernel, a regex, a bit-manipulation trick. You can name it `packColorChannels`, but a short note on the encoding still saves the next reader minutes. ## Trade-offs and failure modes of over-applying it - **Name inflation.** `calculateTheTotalPriceIncludingTaxAndShippingForTheCurrentOrder` is not clearer than a shorter name plus a one-line comment; extremely long names hurt scanning and line length. - **Extraction shrapnel.** Splitting into dozens of one-line functions can make the reader jump around the file to reconstruct a linear process. Extraction pays when the extracted piece has a coherent concept name; it costs when it merely relocates two lines. - **Dogma deleting value.** Teams that adopt "no comments" wholesale often strip warnings and rationale — the exact comments with the highest payoff — because a rule was applied without the criterion behind it. - **Names still rot, just less.** A renamed function whose behaviour later changes can mislead too; the difference is that tools and tests make that drift far more visible than prose drift. ## The practical checklist in review 1. Comment restates the code? → rename or extract. 2. Comment explains a block? → that is a function name. 3. Comment explains a value? → named constant or enum. 4. Comment explains a condition? → explaining variable or predicate function. 5. Comment states an invariant? → a type, a validation, or a test. 6. Comment states a reason, hazard, obligation or external fact? → **keep it, and make it precise.**
- Is the maxim "every comment is a failure to express yourself in code" correct?Only for comments describing what the code does — those really are refactoring signals. It is wrong for rationale, hazard warnings, external constraints, legal headers, TODOs and public API contracts, none of which any identifier can express. Applied absolutely, the maxim deletes the most valuable comments.
- When is extracting a function to replace a comment the wrong call?When the extracted piece has no coherent concept name, when it needs many parameters or shared mutable state, or when it fragments a sequential process into hops that force the reader to jump around. Extraction should name an idea, not merely relocate lines.
Renaming and extracting is like labelling the jars in a kitchen instead of taping a note to the cupboard door listing what is in each jar. The label travels with the jar; the note goes wrong the moment anything is moved.
saying these in an interview costs you the question
- "Good code needs no comments at all" — ignores rationale, warnings, legal text and API contracts.
- Adding a clarifying comment instead of renaming a badly named variable.
- Encoding units or nullability only in a comment (`// milliseconds`, `// may be null`) instead of the name or the type.
- Assuming extracted functions are automatically clearer — a bad name plus a jump is worse than inline code.
- Believing self-documenting code removes the need for tests as behavioural documentation.