Clean Code by Robert C. Martin is the most recommended programming book of all time. Search any list of “top books for software engineers,” and you are almost guaranteed to find this book on the list.
And yet, some people love to hate on Clean Code, even going so far as to say that it’s probably time to stop recommending Clean Code. I’d argue that sentiments like this are deeply misguided.
Yes, some of the advice in the book is questionable. Yes, some of the content feels dated or hasn’t aged well with time. Yes, some of the examples are confusing. All of this is true. But let’s not be so quick to discount all the good advice that the book has to offer!
Completely ignoring a book simply because of a few bad ideas is a perfect example of several cognitive distortions: mental filtering, magnification, and discounting the positive, to name a few.
In fact, Uncle Bob and the other contributing authors have taken care to preemptively handle this concern in the book’s first chapter:
Many of the recommendations in this book are controversial. You will probably not agree with all of them. You might violently disagree with some of them. That’s fine. We can’t claim final authority. On the other hand, the recommendations in this book are things that we have thought long and hard about. We have learned them through decades of experience and repeated trial and error. So whether you agree or disagree, it would be a shame if you did not see, and respect, our point of view.
So without further ado, let’s consider all the timeless advice that Clean Code has to offer! We’ll go through the book, chapter by chapter, summarizing many of the ideas Uncle Bob presents.
Chapter 1: Clean Code
The total cost of owning a mess compounds over time.
It’s very difficult to rebuild a legacy system from the ground up. Refactoring and incremental improvements are often the better path to take.
In messy codebases it can take days or weeks to accomplish tasks that should only take hours.
Take the time to go fast.
Clean code does one thing well. Bad code tries to do too much.
Clean code is well-tested.
When reading well-written code, every function does pretty much what you expected.
If you disagree with a principle that someone with decades of experience is teaching, you’d do well to at least consider their viewpoint before disregarding it.
Code is read far more often than it is written.
Code that is easier to read is easier to change.
Leave the codebase better than you found it (The Boy Scout Rule).
Chapter 2: Meaningful Names
Choose your variable names carefully.
Choosing good names is hard.
The name of a variable or function should tell you what it is and how it is used.
Avoid single character variable names, with the exception of commonly used names like
i
for the counter variable in a loop.Avoid using abbreviation in variable names.
Variable names should be pronounceable so that you can talk about them and say them out loud.
Use variable names that are easily searchable.
Classes and objects should have names that are nouns.
Methods and functions should have names that are verbs or verb-noun pairs.
Chapter 3: Functions
Functions should be small.
Functions should do one thing.
Functions should have descriptive names. (Repeated from Chapter 2)
Extract code in the body of if/else or switch statements into clearly named functions.
Limit the number of arguments a function accepts.
If a function needs a lot of configuration arguments, consider combining them into a single configuration options variable.
Functions should be pure, meaning that they don’t have side effects and don’t modify their input arguments.
A function should be a command or a query, but not both (Command Query Separation).
Throw errors and exceptions rather than returning error codes.
Extract duplicated code into clearly named functions (Don’t Repeat Yourself).
Unit tests make refactoring easier.
Chapter 4: Comments
Comments can lie. They can be wrong to begin with, or they can be originally accurate and then become outdated over time as the related code changes.
Use comments to describe why something is written the way it is, not to explain what is happening.
Comments can often be avoided by using clearly named variables and extracting sections of code into clearly named functions.
Prefix your TODO comments in a consistent manner to make searching for them easier. Revisit and clean up your TODO comments periodically.
Don’t use Javadocs just for the sake of using them. Comments that describe what a method does, what arguments it takes, and what it returns are often redundant at best and misleading at worst.
Comments should include all the relevant info and context someone reading the comment will need. Don’t be lazy or vague when you write a comment.
Journal comments and file author comments are unnecessary due to version control and git blame.
Don’t comment out dead code. Just delete it. If you think you’ll need the code in the future, that’s what version control is for.
Chapter 5: Formatting
As a team, choose a set of rules for formatting your code and then consistently apply those rules. It doesn’t matter so much what rules you agree on, but you do need to come to an agreement.
Use an automated code formatter and code linter. Don’t rely on humans to manually catch and correct each formatting error. This is inefficient, unproductive, and a waste of time during code reviews.
Add vertical whitespace in your code to visually separate related blocks of code. A single new line between groups is all you need.
Small files are easier to read, understand, and navigate than large files.
Variables should be declared close to where they’re used. For small functions, this is usually at the top of the function.
Even for short functions or if statements, still format them properly rather than writing them on a single line.
Chapter 6: Objects and Data Structures
Implementation details in an object should be hidden behind the object’s interface. By providing an interface for consumers of the object to use, you make it easier to refactor the implementation details later on without causing breaking changes. Abstractions make refactoring easier.
Any given piece of code should not know about the internals of an object that it’s working with.
When working with an object, you should be asking it to perform commands or queries, not asking it about its internals.
Chapter 7: Error Handling
Error handling shouldn’t obscure the rest of the code in the module.
Throw errors and exceptions rather than returning error codes. (Repeated from Chapter 3)
Write tests that force errors to make sure your code handles more than just the happy path.
Error messages should be informative, providing all the context someone getting the error message would need in order to effectively troubleshoot.
Wrapping third-party APIs in a thin layer of abstraction makes it easier to swap out one library for another in the future.
Wrapping third-party APIs in a thin layer of abstraction makes it easier to mock the library during testing.
Use the Special Case pattern or Null Object pattern to handle exceptional behavior like when certain data doesn’t exist.
Chapter 8: Boundaries
Third-party libraries help you ship your product faster by allowing you to outsource various concerns.
Write tests to ensure that your usage of any given third-party library is working properly.
Use the Adapter pattern to bridge the gap between a third-party library’s API and the API you wish it had.
Wrapping third-party APIs in a thin layer of abstraction makes it easier to swap out one library for another in the future. (Repeated from Chapter 7)
Wrapping third-party APIs in a thin layer of abstraction makes it easier to mock the library during testing. (Repeated from Chapter 7)
Avoid letting too much of your application know about the particulars of any given third-party library.
It is better to depend on something you control than to depend on something you don’t control.
Chapter 9: Unit Tests
Test code should be kept as clean as production code (with a few exceptions, usually involving memory or efficiency).
As production code changes, test code also changes.
Tests help keep your production code flexible and maintainable.
Tests enable change by allowing you to refactor with confidence without the fear of unknowingly breaking things.
Structure your tests using the Arrange-Act-Assert pattern (also known as Build-Operate-Check, Setup-Exercise-Verify, or Given-When-Then).
Use domain-specific functions to make tests easier to write and easier to read.
Evaluate a single concept per test.
Tests should be fast.
Tests should be independent.
Tests should be repeatable.
Tests should be self-validating.
Tests should be written in a timely manner, either shortly before or after the production code is written, not months later.
If you let your tests rot, your code will rot too.
Chapter 10: Classes
Classes should be small.
Classes should be responsible for only one thing and should have only one reason to change (Single Responsibility Principle).
If you can’t think of a clear name for a class, it’s probably too big.
Your job is not done once you get a piece of code to work. Your next step is to refactor and clean up the code.
Using many small classes instead of a few large classes in your app reduces the amount of information a developer needs to understand while working on any given task.
Having a good test suite in place allows you to refactor with confidence as you break large classes into smaller classes.
Classes should be open for extension but closed for modification (Open-Closed Principle).
Interfaces and abstract classes provide seams that make testing easier.
Chapter 11: Systems
Use dependency injection to give developers the flexibility to pass any object with a matching interface to another class.
Use dependency injection to create object seams in your app to make testing easier.
Software systems are not like a building that must be designed up front. They are more like cities that grow and expand over time, adapting to current needs.
Delay decision making until the last responsible moment.
Use domain-specific language so that domain experts and developers are using the same terminology.
Don’t over-complicate your system. Use the simplest thing that works.
Chapter 12: Emergence
Systems that aren’t testable aren’t verifiable, and systems that aren’t verifiable should never be deployed.
Writing tests leads to better designs because code that is easy to test often uses dependency injection, interfaces, and abstraction.
A good test suite eliminates your fear of breaking the app during refactoring.
Duplication in your code creates more risk, as there are more places in the code to change and more places in the code for bugs to hide.
It’s easy to understand the code you’re currently writing because you’ve been deeply involved in understanding it. It’s not so easy for others to quickly gain that same level of understanding.
The majority of the cost of a software project is in long-term maintenance.
Tests act as living documentation of how your app should (and does) behave.
Don’t move on as soon as you get your code working. Take time to make it cleaner and easier to understand.
The next person to read your code in the near future will most likely be you. Be kind to your future self by writing code that is easy to understand.
Resist dogma. Embrace pragmatism.
It takes decades to get really good at software engineering. You can speed up the learning process by learning from experts around you and by learning commonly used design patterns.
Chapter 13: Concurrency
Writing concurrent code is hard.
Random bugs and hard-to-reproduce issues are often concurrency issues.
Testing does not guarantee that there are no bugs in your application, but it does minimize risk.
Learn about common concurrency issues and their possible solutions.
Chapter 14: Successive Refinement
Clean code usually doesn’t start out clean. You write a dirty solution first and then refactor it to make it cleaner.
It’s a mistake to stop working on the code once you have it “working.” Take some time to make it even better after you have it working.
Messes build gradually.
If you find yourself in a mess where adding features is too difficult or takes too long, stop writing features and start refactoring.
Making incremental changes is often a better choice than rebuilding from scratch.
Use test-driven development (TDD) to make a large number of very small changes.
Good software design involves a separation of concerns in your code and splitting code into smaller modules, classes, and files.
It’s easier to clean up a mess right after you make it than it is to clean it up later.
Chapter 15: JUnit Internals
Negative variable names or conditionals are slightly harder to understand than positive ones.
Refactoring is an iterative process full of trial and error.
Leave the code a little better than you found it (The Boy Scout Rule). (Repeated from Chapter 1)
Chapter 16: Refactoring SerialDate
Code reviews and critiques of our code are how we get better, and we should welcome them.
First make it work, then make it right.
Not every line of code is worth testing.
Chapter 17: Smells and Heuristics
- Clean code is not a set of rules but rather a system of values that drive the quality of your work.
[In this chapter, Uncle Bob lists 66 more of his code smells and heuristics, many of which have been covered throughout the rest of the book. Reproducing them here would essentially be copying and pasting the title of each item, so I’ve refrained from doing so. Instead, I’d encourage you to read the book!]
Conclusion
Let’s finish where we began: Clean Code by Robert C. Martin is the most recommended programming book of all time.
There’s a good reason why.
Top comments (34)
Thanks for the thoughtful comment!
I've seen this go both ways, good and bad. An interesting result of not using a wrapper API around a third-party API is that if you don't do it, you're likely to stay stuck on that library forever (which might be ok in some cases!).
I've seen this happen in a few companies now with things like date libraries (Moment.js, Day.js, date-fns, homegrown library, etc.), currency-formatting libraries, and XHR libraries (jQuery ajax, axios, etc.). The third-party code gets so interwoven with all the other source code that it becomes impossible to migrate away from one library and onto another. Or, not for the faint of heart, months of effort to migrate.
I think the lessons I've learned from those experiences line up pretty well with the advice in Clean Code, which would be to either 1) use a thin API wrapper over the third-party API, or 2) try to limit how much of your codebase knows about the third-party library, since that will make swapping technologies easier. And those two ideas are interesting because the first idea is really just a solution to the second idea! So however you approach it, the second idea seems to be the core principle here.
It's definitely exciting to see the native APIs get better where all three of the examples I gave will soon become a non-issue!
Libraries with types tend to get very interwoven - function libraries such as date-fns generally do not. There is absolutely nothing won by wrapping somebody else's function in your own function - it's the exact same situation, now you're just coupled to that instead. Switching from a function you wrote, or from a function you imported, is the exact same amount of work.
Even with some classes, wrapping them might not mitigate the problem and can actually make matters worse - it really depends... See this:
youtu.be/tqqH_Ib_gDc
I watched his video, and it's interesting, but I don't think his argument is quite right. He basically says throughout the video not to use abstraction because you're going to do it wrong. So he's really arguing against doing abstraction badly.
Some of his key points were:
Both of those ideas aren't criticisms of abstraction though, those are criticisms of doing abstraction badly.
To solve the first problem, just... don't add the third-party library's implementation details into your abstractions's interface. Leaking the implementation details defeats the whole purpose of abstraction.
For the second problem, it may be true in some cases that a breaking change in the third-party library's API will cause a breaking change in your abstraction layer, but it's not necessarily true. It may be very possible to write a good abstraction that can handle the third-party library's breaking change while not actually changing the interface that your abstraction creates. That's the beauty of abstraction: the rest of your app doesn't know about the changed implementation details.
His last point is great though about abstraction being hard to get right and that getting the interface right the first time is important.
See also in your article
It's been my observation that people will usually find ways to avoid doing hard things.
lots of JavaScript code is full of anonymous inline functions (of any size). Among other things it avoids having to name the function, effectively forcing any future reader of the code having to fully mentally parse the function body to divine the function's intended purpose without even a hint from a function name.
similarly if people are convinced that it is "hard to get the right abstraction on the first try" they often aren't going to invest any effort in an attempt.
Perhaps focusing on abstraction is a misdirection.
Chapter 7 Error Handling was written by Michael Feathers, Author of Working Effectively with Legacy Code
All he was suggesting was to "map" the errors/exceptions that are specific to the API to application specific exceptions that the application knows how to handle.
That being said I put forward the perspective that (the right) boundaries are more important than "abstractions". And ultimately even the "right" boundaries are subject to which objectives are perceived to have the highest value.
Data-Oriented Design: What's wrong? — Mapping the problem
Robert C. Martin is clearly in the "good at providing a human oriented representation of the problem in source code" camp and TDD is a key part of his platform.
People often pursue "testing to attempt to show correctness.”
But Michael Feathers makes the observation:
That thought and reflection also iteratively revises the boundaries towards optimal testability.
This seems to be in direct contradiction to DHH's Test-induced design damage. As a framework designer he's a proponent of "Rails is your application". He's primarily interested in speed of initial implementation and is perfectly content to rely on integrated tests to just verify product correctness — but not challenge the quality of design (the design of Rails doesn't change and comes with pre-imposed boundaries).
Robert C. Martin's view is informed by the Hexagonal architecture were:
So going back to the out-of-context over-generalization:
If the semantics between the contract and the third-party API are sufficiently different expect to implement an anti-corruption layer.
When it comes to external APIs it can be worth looking into contract tests.
Is it easy to identify optimal boundaries?
Of course not.
Andy Kinslow, Software Engineering 1968, p.21
Eric Evans, Domain Driven Design 2004, p.196
Startups don’t have stable business rules - so boundaries are constantly in flux
Adam Ralph, Finding your service boundaries 2019
i.e. it's rare to get it right off the bat.
I don't agree. The criticism is that of abstracting the wrong things. The third-party library you're using is already an abstraction of something - and there could be reasons to further abstract that, but often there isn't, and just avoiding coupling, in my opinion is definitely not the right motivation.
Even if you don't leak implementation details, you're going to "leak concepts" - ideas from the underlying library are likely going to bleed into your domain, even if things like types and interfaces do not.
Some reasons I might choose to abstract would be:
The library is really complex and does a lot more than I need - in that case, I can avoid direct coupling to a complex library by hiding it behind a simple interface.
The library doesn't quite do everything I need - in that case, I can build an abstraction that adds in the missing details, and again avoid direct coupling to something that wasn't quite what I needed in the first place.
On the other hand, why would I abstract something if it's already (more or less) exactly what the project needs? If I put my own very similar units in front of some library units, any ideas of being decoupled is really just an idea - if anything changes, it's practically guaranteed to break my abstraction.
I think that's the case he's talking about in the video.
Every line of code, whether that's your code or library code, adds complexity: every line of code is a liability, so every line of code needs to have a specific, measured reason to exist.
In my experience, the most successful projects are always the ones with less complexity.
So it has to be a conscious, case-by-case decision, in my opinion.
This is assuming a one-to-one distinct abstraction to concrete library relationship. That type of alignment isn't necessarily the best way to move forward.
I think it's more important to evaluate if complexity is managed appropriately. In my view OOD invariably adds complexity in order to manage complexity — it can work but it often isn't a slam dunk.
That's pretty much a given. Guidelines tend to be a starting point, not some absolute truth.
What ultimately devalued the video for me was the example - why would there be a need for a concrete messaging abstraction? The actual goal is to have the application logic be "ignorant" of the messaging solution that is being used to handle messages. This idea is similar to Persistence Ignorance (PI):
"The Application" will only need to send a finite number of message types, and receive a finite number of message types. Worst case each send-type has its own function into the infrastructure and the application exposes a separate function for each receive-type. The application is only interested in providing the data for outgoing messages and extracting the data from the incoming messages. The application really doesn't care what happens on the other side of the application boundary.
Quote
Rabbit MQ has no business being inside the application boundary.
To use J. B. Rainsberger's terminology:
FYI: Mock Roles, not Objects
Quote
The "swap out" argument is specious but isolation is the prize.
Isolation is the pre-requisite to being able to leverage fast micro-tests — "tests to detect change", i.e. establish rapid feedback as we refactor the code to reduce the volatility in the marginal cost of adding the next feature.
Doesn't this HOW/DMZ/HZ stuff slow us down?
In the beginning perhaps but again J.B. Rainsberger explains The Scam:
He acknowledges that "the scam" is initially incredibly seductive but eventually there comes the point where the cost of continuing is higher than the cost of starting again.
So the initial investment is aimed at going well for long enough, so you'll beat fast all the time.
Perhaps that article's author simply needed a more appropriate title:
Kevlin Henny stated it this way in reference to the Dreyfus model of skill acquisition:
So it needs to be stated that simply itemizing "Clean Code Guidelines" — without any context - can really only serve novice and advanced beginner developers. To truly advance, one needs to understand the prevailing assumptions and circumstances that tend to arise that give credibility to these guidelines so that it becomes possible to disregard the guidelines outside of their implied, applicable context.
To be clear, a competent developer doesn't just simply disregard the guidelines but needs to be capable of formulating a valid argument of why the guidelines do not apply in a particular context.
At the time I found Designing Object-Oriented C++ Applications Using the Booch Method (1995) and Agile Software Development, Principles, Patterns, and Practices (2002) quite useful, as well as his blog (as long as you keep your critical thinking firmly engaged — don't take anything at face value).
That said, I find to this day that he has failed to reconcile his growing admiration of Clojure (at least since 2010) with his continued evangelism for the class-based object-oriented development methodology. I'm inclined to believe this is entirely motivated by "professional reasons".
absolutely love this comment!
as annoying and incoherent it can seem for a novice seeing experts that do not strictly follow the rules all the times, in the end it is really just that. Context and experience enable you to understand if the rule makes sense in that specific context or not.
rules are easy and necessary, and fundamental at the beginning, the more you grow and the more you understand, the more flexible you can become.
it kinda reminded me of the quote from Pablo Picasso:
Love it! The Dreyfus model of skill acquisition is one of my favorites. The whole original research paper is well worth the read: apps.dtic.mil/sti/pdfs/ADA084551.pdf
Keeping in mind:
Perhaps you'll enjoy Dan North's Dreyfus Squared pattern/hypothesis.
Very good read!
Thanks for the summary! Now, to balance things a bit, here's what I don't like about the book:
It often put the responsibility of bad code on the coder. In my experience, it's more the environment which push developers to do "bad code". The environment include: not enough knowledge about the domain (gaining knowledge about what we're building often comes with a first draft of "bad code"), pressure from the stakeholders, company culture...
Weird idea which are not backed by... anything empirical. Example in chapter 1: a developer needs to have a "code-sense", some sort of artist's sixth sense, to know what clean code is. I'm sorry: what?
The code shouldn't have one responsibility, but enough responsibility to be coherent. It's very similar, arguably, but I've seen too many class with one functions 'cause it needs to have one responsibility. I think that's the result of Martin's absolutism: it's the truth, everybody needs to follow it, who care about the context?
To quote him: "FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. THEY SHOULD DO IT ONLY."
I'd say: it's not always true.
DRY is not about code duplication, it's about duplication of knowledge. Still, Martin puts DRY and code duplication together. Reading The Pragmatic Programmer (the book which coined DRY) will teach you much more about that than Clean Code.
The examples: it follows Java never ending verbosity, and some of them are seriously questionable. I mean what's a "SetupTeardownIncluder"?
I don't think it's a waste of time to read it, but I think it might be confusing for beginners, because of this weird mix of good advice and questionable ones.
I would recommend The Pragmatic Programmer or Refactoring (from Fowler) more than Clean code.
Yet another reference:
Aside: Development by Slogan
Ironically:
Perhaps at times he gets more caught up in his slogans than the essential messages.
Kevlin Henney regularly likes to haul this one out for some of his talks.
I tend to agree because they have already proven to be more timeless (and less context sensitive) because both of them have successfully moved to a 2nd edition after an already long period of relevance:
The Pragmatic Programmer (1999, 2019)
Refactoring (2000, 2018).
For anyone not familiar with Dave Thomas:
Fowler about Workflows of Refactoring. The 2nd edition uses JavaScript:
… but as such it still uses a predominantly class-oriented style (which in my personal opinion isn't the sweet spot for JS — but this is about mainstream development in general, not JS specifically).
The Pragmatic Programmer is a great book! That's also one I'd recommend to everyone.
Refactoring is good as well, although it's a pretty dry read and can be hard to slog through sometimes. The advice is all great, but reading through all the refactoring examples isn't exactly exciting.
A lot of debate around the book seems to be in its context
gofmt
and save a lot of time and energy on pointless debates related to this!In short, the book is written as advice for OO Web applications where it makes many good points. Taking the advice out of its context results in cargo cults and code which is not fit for its domain
This reminded a lot of the Ada style guide (yes, it is a thing!)
About functions
Yes, in theory, but there are many cases where a function with side effects makes the code smoother and clearer. The typical example is a lexical analyzer function that returns the current token and move to the next one. Yes, you can replace it with a procedure, but having a function is much more convenient, so
Absolutely. Being pragmatic about these guidelines is the key. Not every rule applies in every situation, and wisdom is knowing how to apply these ideas appropriately.
Thanks for the summary, very informative and useful.
You’re welcome, thanks for reading!
Yeah, that one should be qualified as a rule of thumb.
This video is worth watching:
youtu.be/tqqH_Ib_gDc
Talks about the pros and cons, when to abstract and when not to.
Same response here: dev.to/thawkin3/comment/1k6hm
But yes, as with most everything, the answer is often "it depends." Abstraction is a valuable tool. The hard part is learning how to do it correctly and knowing when it's an appropriate solution and when it's not.
Thank you!
Thanks for reading!
Uncle BOB. Is it.