core/principles/clean_code.md

Clean Code Principles for 2026

Documented, named critiques of Robert C. Martin's Clean Code lineage - the Muratori/Martin performance debate, the Ousterhout/Martin design debate on function length, comments, and TDD - with decision rules for when each side is actually right.

Assume the reader already knows the textbook rules (small functions, intention-revealing names, few comments, one level of abstraction per function). This document is the part that isn't in the textbook: two documented, named, still-live disputes between Clean Code's author and two of its sharpest critics, and the decision rules that fall out of them.

Dispute 1: Function length and "shallow" decomposition (Ousterhout vs Martin)

John Ousterhout (A Philosophy of Software Design) and Robert Martin held a direct, public, written debate, archived at github.com/johnousterhout/aposd-vs-clean-code. It covers three concrete topics - method length, comments, TDD - and neither side backed down. Treat this as two named experts disagreeing, not as a settled question.

Ousterhout's claim: Clean Code's push toward very short methods produces shallow functions - a function whose interface (name, parameters, the fact that a reader must now go find it) costs more attention than the small amount of logic it hides saves. Splitting a function does not reduce complexity, it relocates and multiplies interfaces. His rule, stated directly against Martin's: "first make functions deep, then try to make them short enough to be easily read - don't sacrifice depth for length." A deep function (small interface, substantial functionality behind it, e.g. a well-designed read() syscall) beats five shallow ones that force the reader to jump around to reconstruct one coherent operation.

Decision rule: Before extracting a function, ask whether the extracted piece is independently meaningful (has its own name-worthy concept, will be reused, or is independently testable) or whether it's just relocating five lines behind a new name to satisfy a line-count target. Only the former is a net win. A 25-line function that reads top-to-bottom as one coherent paragraph beats five 5-line functions with names like validateAndPrepare, doInnerCalc, finalizeStep that only ever call each other once, in order.

Dispute 2: Comments (Ousterhout vs Martin)

Same debate thread, unresolved: Clean Code's position is that a comment is a failure to name something well enough - good naming should make most comments unnecessary. Ousterhout's APOSD position is that structured comments carry information code cannot express at all (why a value was chosen, what invariant a caller must uphold, what the interface promises independent of the current implementation) and that a codebase relying purely on naming under-documents its own contracts.

Decision rule: Naming solves "what does this do." It cannot solve "why does this exist," "what did I already rule out," or "what must remain true across all future implementations of this interface." If the information is the second kind, write the comment - do not keep renaming things trying to make the name carry it.

Dispute 3: TDD as a design method (Ousterhout vs Martin)

Ousterhout's argument, from the same debate: TDD's natural unit of work is one test, which is smaller than a natural design unit (a class, a module boundary). Writing the next test that makes you write the next few lines of code optimizes locally and can produce a design nobody actually thought through end-to-end, because no step in the loop asks "does this whole class hang together." This is a critique of TDD as a design technique, not of having tests.

Decision rule: Use TDD for well-understood, narrow units (a pure function, a parser, a bug-fix regression). For anything where the module boundary itself is undecided (a new subsystem, a new tool's data model), sketch the interface first, then backfill tests - do not let the red-green-refactor loop be the mechanism that decides the shape of the boundary.

Dispute 4: Performance (Muratori vs Martin)

Casey Muratori's "Clean Code, Horrible Performance" (computerenhance.com/p/clean-code-horrible-performance, 2023) benchmarked a shape-area calculation two ways: Clean-Code-style polymorphic dispatch (virtual functions / vtable calls) versus a flat switch/union approach. Result: roughly 1.5x slower (24 cycles/shape flat vs 35 cycles/shape polymorphic) in his base case, and he argues the real cost isn't the vtable indirection itself but that the compiler cannot optimize across a virtual call boundary because it doesn't know the concrete type - the loss compounds with every optimization the compiler is blocked from making, not just the dispatch overhead.

Robert Martin responded directly and at length on GitHub (github.com/unclebob/cmuratori-discussion/blob/main/cleancodeqa.md), conceding the specific Clean Code examples "are not efficient at the nanosecond level," and drew this explicit boundary: "If you are trying to squeeze every nanosecond from a battery of GPUs, then clean code may not be for you... if you are trying to squeeze every man-hour of productivity from a software development team, then clean code can be an effective strategy."

A third party, Evan Teran (blog.codef00.com/2023/04/13/casey-muratori-is-wrong-about-clean-code), adds the load-bearing nuance both sides under-state: Muratori's own benchmark code already follows most Clean Code conventions (small functions, clear naming) - the actual variable is specifically virtual-dispatch-based polymorphism, not "clean code" as a whole. Teran's own benchmarks with std::variant show the direction of the effect is compiler- and pattern-dependent (Clang and GCC disagree on which approach wins), which argues for profiling over ideology in either direction.

Decision rule:

ContextGuidance
Hot loop, profiler shows time in a virtual/interface callFlatten it: switch/tagged-union/std::variant-style dispatch, monomorphize, or devirtualize. Isolate the fast path behind a clearly named boundary.
Everything else (the large majority of business logic)Default to Martin's boundary: optimize for the team's throughput, not for cycles nobody is measuring. Do not pre-flatten abstractions "for speed" without a profiler run backing it up.
Reviewing a PR that removes an abstraction "for performance"Demand the benchmark. Muratori's own point-of-agreement with Martin is that this is an empirical question, not a stylistic one.

Code Smell Reference

A smell is cheap to fix the day it appears, expensive once other code depends on its shape.

SmellSymptomUsual Fix
Shallow functionExtracted piece has no independent meaning, is called once, only exists to hit a line-count targetInline it back; see Dispute 1
Long parameter list5+ positional parametersParameter object / Builder
Shotgun surgeryOne conceptual change touches many filesConsolidate responsibility - see SOLID
Feature envyMethod uses another object's data more than its ownMove method to the object it envies
Primitive obsessionBusiness concepts represented as raw str/intValue objects / typed wrappers
Comment restating code# increment i above i += 1Delete it; if it needed explaining, rename instead
Naming-only documentation of a contractA public interface with no comment on preconditions/invariants, name alone is expected to carry the contractAdd a structured comment per Dispute 2 - this is not "clutter," it's the part naming cannot do

When Not to Apply These Rules

SituationRecommendation
Hot numerical loop with a measured (not assumed) performance requirementPrioritize the profiler's verdict; isolate and comment the exception
Auto-generated codeDo not hand-clean it; fix the generator
A module boundary that is still genuinely unknownSketch the interface first; don't let TDD's next-test loop decide the architecture (Dispute 3)
A public interface whose contract can't be inferred from its signatureWrite the structured comment; don't keep renaming to avoid it (Dispute 2)

Sources