In Defense of Clean Code
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
ifor 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!]
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.
Credit: Source link