2.3 Clean Code
← 2.2 Data Structures | Home | Next: 2.4 Source Code Management →
Core Practices
- Naming is key to code readability. Files, classes, variables, and functions should be named clearly so the reader doesn't need to look up definitions to understand intent.
- Avoid long functions and classes. Split responsibilities properly into methods and classes, following the Single Responsibility Principle.
- Follow conventions to organize project structure. Keep directories clear so new members can intuitively find code.
- Extract complex boolean conditions into well-named functions.
if (user.IsEligibleForDiscount())beatsif (user.Age > 65 && user.YearsAsMember > 5 && !user.HasOutstandingBalance). - Write code that is as self-explanatory as possible. Someone reading it should understand "what" the code does without effort.
- Write documentation as code. Store Markdown files in a
docs/folder within the repository, version-controlled alongside the code, subject to CI checks (spelling, broken links). Documentation should describe "whys" and "hows" (design goals, use cases, component descriptions, architectural overviews). - Follow semantic versioning.
Semantic Versioning (SemVer)
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes.
- MINOR version when you add functionality in a backward compatible manner (and when you mark functionality as deprecated).
- PATCH version when you make backward compatible bug fixes.
Additional labels for pre-release (e.g., 1.0.0-alpha.1) and build metadata (e.g., 1.0.0+20130313144700) are available. Once a versioned package has been released, the contents of that version MUST NOT be modified. Major version zero (0.y.z) is for initial development—anything may change at any time. Version 1.0.0 defines the public API. When MAJOR is incremented, MINOR and PATCH must be reset to 0.
Code Comments Are Lies
Core argument from Pieter Koornhof: If you need to write a comment, you have failed to express the intent of the code.
Why comments become problems:
- They can state the obvious, being redundant: // The Person's First Name followed by public string FirstName { get; set; }.
- Having comments scattered throughout code makes it hard to read code fluently; the intent does not jump out at you.
- Comments Become Lies: Comments rarely get updated when the code changes. Over time, the functionality documented by the comments drifts away from what is actually happening.
- ToDo comments usually don't get done.
- Comments get copied with code, and as the code changes again the comments don't.
Legitimate exceptions for "good" comments:
- Comments documenting badly written legacy code that cannot be refactored.
- Comments documenting framework limitations: // EntityFramework requires a default constructor.
- ToDo comments on feature branches only (must be implemented or removed before merging to main).
- The rare case of highly optimized code that is confusing and not easily readable.
- API documentation comments (JavaDoc, JSDoc, XML doc)—only on public-facing interfaces.
The self-discipline principle: Whenever you want to write a comment, first ask, "Can I refactor the code to make the intent self-evident?" If not, the comment is a necessary compromise—not an excuse.
Composition over Inheritance
Composition over inheritance is a design principle that gives the design higher flexibility, promoted by the Gang of Four's Design Patterns (1994). It tries to achieve code reuse without requiring inheritance. Prefer has-a over is-a.
| Dimension | Inheritance | Composition |
|---|---|---|
| Relationship | is-a | has-a |
| Coupling | High—subclass tightly bound to parent implementation | Low—components independently replaceable |
| Flexibility | Static, decided at compile time | Dynamic, components replaceable at runtime |
| Problems | Deep inheritance trees, diamond problem, fragile base class | May require many forwarding methods |
Example (game objects):
- Inheritance approach: Player inherits Visible, Solid, Movable → requires multiple inheritance → diamond problem.
- Composition approach: GameObject contains VisibilityDelegate, CollisionDelegate, UpdateDelegate members—different combinations are passed via constructor to create Player (visible + solid + movable), Cloud (visible + movable), Trap (solid only).
Modern languages mitigate forwarding method boilerplate through traits, mixins, type embedding, and default interface methods. Empirical study (2013 analysis of 93 open-source Java programs): "While there is not huge opportunity to replace inheritance with composition, the opportunity is significant (median of 2% of uses of inheritance are only internal reuse, and a further 22% are only external or internal reuse)."
Test-Driven Development (TDD)
The TDD cycle: Red → Green → Refactor
- Red: Write a failing test that describes the expected behavior—before any implementation code exists.
- Green: Write the minimum amount of code to make the test pass. Don't over-engineer.
- Refactor: Under the protection of passing tests, optimize code structure—eliminate duplication, improve readability—while keeping tests green.
- Repeat: next test → next feature.
TDD's core value is not the tests themselves but driving design. Writing tests first forces you to think from the caller's perspective, naturally leading to low coupling and high cohesion. Tests are a byproduct; good design is the goal.
Sources
- Semantic Versioning 2.0.0: https://semver.org/
- Pieter Koornhof — Code Comments are Lies: https://sneakycode.net/code-comments-are-lies (original at CodeProject)
- Wikipedia — Composition over inheritance: https://en.wikipedia.org/wiki/Composition_over_inheritance
← 2.2 Data Structures | Home | Next: 2.4 Source Code Management →