CHAPTER 7 · WRITING GOOD CODE

Clean Code

Chapters 1–6 were about design in the large: classes, relationships, patterns. This chapter zooms all the way in — to the individual name, function, comment, and blank line. Based on Robert C. Martin's Clean Code, it teaches the micro-skills that decide whether your beautifully-patterned system is a joy or a nightmare to actually read: meaningful names, small functions, honest comments, consistent formatting, and test-driven development.

Why clean code? We write code for humans

Code is read far more often than it is written. Estimates in industry put the read-to-write ratio at roughly 10:1 — before you write a new line you almost always have to read and understand the surrounding old ones. So the real audience of your code is not the compiler (which is happy with int d and a 400-line method); it is the next human — a teammate, or you in six months, who remembers nothing.

🧠 Why this matters

The machine doesn't care about clean code — Proc(l, f) and CalculateOrderTotal(itemPrices, isMember) compile to the same instructions. We write code for humans to understand, not for machines to execute. Every clean-code rule in this chapter is really the same rule: minimise the time the next reader needs to understand what's going on. (Interestingly, the slides note that AI-generated code today often looks cleaner than average human code — which raises the bar for us, not lowers it: you still have to read, verify, and maintain it.)

The cost of a mess

Why not just "get it working" and clean up later? Because later never comes, and mess compounds. Unclean, disorganised code rots: every hack makes the next change harder, which invites the next hack. Teams that start fast by cutting corners get slower every month, until adding a trivial feature takes weeks. Teams that keep the code clean pay a small, steady tax — and keep their speed.

Time → Productivity → ≈ where the mess catches up with you week 1–2: cutting corners feels faster… …month 3: every change is a fight, estimates explode, morale drops "we'll clean it up later" kept clean continuously
Figure: productivity over time. Mess buys a brief head start, then taxes every change forever. Clean code costs a little every day and pays it back with interest.

The Boy Scout Rule

So who cleans the code? Everyone, constantly, in tiny increments. The Boy Scouts have a rule: "leave the campground cleaner than you found it." Applied to software:

💡 The Boy Scout Rule

Check your code in a little cleaner than when you checked it out. If everyone does this, the code cannot rot. The cleanup doesn't have to be big:

  • rename one variable for the better,
  • break up one function that's a little too large,
  • eliminate one small duplication,
  • clean up one composite if statement.
🌍 Real-world analogy

A professional kitchen. Chefs clean as they cook — wiping the bench between dishes — because during the dinner rush there is no time to stop and scrub everything. A kitchen where "we'll clean on Sunday" is the plan is a kitchen that fails its health inspection. "We'll refactor after the release" works exactly as well.

The rest of this chapter is the toolkit: what to clean and how. We follow the slides' structure — names, functions, comments, formatting, testing — and every rule comes with a bad→good C# example.

Meaningful names — 9 rules

Names are everywhere in software: variables, functions, arguments, classes, namespaces, source files, folders, projects, NuGet packages. Since we name so much, naming well is the highest-leverage clean-code skill. A good name is a one-word comment that can never go stale.

1 · Use intent-revealing names

The name of a variable, function, or class should answer the big questions: why does it exist, what does it do, how is it used? The litmus test: if a name needs a comment, the name has failed.

❌ Name needs a comment
int d; // elapsed time in days

if (d > 30)
    ArchiveFile();
What is d? You must scroll back to the comment — and the comment can lie after the next edit.
✅ Name IS the comment
int elapsedTimeInDays;
// or, depending on what it really means:
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

if (fileAgeInDays > 30)
    ArchiveFile();
Each candidate name forces you to decide what the value actually means — that thinking is the real benefit.

Intent-revealing names transform whole functions. Both versions below are the same algorithm over the same data structure:

❌ What is this code doing?
public List<int[]> GetThem()
{
    var list1 = new List<int[]>();
    foreach (int[] x in theList)
        if (x[0] == 4)
            list1.Add(x);
    return list1;
}
Compiles fine. Means nothing. What is theList? Why cell [0]? Why the value 4?
✅ Same code, with intent
public List<Cell> GetFlaggedCells()
{
    var flaggedCells = new List<Cell>();
    foreach (Cell cell in gameBoard)
        if (cell.IsFlagged)
            flaggedCells.Add(cell);
    return flaggedCells;
}
Now it reads like a sentence: it's a minesweeper board and we're collecting flagged cells. Not one comment needed.

2 · Avoid disinformation

Worse than a name that says nothing is a name that says the wrong thing. Avoid words whose entrenched meaning differs from yours. hp looks like a fine abbreviation for hypotenuse — but to many programmers hp, aix, and sco are Unix platforms. And never bake a type word into a name unless it's true:

❌ The name lies about the type
// it is NOT a List — it's a dictionary!
Dictionary<string, Account> accountList =
    LoadAccounts();

double hp = Math.Sqrt(a * a + b * b);
A reader who sees accountList will assume ordering and index access — false conclusions, wasted debugging.
✅ Honest names
Dictionary<string, Account> accountsById =
    LoadAccounts();
// or simply: accounts, accountGroup

double hypotenuse = Math.Sqrt(a * a + b * b);
If the container type ever matters, the name now tells the truth (ById hints at keyed lookup). Plain accounts is also fine.

3 · Make meaningful distinctions

If two names are different, they should mean something different. Number-series names (a1, a2) and noise words (Info, Data) create distinction without meaning:

❌ Distinction without meaning
public static void CopyChars(char[] a1, char[] a2)
{
    for (int i = 0; i < a1.Length; i++)
        a2[i] = a1[i];
}

class Product { }
class ProductInfo { }   // how is this different?
class ProductData { }   // …and this?
Which array is copied into which? And Info/Data are noise words — like a, an, the — they distinguish the names without distinguishing the concepts.
✅ Each name earns its difference
public static void CopyChars(
    char[] source, char[] destination)
{
    for (int i = 0; i < source.Length; i++)
        destination[i] = source[i];
}

class Product { }
class ProductDescription { }  // marketing copy
class ProductInventory { }    // stock levels
Now the argument order is obvious at the call site, and each class name states its distinct responsibility.

4 · Use pronounceable names

A significant part of the human brain is dedicated to words — and words are, by definition, pronounceable. Unpronounceable names can't be discussed ("what does gen-why-emm-dee-aitch-emm-ess do?"), can't be remembered, and make code reviews sound ridiculous:

❌ Try saying this in a meeting
class DtaRcrd102
{
    public DateTime Genymdhms { get; set; }
    public DateTime Modymdhms { get; set; }
    public string Pszqint { get; set; }
}
genymdhms = "generation date: year, month, day, hour, minute, second". You'd never guess.
✅ Speakable, discussable
class CustomerRecord
{
    public DateTime GenerationTimestamp { get; set; }
    public DateTime ModificationTimestamp { get; set; }
    public string RecordId { get; set; }
}
"Hey, look at this record — the generation timestamp is in the future!" Normal human conversation, restored.

5 · Use searchable names

Sooner or later you will hunt for every place a value or variable is used. Single letters and raw numeric constants are nearly impossible to grep for — searching 5 or e in a codebase returns thousands of hits. Rule of thumb: the length of a name should correspond to the size of its scope. A loop counter i inside a 3-line loop is fine; a class-level field named e is a crime.

❌ Unsearchable magic
for (int j = 0; j < 34; j++)
{
    s += (t[j] * 4) / 5;
}
What is 34? What is 4/5? Try finding every usage of "5" in the project when the work-week changes.
✅ Grep-friendly
const int WorkDaysPerWeek = 5;
const int NumberOfTasks = 34;
const int IdealDaysPerRealDay = 4;

for (int j = 0; j < NumberOfTasks; j++)
{
    int realTaskDays = taskEstimates[j] * IdealDaysPerRealDay;
    int realTaskWeeks = realTaskDays / WorkDaysPerWeek;
    sum += realTaskWeeks;
}
Searching WorkDaysPerWeek finds exactly the relevant lines. Longer code, dramatically faster to understand and change.

6 · Class names are nouns

A class is a thing, so its name should be a noun or noun phrase: Customer, WikiPage, Account, AddressParser. A class name should never be a verb — and beware the weasel-nouns Manager, Processor, Data, Info: they usually mean "I couldn't decide what this class is responsible for", which is a Single Responsibility smell in disguise.

❌ Verbs and weasel-nouns
class ProcessOrders { }   // a verb — that's a method's job
class OrderManager { }    // manages… what, exactly? everything?
class CustomerData { }    // noise word
"Manager" classes attract unrelated responsibilities like a junk drawer.
✅ Concrete noun = clear responsibility
class OrderProcessor { }      // ok: an agent-noun, one job
class OrderRepository { }     // stores/loads orders
class InvoiceCalculator { }   // computes invoices
class Customer { }
Each name answers "what is this thing?" — and its single responsibility falls straight out of the name.

7 · Method names are verbs

A method does something, so its name should be a verb or verb phrase: PostPayment, DeletePage, Save. Accessors, mutators, and predicates follow convention: prefix with Get, Set, and Is/Has/Can.

❌ Nouns pretending to act
invoice.Payment(100);     // is it paying? fetching a payment?
page.Deletion();          // ??
if (user.Admin()) { }     // reads like a noun
Every call site becomes a small puzzle.
✅ Verb phrases + C# conventions
invoice.PostPayment(100);
page.Delete();
if (user.IsAdmin) { }        // predicate → Is/Has/Can

// C# note: prefer properties over Get/Set methods
// for simple state:
customer.Name = "Amara";     // instead of SetName(...)
var name = customer.Name;    // instead of GetName()
In idiomatic C#, trivial GetX/SetX pairs become properties; keep Get… for operations that do real work (GetFlaggedCells()).

8 · Use solution-domain names

Your readers are programmers. So use computer-science vocabulary freely: algorithm names, pattern names, math terms. AccountVisitor tells anyone who knows the Visitor pattern exactly what to expect; JobQueue is instantly clear. Don't force every name into customer language — your coworkers shouldn't have to phone the customer to decode a name for a concept they already know by its CS name.

❌ Problem-domain circumlocution
class ThingThatWalksThroughAccounts { }
class ListOfWaitingPrintTasks { }
class OrderHelperUtility { }
Vague, verbose, and it hides the fact that well-known structures are being used.
✅ Speak programmer
class AccountVisitor { }   // Visitor pattern — chapter 5
class PrintJobQueue { }    // everyone knows a queue
class OrderFactory { }     // Factory — chapter 3
Pattern names in class names are documentation: they import a whole chapter of shared understanding for free.

9 · Add meaningful context

Few names are meaningful alone. state — part of an address, or a state machine? Give names context by enclosing them in well-named classes, functions, or namespaces. Prefixes (addrState) are the last resort when structure isn't possible:

❌ Context-free loose variables
string firstName, lastName, street,
       houseNumber, city, state, zipCode;

// somewhere far away…
Print(state);   // address? order state? US state?
In isolation state is ambiguous. Prefixing helps a bit (addrState) but is a band-aid.
✅ A class provides the context
public class Address
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public string Street { get; init; }
    public string City { get; init; }
    public string State { get; init; }
    public string ZipCode { get; init; }
}

Print(shippingAddress.State);   // unambiguous
Now State can only mean one thing. Bonus: the variables that always travelled together got a concept-name of their own — see argument objects below.

Functions — 10 rules

Functions are the first line of organisation in any program. (History: early systems were built from routines and subroutines, then Fortran-era programs, subprograms and functions — only the function survived.) These ten rules make a function tell a clear story.

1 · Small

The first rule of functions: they should be small. The second rule: they should be smaller than that. Uncle Bob's target is functions of 2–4 lines, rarely more than a screenful. Each function should be transparently obvious, tell one part of the story, and lead compellingly to the next. A 300-line method isn't "thorough" — it's ten functions that never got names.

2 · Blocks and indenting

Corollary of small: the block inside an if, else, or while should be one line — a function call. That call's descriptive name documents what the block does. It also means the indent level of a function should not exceed one or two — deeply nested code means hidden functions are trapped inside.

❌ Nesting three levels deep
public void Pay(List<Employee> employees)
{
    foreach (var e in employees)
    {
        if (e.IsPayday(today))
        {
            var pay = e.Salary / 12 - e.Deductions;
            if (pay > 0)
            {
                bank.Transfer(e.AccountNumber, pay);
                e.LastPaidOn = today;
            }
        }
    }
}
To understand the loop you must simultaneously hold pay maths, banking, and bookkeeping in your head.
✅ Each block is one named call
public void Pay(List<Employee> employees)
{
    foreach (var e in employees)
        if (e.IsPayday(today))
            PayEmployee(e);
}

private void PayEmployee(Employee e)
{
    var pay = CalculateNetMonthlyPay(e);
    if (pay > 0)
        DeliverPay(e, pay);
}
Max indent: two. Each block body is a call whose name says what happens — the code documents itself.

3 · Do one thing

FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. THEY SHOULD DO IT ONLY. (The all-caps are Uncle Bob's.) Two practical tests for "more than one thing":

ℹ️ This is SRP at micro-scale

You've met this idea before at class level: the Single Responsibility Principle — a class should have one reason to change. "Do one thing" is exactly SRP applied to a single function. Clean code and SOLID are the same philosophy at different zoom levels: one unit, one responsibility, one reason to change — whether the unit is a microservice, a class, or a four-line method.

4 · The step-down rule: read top-to-bottom

We want a source file to read like a top-down narrative: every function is followed by those at the next level of abstraction, so you can read the program descending one level at a time. Uncle Bob describes it as a set of "TO paragraphs" — read each function as "TO do X, we do A, then B, then C", where A, B, C are defined just below:

PayrollRun.cs — reads like an outline
// TO run payroll, we pay every employee due today.
public void RunPayroll()
{
    foreach (var employee in EmployeesDueToday())
        PayEmployee(employee);
}

// TO pay an employee, we compute the net pay and deliver it.
private void PayEmployee(Employee employee)
{
    var pay = CalculateNetMonthlyPay(employee);
    DeliverPay(employee, pay);
}

// TO compute net pay, we take the monthly salary minus deductions.
private Money CalculateNetMonthlyPay(Employee employee) =>
    employee.Salary.PerMonth() - employee.Deductions;

// TO deliver pay, we transfer it and record the payment date.
private void DeliverPay(Employee employee, Money pay)
{
    bank.Transfer(employee.AccountNumber, pay);
    employee.LastPaidOn = today;
}

Notice you can stop reading at any depth and still have a correct (if less detailed) understanding — like reading only the headlines, or the headlines plus first paragraphs, of a newspaper.

5 · Use descriptive names

Small functions that do one thing are easy to name — and don't fear long names. A long descriptive name beats a short enigmatic name, and beats a long descriptive comment. Use your naming convention (PascalCase in C#) to make multi-word names readable, and make function + argument form a natural verb/noun pair:

Write(name);          // fine…
WriteField(name);     // …better: says WHAT name is being written

AssertExpectedEqualsActual(expected, actual);  // argument ORDER is now self-evident
IsTestableHtmlPage(pageData);                  // predicate reads as a question

6 · Function arguments: 0 > 1 > 2 > 3 ≫ more

The ideal number of arguments is zero (niladic). Then one (monadic), then two (dyadic). Three should be avoided; more than three requires special justification — and then shouldn't be used anyway. Why so strict? Every argument is another thing the reader must track, another combination to test, and another chance to pass values in the wrong order. Three specific traps:

Flag arguments. Passing true/false into a function loudly proclaims it does two things — one for true, one for false. Split it: Render(true)RenderForSuite() / RenderForSingleTest().
out/ref parameters. Readers expect data to go in through arguments and out through the return value. C#'s out reverses that expectation — prefer returning a result object or a tuple: (bool ok, Order order) TryLoad(...).
Argument order puzzles. CopyChars(a1, a2) — which way does the copy go? Either encode order in the name (AssertExpectedEqualsActual) or reduce the argument count.
❌ Flag argument = two functions in a trench coat
public void SaveDocument(Document doc, bool asDraft)
{
    if (asDraft)
    {
        doc.Status = DocumentStatus.Draft;
        drafts.Add(doc);
    }
    else
    {
        doc.Status = DocumentStatus.Published;
        repository.Store(doc);
        notifier.NotifySubscribers(doc);
    }
}

// Call site — what does 'true' mean here?!
SaveDocument(doc, true);
The boolean forks the function into two unrelated behaviours, and call sites become unreadable.
✅ One function per intention
public void SaveAsDraft(Document doc)
{
    doc.Status = DocumentStatus.Draft;
    drafts.Add(doc);
}

public void Publish(Document doc)
{
    doc.Status = DocumentStatus.Published;
    repository.Store(doc);
    notifier.NotifySubscribers(doc);
}

// Call sites now read as English:
SaveAsDraft(doc);
Publish(doc);
Each function does one thing, and the call site needs no decoding. (If the two paths must share a skeleton, that's Template Method territory.)

7 · Argument objects

When a function seems to need more than two or three arguments, some of them are probably a concept in disguise, waiting for a name. Wrapping them in a class isn't cheating — it's discovering a domain object:

❌ Three loose arguments
Circle MakeCircle(double x, double y, double radius);

// call sites invite x/y mix-ups:
var c = MakeCircle(3.0, radius, 4.0);  // oops — compiles!
x and y always travel together — that's the tell.
✅ The hidden concept gets a name
public record Point(double X, double Y);

Circle MakeCircle(Point center, double radius);

var c = MakeCircle(new Point(3.0, 4.0), radius: 5.0);
Two arguments instead of three, an impossible-to-swap call site, and a reusable Point the rest of the codebase will thank you for. C# record types make this nearly free.

8 · No side effects

Side effects are lies. Your function promises (by its name) to do one thing, but it also does hidden things: mutating fields of its class, modifying its parameters, or touching globals. These lies cause temporal couplings — code that only works if called in a magic order — and bugs that appear far from their cause. Here's the classic example, translated to C#. Spot the bug:

❌ The name promises a check. It delivers a mutation.
public bool CheckPassword(string userName, string password)
{
    User user = userGateway.FindByName(userName);
    if (user != null)
    {
        string codedPhrase = user.GetPhraseEncodedByPassword();
        string phrase = cryptographer.Decrypt(codedPhrase, password);
        if (phrase == "Valid Password")
        {
            Session.Initialize();   // ← the hidden side effect!
            return true;
        }
    }
    return false;
}
The bug it causes: someone calls CheckPassword merely to verify credentials (say, on a settings page) — and their current session data is silently wiped. The crash happens three screens later, nowhere near this function. The name said "check"; the function also initialised a session.
✅ Do what the name says — or say what you do
// Option A: pure check, caller owns the session
public bool IsPasswordValid(string userName, string password)
{
    User user = userGateway.FindByName(userName);
    if (user == null) return false;
    string phrase = cryptographer.Decrypt(
        user.GetPhraseEncodedByPassword(), password);
    return phrase == "Valid Password";
}

// login flow composes the two steps EXPLICITLY:
if (IsPasswordValid(name, pw))
    Session.Initialize();

// Option B (if coupling is truly required): honest name
public bool CheckPasswordAndInitializeSession(...) { }
Option A separates query from command. Option B at least stops lying — though the "And" in the name admits it does two things, which violates do one thing. Prefer A.
💡 Command–Query Separation (CQS)

A function should either do something (a command, changes state, returns nothing) or answer something (a query, returns a value, changes nothing) — never both. IsPasswordValid is a query; Session.Initialize() is a command. Mixing them is how side-effect lies are born.

9 · Prefer exceptions to returning error codes

Returning error codes from command functions subtly violates CQS: it makes commands usable as expressions inside if predicates, and it forces the caller to handle the error immediately, tangling happy path with error path:

❌ Error codes: nested, tangled
if (DeletePage(page) == ErrorCode.Ok)
{
    if (registry.DeleteReference(page.Name) == ErrorCode.Ok)
    {
        if (configKeys.DeleteKey(page.Name.MakeKey()) == ErrorCode.Ok)
            logger.Log("page deleted");
        else
            logger.Log("configKey not deleted");
    }
    else
        logger.Log("deleteReference failed");
}
else
{
    logger.Log("delete failed");
    return ErrorCode.Error;
}
Every call demands an instant verdict; the actual operation drowns in bookkeeping. Also: who forgot to check a code today?
✅ Exceptions: happy path reads straight down
public void DeletePageAndAllReferences(Page page)
{
    try
    {
        DeletePageAndReferences(page);
    }
    catch (PageDeletionException e)
    {
        logger.Log(e.Message);
    }
}

private void DeletePageAndReferences(Page page)
{
    pageStore.Delete(page);
    registry.DeleteReference(page.Name);
    configKeys.DeleteKey(page.Name.MakeKey());
}
Error processing is separated from the happy path. Note the extra refinement from the slides: try/catch blocks are ugly, so extract their bodies into functions of their own — the first function does only error handling, the second only deletion. One thing each.

10 · Don't repeat yourself (DRY)

Duplication may be the root of all evil in software. If the same logic lives in four places, then any change is a four-fold modification — and a four-fold opportunity for an error of omission (fixing three and forgetting the fourth). Huge portions of software practice exist to fight duplication: functions themselves, inheritance and Template Method, generics, and most of the patterns from chapters 3–5.

Crucial nuance: DRY is about duplicated knowledge — one fact, rule, or algorithm represented more than once — not about code that merely looks similar. Two functions can share every character and still not be duplicates, if they're expressing two independent business rules that only coincidentally match today.

❌ Copy-paste-tweak
public double StandardShipping(Order o)
{
    double w = 0;
    foreach (var item in o.Items) w += item.Weight;
    return w * 0.5 + 4.0;
}

public double ExpressShipping(Order o)
{
    double w = 0;
    foreach (var item in o.Items) w += item.Weight;
    return w * 0.5 + 12.0;   // only the fee differs!
}
The weight-summing algorithm exists twice. When volumetric weight rules arrive, will both copies get updated?
✅ One algorithm, one place
private double TotalWeight(Order o) =>
    o.Items.Sum(item => item.Weight);

private double ShippingCost(Order o, double baseFee) =>
    TotalWeight(o) * RatePerKg + baseFee;

public double StandardShipping(Order o) => ShippingCost(o, 4.0);
public double ExpressShipping(Order o) => ShippingCost(o, 12.0);
The algorithm has exactly one home. (If shipping strategies multiply, promote them to a Strategy.)
💡 AHA — avoid hasty abstractions

Don't over-correct. Extracting a shared function the moment two code blocks look alike — before you know whether they represent the same knowledge — creates a different mess: one abstraction awkwardly serving two unrelated rules, with parameters and flags bolted on every time one rule changes and the other mustn't. If you're not sure two blocks are the same fact in disguise, it's often cheaper to leave the duplication for now and merge them once a real change forces the question — prefer duplication over the wrong abstraction.

⚡ Cleanup workshop: watch a mess become clean

Theory is easy to nod along to. Let's apply the rules one refactoring at a time to a genuinely messy function — the kind you will actually inherit. It computes an order total: negative "prices" are corrections to be skipped, members get a discount, and orders over a threshold get the shipping fee refunded. You'd never guess any of that from the code:

Try it: step-by-step cleanup of a messy C# function
Step 0

What to notice: highlighted lines changed in the current step. No single step is clever — renames, constants, extractions — yet after five tiny, safe steps the function needs zero comments and reads top-down like prose. This is the Boy Scout Rule in action: each step alone would already have left the campground cleaner.

Comments — a necessary evil

Uncle Bob's most provocative claim: "comments are always failures." Not because explanations are bad — but because a comment is what we write when we fail to express ourselves in code. If our languages were expressive enough and we wielded them well, we'd hardly need comments at all. Nothing helps like a well-placed comment; nothing clutters like a frivolous one; and nothing damages like a stale comment propagating lies — because comments don't compile, don't run, and don't fail tests when they drift out of date. The code moves; the comment stays behind and starts lying.

Comments don't make up for bad code

The most common motivation for a comment is: "this code is confusing, let me explain it." Don't explain the mess — clean the mess. In most cases, it's simply a matter of creating a function or variable that says the same thing as the comment you wanted to write:

❌ Comment explains cryptic code
// Check to see if the employee is eligible
// for full benefits
if ((employee.Flags & HourlyFlag) != 0 &&
    employee.Age > 65)
{
    GrantFullBenefits(employee);
}
The comment is doing the code's job. And when the eligibility rule changes, will the comment change too?
✅ The comment became a method name
if (employee.IsEligibleForFullBenefits)
{
    GrantFullBenefits(employee);
}

// inside Employee:
public bool IsEligibleForFullBenefits =>
    IsHourly && Age > RetirementAge;
Same information, but now it's code: the compiler checks it, refactoring tools rename it, and it can never silently go stale. The comment was refactored out of existence.

Good comments — the few that earn their place

⚖️ Legal comments. Copyright and license headers required by law or policy.
// Copyright (C) 2026 UCSC. Released under
// the MIT license. See LICENSE.md.
🎯 Explanation of intent. The why behind a decision that code cannot express.
// Sort a copy: callers rely on the original
// order for the audit trail (reg. 44/2019).
var sorted = orders.OrderBy(o => o.Total).ToList();
⚠️ Warning of consequences. Save a colleague from a trap.
// WARNING: takes ~40 minutes against the full
// production dataset. Run only against a snapshot.
[Trait("Category", "SlowIntegration")]
public void RebuildEntireSearchIndex() { }
📌 TODO comments. A legitimate note about work that should be done but can't be right now — with enough context for a stranger to act on.
// TODO: remove this fallback once all clients
// are on API v3 (tracked in JIRA SCS-412).
if (request.Version < 3) return LegacyResponse();

Bad comments — the usual suspects

❌ Redundant comment
// Waits for the lock to be available,
// then acquires the lock.
public void AcquireLock()
{
    lockHandle.Wait();
    lockHandle.Acquire();
}
Takes longer to read than the code, adds zero information, and is one refactor away from being a lie.
❌ Mandated / noise comments
/// <summary>The name.</summary>
public string Name { get; set; }

/// <summary>The age.</summary>
public int Age { get; set; }

/// <summary>Default constructor.</summary>
public Customer() { }
A rule that says "every member must have a doc comment" produces this clutter — obfuscation, plus the standing potential for lies. Document public library APIs where docs add real information; never mandate boilerplate.
❌ Mumbling / historical essays
// I'm not sure why this needs to be 3.
// It used to be 2 before the migration in 2019
// when Kasun changed the batching (see email).
// Anyway don't touch it.
int retryCount = 3;
A comment written because you felt you should, not because it communicates. If you write a comment, spend the time to make it the best comment you can write — or delete it.
❌ Commented-out code
var total = CalculateTotal(order);
// var total = CalculateTotalV2(order);
// ApplyLegacyDiscount(order, total);
// if (order.Region == "EU") total *= 1.2;
return total;
Nobody dares delete it ("it must be there for a reason") so it accumulates like sediment. Delete it — version control remembers everything; that's its job.
⚠️ The comment litmus test

Before writing a comment, ask: "Can I express this in code instead?" — a better name, an extracted method, an explanatory variable, a named constant. Only when the answer is genuinely no (intent, warnings, legal, TODO) does the comment earn its place.

⚡ Smell detector: name that violation

Time to test your nose. For each snippet, click the clean-code rule it violates most directly. (Several answers may be arguably wrong with the code — pick the primary smell.)

Try it: which rule does this snippet violate?
What to notice: spotting smells in someone else's code is easy — the skill transfers when you review your own diffs with the same eyes before every commit. That habit is the Boy Scout Rule.

Formatting — communication through layout

When people look under the hood, we want them impressed by neatness, consistency, and attention to detail — because code formatting is about communication, and communication is the professional developer's first order of business. Here's the surprising part: your style and discipline survive even after your code doesn't. The functions you write today will be rewritten beyond recognition; the readability standards you set keep shaping the codebase long after.

Vertical formatting: openness and density

Nearly all code is read left to right, top to bottom, like a newspaper article: name (headline) at the top, the big picture first, detail increasing as you descend. Two complementary rules govern the vertical dimension:

❌ No breathing room — one grey wall
public class ReportGenerator
{
    private readonly IOrderRepository orders;
    public ReportGenerator(IOrderRepository orders)
    { this.orders = orders; }
    public Report GenerateMonthly(int year, int month)
    {
        var range = DateRange.ForMonth(year, month);
        var monthlyOrders = orders.InRange(range);
        var totals = Summarize(monthlyOrders);
        var report = new Report(range, totals);
        report.AddFooter(DateTime.UtcNow);
        return report;
    }
}
Field, constructor, and method blur into one block; the reader's eye has no anchors.
✅ Blank lines separate complete thoughts
public class ReportGenerator
{
    private readonly IOrderRepository orders;

    public ReportGenerator(IOrderRepository orders)
    {
        this.orders = orders;
    }

    public Report GenerateMonthly(int year, int month)
    {
        var range = DateRange.ForMonth(year, month);
        var monthlyOrders = orders.InRange(range);

        var totals = Summarize(monthlyOrders);

        var report = new Report(range, totals);
        report.AddFooter(DateTime.UtcNow);
        return report;
    }
}
Blank lines mark thought boundaries: gather data / summarise / build report. Within each thought, lines stay dense.

Dependent functions: caller above callee

If one function calls another, they should be vertically close, with the caller above the callee. This gives the program the natural top-down flow you saw in the step-down rule — and when the convention is followed reliably, readers learn to trust that a function's definition appears shortly after its first use, so they stop scroll-hunting. The PayrollRun example above is formatted exactly this way: RunPayrollPayEmployeeCalculateNetMonthlyPay / DeliverPay.

Horizontal formatting

Horizontal white space associates things that are strongly related and disassociates things that are weakly related:

int netPay = salary - deductions;   // spaces accentuate the assignment's
                                    // two sides: left = right

DeliverPay(employee, netPay);       // NO space between function name and (
                                    // — the function and its args are one unit;
                                    // spaces AFTER commas separate the args

return (a * b) + (c * d);           // tighter spacing inside higher-precedence
                                    // terms (multiplication binds first)

Keep lines short — the modern convention is 100–120 characters; if you have to scroll sideways, the line is doing too much. And don't horizontally align declarations into columns:

❌ Column alignment (looks tidy, reads badly)
private   Socket        socket;
private   InputStream   input;
private   long          requestProgress;
protected List<Header>  headers;
Your eye reads the column of names and ignores the types — the alignment defeats its own purpose, and every rename re-shuffles the whitespace.
✅ Plain, unaligned declarations
private Socket socket;
private InputStream input;
private long requestProgress;
protected List<Header> headers;
If the list is so long that alignment seems needed, the problem is the length of the list, not the alignment.

Indentation: the hierarchy made visible

A source file is a hierarchy — an outline. Each level (namespace → class → method → block) is a scope, and we indent in proportion to position in that hierarchy so the structure is visible at a glance. Never collapse indentation to "save space": if (x) { y(); return z; } on one line hides the structure your eye navigates by.

Team rules

💡 Every programmer has favourite rules — but if you work in a team, the team rules

The goal is software with one consistent style, not something that looks written by a bunch of disagreeing individuals. Agree once on a single set of formatting rules, then everyone complies — and automate it so nobody wastes review time on brace positions. In .NET: an .editorconfig file checked into the repository plus dotnet format (or IDE format-on-save) enforces the team style mechanically. Arguments about formatting are the cheapest ones to eliminate.

Unit testing & the three laws of TDD

Clean code needs one more ingredient: the confidence to change it. Every refactoring in this chapter — renames, extractions, restructurings — is only safe if a test suite verifies that behaviour didn't change. Without tests, code rots even faster, because everyone is too scared to clean it. The Agile and TDD (Test-Driven Development) movements made automated unit tests mainstream — but many programmers, rushing to adopt testing, missed the subtle points of doing it well.

The three laws of TDD

TDD is not "write tests at some point". It's a strict, tiny loop, defined by three laws:

First law. You may not write production code until you have written a failing unit test.
The tests drive the code: you only ever write code that some test demanded.
Second law. You may not write more of a unit test than is sufficient to fail — and not compiling is failing.
Write the smallest possible failing test. The moment it fails (even by not compiling), stop and switch to production code.
Third law. You may not write more production code than is sufficient to pass the currently failing test.
Write the minimum — even an embarrassing hard-coded return. The next test will force the generalisation.

Working this way, tests and production code are written together, the tests just seconds ahead. You'll write dozens of tests a day, thousands a year, covering virtually all production code. That bulk is a management problem of its own, which leads to a rule people forget: test code is just as important as production code. It is not a second-class citizen — it requires thought, design, and care, and must be kept as clean as production code, because dirty tests are abandoned tests, and abandoned tests mean the production code can no longer be safely changed.

1 · RED write a small failing test 2 · GREEN make it pass — minimally 3 · REFACTOR clean up; tests stay green just enough code to pass now it's safe to clean up repeat with the next tiny test
Figure: the red–green–refactor cycle. One full lap takes a minute or two — the three laws keep each lap tiny.

F.I.R.S.T. — what makes a unit test good

PropertyMeaningWhy it matters
FastTests run in milliseconds.Slow tests don't get run; tests that don't get run don't catch bugs.
IndependentNo test depends on another test's leftovers or order.One failure shouldn't cascade into thirty mysterious failures.
RepeatableSame result on any machine, any network, any time of day."Works on my machine" tests train the team to ignore red builds.
Self-validatingThe test outputs pass or fail — no reading logs to judge.Manual verification doesn't scale past ten tests.
TimelyWritten just before the production code (that's TDD).Code written first tends to be untestable — testability must be designed in.

A full red–green–refactor walkthrough

Let's build a tiny StringCalculator the strict-TDD way, with xUnit (the most common .NET test framework — [Fact] marks a test method, Assert.Equal(expected, actual) checks a value). Requirement: Add("") → 0, Add("7") → 7, Add("2,3") → 5. Watch the three laws steering every keystroke:

StringCalculatorTests.cs
using Xunit;

public class StringCalculatorTests
{
    [Fact]
    public void Add_EmptyString_ReturnsZero()
    {
        var calculator = new StringCalculator();

        int result = calculator.Add("");

        Assert.Equal(0, result);
    }
}

RED. This doesn't even compile — StringCalculator doesn't exist yet. By the second law, not compiling is failing, so we must stop writing test code right here and switch to production code. (Note the test's shape: Arrange, Act, Assert — set up, do the thing, check the result. Clean tests read in those three beats.)

StringCalculator.cs
public class StringCalculator
{
    public int Add(string numbers)
    {
        return 0;
    }
}

GREEN. return 0; — yes, really. The third law forbids writing more production code than needed to pass the currently failing test, and this passes it. It feels like cheating; it isn't. The hard-coded value is a placeholder that the next test will force us to generalise. Resisting the urge to "just write the real thing" is what keeps TDD steps small and safe.

Red: the next smallest test
[Fact]
public void Add_SingleNumber_ReturnsThatNumber()
{
    var calculator = new StringCalculator();

    int result = calculator.Add("7");

    Assert.Equal(7, result);
}

Fails: Add returns 0. Now — minimum code to pass both tests:

Green
public int Add(string numbers)
{
    if (numbers == "")
        return 0;
    return int.Parse(numbers);
}

Both tests green. Nothing speculative added — no comma handling yet, because no test demands it.

Red: two numbers
[Fact]
public void Add_TwoNumbers_ReturnsTheirSum()
{
    var calculator = new StringCalculator();

    int result = calculator.Add("2,3");

    Assert.Equal(5, result);
}

Fails — int.Parse("2,3") throws. Minimum to pass all three:

Green
public int Add(string numbers)
{
    if (numbers == "")
        return 0;

    string[] parts = numbers.Split(',');
    int sum = 0;
    foreach (string part in parts)
        sum += int.Parse(part);
    return sum;
}

All green. The code works — but it has two branches and a manual accumulation loop. Time for the step people skip.

Refactor — with the tests as a safety net
using System.Linq;

public class StringCalculator
{
    public int Add(string numbers) =>
        numbers
            .Split(',', StringSplitOptions.RemoveEmptyEntries)
            .Sum(int.Parse);
}

REFACTOR. RemoveEmptyEntries makes the empty string yield an empty sequence, whose Sum is 0 — so the special case disappears. Re-run the tests: all three still green, which is the proof the refactoring preserved behaviour. This is the payoff of the whole chapter: because the tests exist, we could restructure fearlessly. Tests enable clean code; clean code keeps tests readable. It's one discipline.

⚠️ Common TDD misunderstanding

TDD is not "write all the tests first, then all the code". That violates the second law (you wrote far more test than needed to fail) and produces a wall of red that's demoralising to dig out of. The laws force an interleaved rhythm: one tiny test, a few lines of code, repeat — the tests stay seconds ahead of the code, never miles.

Clean code and the rest of the module

Clean code isn't a separate subject bolted onto architecture — it's the same principles at the smallest zoom level:

Clean-code ruleLarge-scale siblingShared idea
A function does one thingSRP for classes; one capability per microservice (ch. 9)One unit, one responsibility, one reason to change
Replace a flag argument with separate methodsOCP / Strategy: vary behaviour without if forksDon't branch on a mode — give each mode its own home
Argument objects (x, y → Point)Abstraction: find the concept, name it, encapsulate itData that travels together is a concept in disguise
DRY at line levelTemplate Method, generics, inheritanceEvery fact in the system has exactly one home
No side effects, CQSEncapsulation; honest interfaces (ch. 2)A unit's surface must tell the truth about its behaviour
Solution-domain namesPattern vocabulary from chapters 3–5Shared names compress communication

The takeaway: when you internalise SOLID, "do one thing" stops being a rule you memorise and becomes the only way code looks right to you — at every scale from a four-line method to a fleet of services.

Check yourself — Clean Code

ℹ️ Where to next

You now have design principles (ch. 2), patterns (ch. 3–6), and line-level craftsmanship (this chapter). Chapter 8 zooms out the other way — from lines to whole-system architectural patterns. And as Shakespeare (via the slides) almost said: a rose by any other name may smell as sweet — but a variable by a bad name will cost you an afternoon.