Book summary · Software craftsmanship · Kotlin
Clean Code, Distilled: What I Learned from Robert C. Martin’s Classic
I read Clean Code by Robert C. Martin (“Uncle Bob”) and it changed how I name things, how long I let functions grow, and how I feel about comments. This post is my own summary of the ideas that stuck with me. For each one I show a bad and a clean version in Kotlin, because the book’s examples are in Java and a principle is easier to remember when you can see the difference.
- Code is read far more often than it is written, so write for the reader.
- Names should say what a thing is or does. Magic numbers get names too.
- Functions should be small, do one thing, and avoid flag arguments and hidden side effects.
- Prefer clear code over comments. Keep comments for intent and warnings.
- Hide internals, and tell objects what to do instead of reaching through them.
- Tests are code too: keep them readable, fast, independent and focused.
- Use exceptions with context, wrap third-party code, keep classes focused, build objects in one place, and share as little mutable state as you can.
- Leave every file a little cleaner than you found it.
Contents
- About the book and where to get it
- The big idea: write for the reader (Chapter 1)
- Meaningful names (Chapter 2)
- Functions (Chapter 3)
- Comments (Chapter 4)
- Formatting (Chapter 5)
- Objects, data and the Law of Demeter (Chapter 6)
- Error handling (Chapter 7)
- Boundaries (Chapter 8)
- Unit tests (Chapter 9)
- Classes (Chapter 10)
- Systems (Chapter 11)
- Emergence (Chapter 12)
- Concurrency (Chapter 13)
- Successive refinement (Chapter 14)
- JUnit internals (Chapter 15)
- Refactoring SerialDate (Chapter 16)
- Smells and heuristics (Chapter 17)
- Conditionals and understandability (from Chapter 17)
- Where I use judgement
- Takeaways, checklist and references
1. About the book and where to get it
Clean Code: A Handbook of Agile Software Craftsmanship was written by Robert C. Martin with contributions from several co-authors and published by Prentice Hall (Pearson) in 2008. It has three parts: principles and patterns for writing good code, long case studies that clean up real code step by step, and a final catalogue of “smells” and heuristics. The examples are in Java.
| Detail | Value |
|---|---|
| Title | Clean Code: A Handbook of Agile Software Craftsmanship |
| Author | Robert C. Martin |
| ISBN-10 / ISBN-13 | 0132350882 / 978-0132350884 |
| Buy the book | Amazon · InformIT (publisher’s store) |
About “downloading”: the book is under copyright, so the official ways to get it are to buy a print copy or an authorised e-book from the links above. I do not link to free copies. If you want a short checklist of the rules first, the community Clean Code summary on GitHub by wojteklu is a good outline, and it shaped the order of this post.
This is my own retelling in my own words, with my own Kotlin examples. It is a map, not a substitute: the book’s long refactoring walk-throughs are where most of the learning happens.
Where each chapter is in this post
| Book chapter | Section here |
|---|---|
| 1. Clean Code | The big idea |
| 2. Meaningful Names | Meaningful names |
| 3. Functions | Functions |
| 4. Comments | Comments |
| 5. Formatting | Formatting |
| 6. Objects and Data Structures | Objects, data and the Law of Demeter |
| 7. Error Handling | Error handling |
| 8. Boundaries | Boundaries |
| 9. Unit Tests | Unit tests |
| 10. Classes | Classes |
| 11. Systems | Systems |
| 12. Emergence | Emergence |
| 13. Concurrency | Concurrency |
| 14. Successive Refinement | Successive refinement |
| 15. JUnit Internals | JUnit internals |
| 16. Refactoring SerialDate | Refactoring SerialDate |
| 17. Smells and Heuristics | Smells and heuristics, Conditionals and understandability |
2. The big idea: write for the reader (Chapter 1)
The book’s starting point is a simple observation: we spend much more time reading code than writing it, because every change begins with reading what is already there. Martin puts the ratio at well over ten to one. So anything that makes code quicker to understand pays off many times over.
Three general rules follow, and I keep them in my head more than any other:
- Follow the team’s conventions. Consistency beats personal taste.
- Keep it simple. If two designs work, take the one that is easier to explain.
- The Boy Scout rule: leave the code a little cleaner than you found it. No giant clean-ups, just one better name or one extracted function each time you pass through.
- Find the root cause. A patch that hides a symptom will come back.
3. Meaningful names (Chapter 2)
A name is a tiny comment that cannot go out of date, if you choose it well. The book asks for names that reveal intent, are pronounceable and searchable, and do not make the reader decode abbreviations. It also asks for real differences: data and info, or getAccount and getAccountInfo, tell the reader nothing about how they differ.
Bad
fun calc(d: List<Int>, t: Int): Int {
var r = 0
for (x in d) {
if (x > t) r += x
}
return r
}
Clean
fun totalOfOrdersAbove(minimum: Int, orderTotals: List<Int>): Int =
orderTotals.filter { it > minimum }.sum()
Same behaviour. The second version tells you what it computes before you read a line of the body. Short names are fine in tiny scopes (a loop index, a lambda’s it), but the wider the scope, the more descriptive the name should be.
Magic numbers get names
// Bad
if (user.age >= 18 && user.status == 2) { allowPurchase() }
// Clean
const val ADULT_AGE = 18
enum class Status { PENDING, ACTIVE, SUSPENDED }
if (user.age >= ADULT_AGE && user.status == Status.ACTIVE) { allowPurchase() }
4. Functions (Chapter 3)
The book’s rules for functions are short and strict: small, do one thing, a descriptive name, few arguments, no side effects, and no flag arguments. A boolean flag is a sign the function does two things, one for each value.
Bad
fun processOrder(order: Order, sendEmail: Boolean) {
if (order.items.isEmpty()) throw IllegalArgumentException("Empty order")
var total = 0.0
for (item in order.items) total += item.price * item.quantity
order.total = total // hidden change to the input
database.save(order)
if (sendEmail) {
mailer.send(order.customerEmail, "Thanks! You paid $total")
}
}
This function validates, calculates, mutates its argument, saves, and sometimes emails. The flag sendEmail tells you it has two modes.
Clean
fun placeOrder(order: Order): PlacedOrder {
requireNotEmpty(order)
val placed = PlacedOrder(order, total = totalOf(order))
orders.save(placed)
return placed
}
fun sendReceipt(placed: PlacedOrder) {
mailer.send(placed.customerEmail, "Thanks! You paid ${placed.total}")
}
private fun requireNotEmpty(order: Order) =
require(order.items.isNotEmpty()) { "Empty order" }
private fun totalOf(order: Order): Double =
order.items.sumOf { it.price * it.quantity }
Each function now fits in your head, has a name that says what it does, and the caller decides whether to send a receipt. The original order is not changed behind your back.
5. Comments (Chapter 4)
The book’s attitude is that a comment is often an apology for code that is not clear enough. Before writing one, try to put the idea into a name or a small function. Comments that repeat the code, closing-brace markers, and commented-out code (which version control already remembers) are all noise. Good comments explain why, give a warning, or note a decision that the code cannot show.
Bad
// check if the employee is eligible for full benefits
if (employee.flags and HOURLY_FLAG != 0 && employee.age > 65) {
i++ // increment i
}
// val oldRate = 0.08
// applyRate(oldRate)
Clean
if (employee.isEligibleForFullBenefits()) {
benefitsGranted++
}
// The payment provider rejects more than 5 requests per second,
// so we pause between calls. Remove when we move to the batch API.
Thread.sleep(250)
The first comment disappeared because the function name says it. The last one stays because no name could explain a rule that lives in someone else’s system.
6. Formatting (Chapter 5)
Formatting is communication. The book’s advice is practical:
- Keep related code close and separate unrelated ideas with a blank line.
- Declare variables near where they are used.
- Put a function below the code that calls it, so a file reads top to bottom like a newspaper story.
- Keep lines short and do not align things in columns.
- Agree on one style with your team and let a formatter enforce it. In Kotlin that means
ktlintor the IDE formatter, so nobody argues about it in review.
7. Objects, data and the Law of Demeter (Chapter 6)
Objects should hide their insides and expose behaviour. Data structures should expose their data and have little behaviour. The trouble starts with hybrids: classes with public fields and logic, where every caller edits the fields and the rules end up scattered.
Bad
class Account { var balance: Double = 0.0 }
// Somewhere else, and again somewhere else:
if (account.balance >= 50.0) {
account.balance -= 50.0
}
Clean
class Account(balance: Double) {
var balance: Double = balance
private set
fun withdraw(amount: Double) {
require(amount > 0) { "Amount must be positive" }
check(balance >= amount) { "Insufficient funds" }
balance -= amount
}
}
account.withdraw(50.0)
The rule “you cannot overdraw” now lives in one place. Callers tell the account what to do; they do not reach in and decide for it.
The Law of Demeter
A method should talk to its direct collaborators, not to the collaborators of its collaborators. A long chain of calls means your code knows too much about how other classes are built.
// Bad: the caller knows the whole structure
val city = order.customer.address.city.name
// Clean: ask the object that owns the knowledge
val city = order.shippingCity()
8. Error handling (Chapter 7)
Error handling should not bury the real logic. The book’s advice: use exceptions instead of return codes, give them enough context to be useful, define exception types around what the caller needs, and do not return or pass null to represent “something went wrong”.
Bad
// 0 = ok, 1 = insufficient funds, 2 = unknown account
fun withdraw(id: String, amount: Double): Int {
val account = accounts[id] ?: return 2
if (account.balance < amount) return 1
account.balance -= amount
return 0
}
when (withdraw("A-1", 50.0)) { // every caller must remember the codes
1 -> showMessage("Not enough money")
2 -> showMessage("No such account")
}
Clean
class AccountNotFoundException(id: String) : Exception("No account with id $id")
class InsufficientFundsException(val needed: Double, val available: Double) :
Exception("Needed $needed but only $available available")
fun withdraw(id: String, amount: Double) {
val account = accounts[id] ?: throw AccountNotFoundException(id)
account.withdraw(amount) // throws InsufficientFundsException
}
try {
withdraw("A-1", 50.0)
} catch (e: InsufficientFundsException) {
showMessage("Short by ${e.needed - e.available}")
} catch (e: AccountNotFoundException) {
showMessage("No such account")
}
Do not return null for “nothing special”: use a Special Case object
Kotlin makes null visible in the type (User?), which already helps a lot. Still, forcing every caller to check can be avoided when there is a sensible default behaviour:
// Bad: every caller must handle null
val discount: Discount? = customer.discount
val price = if (discount != null) discount.apply(total) else total
// Clean: a Special Case object with the same interface
object NoDiscount : Discount { override fun apply(total: Double) = total }
val discount: Discount = customer.discount ?: NoDiscount
val price = discount.apply(total)
Other points from the chapter: start by writing the try block (and its tests) so you decide what the operation guarantees, and avoid passing null into functions. In Kotlin, simply avoid nullable parameters unless null truly means something.
9. Boundaries (Chapter 8)
A boundary is where your code meets code you do not control: a library, an SDK, another team’s service. The advice is to keep that third-party code at arm’s length. Wrap it behind a small interface you own, so only one class knows the vendor’s details, and a vendor change touches one file.
Bad
// Vendor details leak into business code, in many places
fun checkout(order: Order) {
val response = VendorClient(API_KEY).charges.create(
mapOf("amount" to (order.total * 100).toInt(), "currency" to "usd")
)
if (response["status"] != "succeeded") error("Payment failed")
}
Clean
interface PaymentGateway { fun charge(amount: Money): PaymentResult }
class VendorPaymentGateway(private val client: VendorClient) : PaymentGateway {
override fun charge(amount: Money): PaymentResult {
val response = client.charges.create(
mapOf("amount" to amount.inCents(), "currency" to amount.currency)
)
return if (response["status"] == "succeeded") PaymentResult.Paid else PaymentResult.Declined
}
}
fun checkout(order: Order, payments: PaymentGateway) {
check(payments.charge(order.total) == PaymentResult.Paid) { "Payment failed" }
}
Two more ideas in this chapter:
- Learning tests. Write small tests that check how a third-party library behaves, as you learn it. They cost little and turn into an alarm when a library upgrade changes behaviour.
- Code that does not exist yet. If another team has not built their part, define the interface you wish they offered and code against it, then adapt it later.
@Test
fun `the library parses ISO dates`() {
assertEquals(LocalDate.of(2026, 10, 15), LocalDate.parse("2026-10-15"))
}
10. Unit tests (Chapter 9)
The book treats test code as first-class code. Tests should be F.I.R.S.T.: Fast, Independent, Repeatable, Self-validating, and Timely. A test should check one concept, and its name should read like a sentence about the behaviour.
Bad
@Test
fun test1() {
val cart = Cart()
cart.add(Item("pen", 10))
assertEquals(10, cart.total())
cart.add(Item("book", 20))
assertEquals(30, cart.total())
cart.clear()
assertEquals(0, cart.total())
}
If this fails, which of three behaviours is broken? The name does not say.
Clean
@Test
fun `an empty cart has a total of zero`() {
assertEquals(0, Cart().total())
}
@Test
fun `total is the sum of all item prices`() {
val cart = Cart().apply { add(Item("pen", 10)); add(Item("book", 20)) }
assertEquals(30, cart.total())
}
@Test
fun `clearing the cart resets the total`() {
val cart = Cart().apply { add(Item("pen", 10)) }
cart.clear()
assertEquals(0, cart.total())
}
“One assert per test” is a guideline, not a law. What matters is one concept per test, so a failure points to one cause.
11. Classes (Chapter 10)
Functions should be small, and so should classes, but the book measures a class by responsibilities, not lines. A class should have one reason to change (the Single Responsibility Principle), and its methods should use its fields, which is called cohesion. When a few methods use a few fields and ignore the rest, there is a second class trying to get out.
Bad
class UserManager {
fun logIn(name: String, password: String): Boolean { /* ... */ return true }
fun monthlyReport(): String { /* builds a report */ return "" }
fun sendWelcomeEmail(user: User) { /* talks to SMTP */ }
}
Clean
class Authenticator { fun logIn(name: String, password: String): Boolean { /* ... */ return true } }
class UserReport { fun monthly(): String { /* ... */ return "" } }
class WelcomeMailer { fun send(user: User) { /* ... */ } }
- Organise a class top to bottom: constants, then properties, then public functions, then the private helpers each public function uses.
- Organise for change. Make adding a feature mean adding a class, not editing several (Open/Closed).
- Isolate from change. Depend on interfaces, not concrete details (Dependency Inversion), so a change elsewhere does not ripple into you.
I wrote about these two principles with exercises in the SOLID post.
12. Systems (Chapter 11)
Clean code at the scale of a whole application starts with one separation: building things is a different job from using them. If a class creates its own collaborators, it is welded to them. Create objects in one place at startup and hand them in.
Bad
class OrderService {
private val repository = SqlOrderRepository() // hard-wired, hard to test
}
Clean
class OrderService(private val repository: OrderRepository)
// One place builds the object graph (the "composition root")
fun main() {
val service = OrderService(SqlOrderRepository())
// ...
}
The same chapter covers a few related ideas:
- Dependency injection is how the pieces are handed in. You can do it by hand, as above, or with a framework.
- Grow the system incrementally. Build for today’s requirements and keep the design clean enough to extend, rather than designing everything upfront.
- Cross-cutting concerns such as logging, transactions and security should not be tangled into business code. Frameworks offer proxies or aspects for this.
- Postpone decisions until the last responsible moment, when you know more.
- Use standards wisely, when they add real value, not just because they are popular.
- Domain-specific languages can let domain experts read code. Kotlin’s builder syntax is a good example.
13. Emergence (Chapter 12)
This short chapter passes on Kent Beck’s four rules of simple design. In priority order, a design is simple when it:
- Runs all the tests. A system that cannot be verified cannot be trusted.
- Contains no duplication.
- Expresses the intent of the programmer. Good names, small functions, and tests that read as examples.
- Minimises classes and methods. Do not split things so far that it becomes ceremony.
With tests in place, you can refactor freely to satisfy rules two to four. Removing duplication is often the first step:
// Before: the same idea twice
fun priceWithMemberDiscount(p: Double) = p - p * 0.10
fun priceWithStudentDiscount(p: Double) = p - p * 0.15
// After: the idea once, the difference as a parameter
fun priceAfterDiscount(price: Double, rate: Double) = price * (1 - rate)
14. Concurrency (Chapter 13)
Concurrency is hard because bugs depend on timing: they appear rarely and are hard to reproduce. The book lists defensive habits: keep concurrent code separate from other code, limit shared data, use copies of data where possible, keep threads as independent as you can, keep synchronised sections small, know your library’s tools, and test threaded code deliberately (run it many times, on different machines and loads).
Bad
var count = 0
repeat(1000) {
thread { count++ } // read, add, write: two threads can overwrite each other
}
// count is often less than 1000
Clean
val count = AtomicInteger()
coroutineScope {
repeat(1000) {
launch(Dispatchers.Default) { count.incrementAndGet() }
}
}
// count is always 1000
The book’s examples use Java threads; in Kotlin, coroutines and structured concurrency give you cleaner tools for the same ideas. The common patterns it names are still worth knowing: producer-consumer, readers-writers and dining philosophers. The most reliable defence is to share as little mutable state as possible.
15. Successive refinement (Chapter 14)
This is the first of the three case studies. It follows the author as they build a small command-line argument parser, a class that reads flags such as -l -p 8080 -d /logs. The first version works and looks fine while it handles two kinds of argument. Then more kinds arrive (numbers, then text, then lists), and the code grows if chains and special cases until it becomes hard to touch. The chapter is mostly about what to do at that moment: stop adding features, and clean up first, in many tiny steps, running the tests after each one.
The lesson I took from it is that clean code is rarely written clean the first time. It is like writing an essay: the first draft makes it work, and the next drafts make it right. The mistake is shipping the first draft and moving on.
Before: every new type adds another branch
fun parseArg(schema: Char, value: String?): Any = when (schema) {
'b' -> true
'i' -> value?.toIntOrNull() ?: throw IllegalArgumentException("Expected a number")
's' -> value ?: throw IllegalArgumentException("Expected text")
else -> throw IllegalArgumentException("Unknown type $schema")
}
// A new type means editing this function, and every other function that switches on the letter
After: one small type per kind of argument
interface ArgumentMarshaler { fun parse(value: String?): Any }
object BooleanMarshaler : ArgumentMarshaler { override fun parse(value: String?) = true }
object IntMarshaler : ArgumentMarshaler {
override fun parse(value: String?) =
value?.toIntOrNull() ?: throw IllegalArgumentException("Expected a number")
}
object StringMarshaler : ArgumentMarshaler {
override fun parse(value: String?) =
value ?: throw IllegalArgumentException("Expected text")
}
val marshalers = mapOf('b' to BooleanMarshaler, 'i' to IntMarshaler, 's' to StringMarshaler)
The change is not done in one big jump. The book takes it a step at a time: introduce the new type beside the old code, move one kind across, run the tests, then the next. Adding a list type later means adding one object, not editing everything.
16. JUnit internals (Chapter 15)
The second case study takes a small class from the JUnit testing framework, the one that builds the “expected X but was Y” message and trims it to the part that differs. It was written by respected programmers, it has tests, and it is good code. The author then shows that it can still be improved, by applying the rules of the earlier chapters one by one: encapsulate conditions, rename things, remove awkward prefixes, extract small methods, and make hidden ordering between steps explicit.
The lesson is the Boy Scout rule in action. You do not need broken code to justify a small improvement, and you do not need a rewrite. You need tests to protect each small change.
Before: a negative condition and a vague name
class Compactor(private val fExpected: String?, private val fActual: String?) {
fun compact(message: String): String {
if (shouldNotCompact()) return format(message, fExpected, fActual)
// ... build the shortened strings ...
return format(message, shortExpected(), shortActual())
}
private fun shouldNotCompact() = fExpected == null || fActual == null || fExpected == fActual
}
After: a positive condition that names the case
class Compactor(private val expected: String?, private val actual: String?) {
fun compact(message: String): String =
if (canBeCompacted()) format(message, shortExpected(), shortActual())
else format(message, expected, actual)
private fun canBeCompacted() =
expected != null && actual != null && expected != actual
}
Small changes: no f prefix on fields (the IDE already shows what is a field), one positive condition instead of a negated one, and the main path reads top to bottom.
17. Refactoring SerialDate (Chapter 16)
The third case study is a review of a date class from a widely used open-source library. The author does not begin by changing code. They begin with the tests: measure how much of the class they cover, add the missing ones, and only then start improving the design. After that come many findings, such as unclear names, unused code, comments that repeat the code, and numbers standing in for concepts, like 1 for January or 2 for Monday.
The lesson is the order of work: tests first, then refactor. A clean-up without a safety net is just a risky edit. The chapter also shows that even a heavily used library can hide design weaknesses, and that reviewing code is a skill worth practising.
Before: numbers that stand for months
fun daysInMonth(month: Int, isLeapYear: Boolean): Int = when (month) {
2 -> if (isLeapYear) 29 else 28
4, 6, 9, 11 -> 30
else -> 31 // what if month is 13?
}
After: a type that makes invalid values impossible
enum class Month(private val days: Int) {
JANUARY(31), FEBRUARY(28), MARCH(31), APRIL(30), MAY(31), JUNE(30),
JULY(31), AUGUST(31), SEPTEMBER(30), OCTOBER(31), NOVEMBER(30), DECEMBER(31);
fun length(isLeapYear: Boolean) =
if (this == FEBRUARY && isLeapYear) 29 else days
}
val days = Month.FEBRUARY.length(isLeapYear = true) // 29
The numeric version needs a comment and a default branch to be safe. The enum version cannot be called with month 13 at all. (In real Kotlin code you would normally use java.time.Month or YearMonth rather than writing your own.)
18. Smells and heuristics (Chapter 17)
The last chapter is a reference catalogue of more than sixty “smells”: signs that code may be wrong. It is grouped by area, and it is meant for looking things up during review rather than reading straight through. First, the six big symptoms of a rotting design:
| Smell | What it feels like |
|---|---|
| Rigidity | A small change forces changes in many other places. |
| Fragility | You change one thing and something unrelated breaks. |
| Immobility | You cannot reuse a piece without dragging half the system along. |
| Needless complexity | Machinery for a future that never came. |
| Needless repetition | The same logic copied in several places, so fixes must be repeated. |
| Opacity | Code that is hard to understand, even for its author after a month. |
Then the catalogue, with a few examples from each group:
| Group | Examples of smells |
|---|---|
| Comments | Outdated comments, redundant comments, commented-out code. |
| Environment | A build or a test run that needs more than one step. |
| Functions | Too many arguments, output arguments, flag arguments, dead functions. |
| General | Duplication, dead code, inconsistency, clutter, feature envy (a method more interested in another class’s data), hidden dependencies between steps, magic numbers, code at the wrong level of abstraction, artificial coupling. |
| Names | Non-descriptive names, names at the wrong level of abstraction, ambiguous names, names that hide side effects. |
| Tests | Insufficient tests, skipped trivial tests, ignored tests, untested boundary conditions, slow tests. |
The book also ships a Java-specific group (wildcard imports, constants versus enums, and so on), which I leave out here. The next section covers the heuristics from this chapter that I use most.
19. Conditionals and understandability (from Chapter 17)
Prefer polymorphism to long type checks
A when over a type code has to be edited every time a new type arrives, and it is repeated everywhere the type matters. Let each type carry its own behaviour. (This is the same idea as the Open/Closed principle in my SOLID post.)
// Bad
fun area(shape: Shape): Double = when (shape.type) {
"circle" -> Math.PI * shape.radius * shape.radius
"rectangle" -> shape.width * shape.height
else -> throw IllegalArgumentException("Unknown shape")
}
// Clean
sealed interface Shape { fun area(): Double }
class Circle(private val radius: Double) : Shape {
override fun area() = Math.PI * radius * radius
}
class Rectangle(private val width: Double, private val height: Double) : Shape {
override fun area() = width * height
}
Kotlin adds a nuance: a when over a sealed type is checked by the compiler, so a missing case is a compile error. For a closed set of shapes that is also a clean design. Use polymorphism when behaviour belongs to the type, and a sealed when when you are adding operations rather than types.
Explain with variables, and avoid negative conditions
// Bad
if (!(order.items.isEmpty() || !customer.isActive)) { ship(order) }
// Clean
val hasItems = order.items.isNotEmpty()
val canOrder = customer.isActive
if (hasItems && canOrder) { ship(order) }
Other tips from this part of the book: be consistent (if you call it fetch in one place, do not call it retrieve in another), wrap boundary conditions in a named value, and prefer a small value type like Money or EmailAddress over a bare Double or String. Kotlin’s @JvmInline value class makes that cheap.
20. Where I use judgement
The book is influential and also debated, and it is worth reading with a critical eye. These are the places where I apply it with care:
- Function size. Splitting everything into three-line functions can hide the flow. I split when a name improves understanding, not to hit a number.
- Comments. “No comments” is too strong. Public APIs, non-obvious decisions and workarounds deserve a sentence or two.
- Age. It was written in 2008 for Java. Kotlin features such as data classes, sealed types, extension functions, default arguments and immutability remove some of the problems the book works around.
- Rules are heuristics. Use them to start a conversation in review, not to end one.
- Do not rewrite for its own sake. Clean the code you are already changing, which is the Boy Scout rule again.
- Chapters I could only sketch. The three case studies (chapters 14 to 16) are best read in the book itself, where you can follow every step, and the Java-specific smells do not all carry over to Kotlin.
21. Takeaways, checklist and references
Before I open a pull request, I run through this list:
- Could a new teammate say what each name means without asking me?
- Does each function do one thing, with no flag argument?
- Can a comment be replaced by a better name?
- Did I name every magic number and avoid double negatives?
- Are internals hidden, with no long
a.b.c.dchains? - Does each test check one concept and read like a sentence?
- Is the file a little cleaner than it was when I opened it?
References
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Prentice Hall, 2008. Amazon · InformIT
- wojteklu, Clean Code summary (GitHub Gist)
- My post Designing for Change: The SOLID Principles, which covers several of the same ideas with runnable exercises.
| Term | In plain words |
|---|---|
| Clean code | Code that is easy to read, understand and change safely. |
| Boy Scout rule | Leave the code cleaner than you found it. |
| Magic number | A bare value in code whose meaning is not obvious. |
| Flag argument | A boolean parameter that switches a function between two behaviours. |
| Side effect | A hidden change a function makes beyond returning a value. |
| Law of Demeter | Talk to your direct neighbours, not to their neighbours. |
| Code smell | A surface sign that something deeper in the design may be wrong. |
| F.I.R.S.T. | Fast, Independent, Repeatable, Self-validating, Timely: qualities of good tests. |