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.
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.
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:
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
ifstatement.
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.
int d; // elapsed time in days
if (d > 30)
ArchiveFile();d? You must scroll back to the comment — and the comment can lie after the next edit.int elapsedTimeInDays;
// or, depending on what it really means:
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
if (fileAgeInDays > 30)
ArchiveFile();Intent-revealing names transform whole functions. Both versions below are the same algorithm over the same data structure:
public List<int[]> GetThem()
{
var list1 = new List<int[]>();
foreach (int[] x in theList)
if (x[0] == 4)
list1.Add(x);
return list1;
}theList? Why cell [0]? Why the value 4?public List<Cell> GetFlaggedCells()
{
var flaggedCells = new List<Cell>();
foreach (Cell cell in gameBoard)
if (cell.IsFlagged)
flaggedCells.Add(cell);
return flaggedCells;
}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:
// it is NOT a List — it's a dictionary!
Dictionary<string, Account> accountList =
LoadAccounts();
double hp = Math.Sqrt(a * a + b * b);accountList will assume ordering and index access — false conclusions, wasted debugging.Dictionary<string, Account> accountsById =
LoadAccounts();
// or simply: accounts, accountGroup
double hypotenuse = Math.Sqrt(a * a + b * b);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:
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?Info/Data are noise words — like a, an, the — they distinguish the names without distinguishing the concepts.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 levels4 · 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:
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.class CustomerRecord
{
public DateTime GenerationTimestamp { get; set; }
public DateTime ModificationTimestamp { get; set; }
public string RecordId { get; set; }
}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.
for (int j = 0; j < 34; j++)
{
s += (t[j] * 4) / 5;
}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;
}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.
class ProcessOrders { } // a verb — that's a method's job
class OrderManager { } // manages… what, exactly? everything?
class CustomerData { } // noise wordclass OrderProcessor { } // ok: an agent-noun, one job
class OrderRepository { } // stores/loads orders
class InvoiceCalculator { } // computes invoices
class Customer { }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.
invoice.Payment(100); // is it paying? fetching a payment?
page.Deletion(); // ??
if (user.Admin()) { } // reads like a nouninvoice.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()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.
class ThingThatWalksThroughAccounts { }
class ListOfWaitingPrintTasks { }
class OrderHelperUtility { }class AccountVisitor { } // Visitor pattern — chapter 5
class PrintJobQueue { } // everyone knows a queue
class OrderFactory { } // Factory — chapter 39 · 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:
string firstName, lastName, street,
houseNumber, city, state, zipCode;
// somewhere far away…
Print(state); // address? order state? US state?state is ambiguous. Prefixing helps a bit (addrState) but is a band-aid.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); // unambiguousState 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.
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;
}
}
}
}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);
}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":
- You can extract another function from it whose name is not merely a
restatement of its implementation. (
SumChargeableItemsextracted fromCalculateOrderTotal— meaningful.LoopOverItems— just a restatement.) - The function is divided into sections — declarations, initialisation, processing, output. A function that does one thing can't reasonably be split into sections.
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:
// 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:
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(...).CopyChars(a1, a2) — which way
does the copy go? Either encode order in the name (AssertExpectedEqualsActual) or reduce
the argument count.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);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);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:
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.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);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:
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;
}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.// 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(...) { }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:
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;
}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());
}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.
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!
}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);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:
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:
// Check to see if the employee is eligible
// for full benefits
if ((employee.Flags & HourlyFlag) != 0 &&
employee.Age > 65)
{
GrantFullBenefits(employee);
}if (employee.IsEligibleForFullBenefits)
{
GrantFullBenefits(employee);
}
// inside Employee:
public bool IsEligibleForFullBenefits =>
IsHourly && Age > RetirementAge;Good comments — the few that earn their place
// Copyright (C) 2026 UCSC. Released under
// the MIT license. See LICENSE.md.// 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: takes ~40 minutes against the full
// production dataset. Run only against a snapshot.
[Trait("Category", "SlowIntegration")]
public void RebuildEntireSearchIndex() { }// 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
// Waits for the lock to be available,
// then acquires the lock.
public void AcquireLock()
{
lockHandle.Wait();
lockHandle.Acquire();
}/// <summary>The name.</summary>
public string Name { get; set; }
/// <summary>The age.</summary>
public int Age { get; set; }
/// <summary>Default constructor.</summary>
public Customer() { }// 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;var total = CalculateTotal(order);
// var total = CalculateTotalV2(order);
// ApplyLegacyDiscount(order, total);
// if (order.Region == "EU") total *= 1.2;
return total;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.)
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:
- Vertical openness: each group of lines represents a complete thought; separate thoughts with blank lines.
- Vertical density: lines that are tightly related should sit tightly together — no blank lines or comment clutter inside one thought.
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;
}
}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;
}
}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:
RunPayroll → PayEmployee → CalculateNetMonthlyPay /
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:
private Socket socket;
private InputStream input;
private long requestProgress;
protected List<Header> headers;private Socket socket;
private InputStream input;
private long requestProgress;
protected List<Header> headers;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
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:
The tests drive the code: you only ever write code that some test demanded.
Write the smallest possible failing test. The moment it fails (even by not compiling), stop and switch to production code.
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.
F.I.R.S.T. — what makes a unit test good
| Property | Meaning | Why it matters |
|---|---|---|
| Fast | Tests run in milliseconds. | Slow tests don't get run; tests that don't get run don't catch bugs. |
| Independent | No test depends on another test's leftovers or order. | One failure shouldn't cascade into thirty mysterious failures. |
| Repeatable | Same result on any machine, any network, any time of day. | "Works on my machine" tests train the team to ignore red builds. |
| Self-validating | The test outputs pass or fail — no reading logs to judge. | Manual verification doesn't scale past ten tests. |
| Timely | Written 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:
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.)
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.
[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:
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.
[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:
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.
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.
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 rule | Large-scale sibling | Shared idea |
|---|---|---|
| A function does one thing | SRP for classes; one capability per microservice (ch. 9) | One unit, one responsibility, one reason to change |
| Replace a flag argument with separate methods | OCP / Strategy: vary behaviour without if forks | Don't branch on a mode — give each mode its own home |
Argument objects (x, y → Point) | Abstraction: find the concept, name it, encapsulate it | Data that travels together is a concept in disguise |
| DRY at line level | Template Method, generics, inheritance | Every fact in the system has exactly one home |
| No side effects, CQS | Encapsulation; honest interfaces (ch. 2) | A unit's surface must tell the truth about its behaviour |
| Solution-domain names | Pattern vocabulary from chapters 3–5 | Shared 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
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.