DEV Community

Cover image for In Defense of Clean Code: 100+ pieces of timeless advice from Uncle Bob
Tyler Hawkins
Tyler Hawkins

Posted on

In Defense of Clean Code: 100+ pieces of timeless advice from Uncle Bob

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

  1. The total cost of owning a mess compounds over time.

  2. It’s very difficult to rebuild a legacy system from the ground up. Refactoring and incremental improvements are often the better path to take.

  3. In messy codebases it can take days or weeks to accomplish tasks that should only take hours.

  4. Take the time to go fast.

  5. Clean code does one thing well. Bad code tries to do too much.

  6. Clean code is well-tested.

  7. When reading well-written code, every function does pretty much what you expected.

  8. 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.

  9. Code is read far more often than it is written.

  10. Code that is easier to read is easier to change.

  11. Leave the codebase better than you found it (The Boy Scout Rule).


Chapter 2: Meaningful Names

  1. Choose your variable names carefully.

  2. Choosing good names is hard.

  3. The name of a variable or function should tell you what it is and how it is used.

  4. Avoid single character variable names, with the exception of commonly used names like i for the counter variable in a loop.

  5. Avoid using abbreviation in variable names.

  6. Variable names should be pronounceable so that you can talk about them and say them out loud.

  7. Use variable names that are easily searchable.

  8. Classes and objects should have names that are nouns.

  9. Methods and functions should have names that are verbs or verb-noun pairs.


Chapter 3: Functions

  1. Functions should be small.

  2. Functions should do one thing.

  3. Functions should have descriptive names. (Repeated from Chapter 2)

  4. Extract code in the body of if/else or switch statements into clearly named functions.

  5. Limit the number of arguments a function accepts.

  6. If a function needs a lot of configuration arguments, consider combining them into a single configuration options variable.

  7. Functions should be pure, meaning that they don’t have side effects and don’t modify their input arguments.

  8. A function should be a command or a query, but not both (Command Query Separation).

  9. Throw errors and exceptions rather than returning error codes.

  10. Extract duplicated code into clearly named functions (Don’t Repeat Yourself).

  11. Unit tests make refactoring easier.


Chapter 4: Comments

  1. 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.

  2. Use comments to describe why something is written the way it is, not to explain what is happening.

  3. Comments can often be avoided by using clearly named variables and extracting sections of code into clearly named functions.

  4. Prefix your TODO comments in a consistent manner to make searching for them easier. Revisit and clean up your TODO comments periodically.

  5. 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.

  6. 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.

  7. Journal comments and file author comments are unnecessary due to version control and git blame.

  8. 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

  1. 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.

  2. 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.

  3. Add vertical whitespace in your code to visually separate related blocks of code. A single new line between groups is all you need.

  4. Small files are easier to read, understand, and navigate than large files.

  5. Variables should be declared close to where they’re used. For small functions, this is usually at the top of the function.

  6. 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

  1. 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.

  2. Any given piece of code should not know about the internals of an object that it’s working with.

  3. 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

  1. Error handling shouldn’t obscure the rest of the code in the module.

  2. Throw errors and exceptions rather than returning error codes. (Repeated from Chapter 3)

  3. Write tests that force errors to make sure your code handles more than just the happy path.

  4. Error messages should be informative, providing all the context someone getting the error message would need in order to effectively troubleshoot.

  5. Wrapping third-party APIs in a thin layer of abstraction makes it easier to swap out one library for another in the future.

  6. Wrapping third-party APIs in a thin layer of abstraction makes it easier to mock the library during testing.

  7. Use the Special Case pattern or Null Object pattern to handle exceptional behavior like when certain data doesn’t exist.


Chapter 8: Boundaries

  1. Third-party libraries help you ship your product faster by allowing you to outsource various concerns.

  2. Write tests to ensure that your usage of any given third-party library is working properly.

  3. Use the Adapter pattern to bridge the gap between a third-party library’s API and the API you wish it had.

  4. 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)

  5. Wrapping third-party APIs in a thin layer of abstraction makes it easier to mock the library during testing. (Repeated from Chapter 7)

  6. Avoid letting too much of your application know about the particulars of any given third-party library.

  7. It is better to depend on something you control than to depend on something you don’t control.


Chapter 9: Unit Tests

  1. Test code should be kept as clean as production code (with a few exceptions, usually involving memory or efficiency).

  2. As production code changes, test code also changes.

  3. Tests help keep your production code flexible and maintainable.

  4. Tests enable change by allowing you to refactor with confidence without the fear of unknowingly breaking things.

  5. Structure your tests using the Arrange-Act-Assert pattern (also known as Build-Operate-Check, Setup-Exercise-Verify, or Given-When-Then).

  6. Use domain-specific functions to make tests easier to write and easier to read.

  7. Evaluate a single concept per test.

  8. Tests should be fast.

  9. Tests should be independent.

  10. Tests should be repeatable.

  11. Tests should be self-validating.

  12. Tests should be written in a timely manner, either shortly before or after the production code is written, not months later.

  13. If you let your tests rot, your code will rot too.


Chapter 10: Classes

  1. Classes should be small.

  2. Classes should be responsible for only one thing and should have only one reason to change (Single Responsibility Principle).

  3. If you can’t think of a clear name for a class, it’s probably too big.

  4. 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.

  5. 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.

  6. Having a good test suite in place allows you to refactor with confidence as you break large classes into smaller classes.

  7. Classes should be open for extension but closed for modification (Open-Closed Principle).

  8. Interfaces and abstract classes provide seams that make testing easier.


Chapter 11: Systems

  1. Use dependency injection to give developers the flexibility to pass any object with a matching interface to another class.

  2. Use dependency injection to create object seams in your app to make testing easier.

  3. 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.

  4. Delay decision making until the last responsible moment.

  5. Use domain-specific language so that domain experts and developers are using the same terminology.

  6. Don’t over-complicate your system. Use the simplest thing that works.


Chapter 12: Emergence

  1. Systems that aren’t testable aren’t verifiable, and systems that aren’t verifiable should never be deployed.

  2. Writing tests leads to better designs because code that is easy to test often uses dependency injection, interfaces, and abstraction.

  3. A good test suite eliminates your fear of breaking the app during refactoring.

  4. 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.

  5. 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.

  6. The majority of the cost of a software project is in long-term maintenance.

  7. Tests act as living documentation of how your app should (and does) behave.

  8. Don’t move on as soon as you get your code working. Take time to make it cleaner and easier to understand.

  9. 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.

  10. Resist dogma. Embrace pragmatism.

  11. 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

  1. Writing concurrent code is hard.

  2. Random bugs and hard-to-reproduce issues are often concurrency issues.

  3. Testing does not guarantee that there are no bugs in your application, but it does minimize risk.

  4. Learn about common concurrency issues and their possible solutions.


Chapter 14: Successive Refinement

  1. Clean code usually doesn’t start out clean. You write a dirty solution first and then refactor it to make it cleaner.

  2. 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.

  3. Messes build gradually.

  4. If you find yourself in a mess where adding features is too difficult or takes too long, stop writing features and start refactoring.

  5. Making incremental changes is often a better choice than rebuilding from scratch.

  6. Use test-driven development (TDD) to make a large number of very small changes.

  7. Good software design involves a separation of concerns in your code and splitting code into smaller modules, classes, and files.

  8. 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

  1. Negative variable names or conditionals are slightly harder to understand than positive ones.

  2. Refactoring is an iterative process full of trial and error.

  3. Leave the code a little better than you found it (The Boy Scout Rule). (Repeated from Chapter 1)


Chapter 16: Refactoring SerialDate

  1. Code reviews and critiques of our code are how we get better, and we should welcome them.

  2. First make it work, then make it right.

  3. Not every line of code is worth testing.


Chapter 17: Smells and Heuristics

  1. 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.

Discussion (37)

Collapse
jfbrennan profile image
Jordan Brennan • Edited on

Thanks for this summary!

Wrapping third-party APIs in a thin layer of abstraction

I used to do that - the Adapter pattern - but have changed my opinion and even thinking about doing it today makes me ill. That layer still ends up being a compromise in my experience. It's just a sad bag of code I don't want to maintain. In the JS world, the native APIs are mostly good enough now to use directly and the 3rd-party libraries I do use I picked primarily because their good design and focus allow for easy integration and easy removal.

Collapse
thawkin3 profile image
Tyler Hawkins Author

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!

Collapse
mindplay profile image
Rasmus Schultz

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

Thread Thread
thawkin3 profile image
Tyler Hawkins Author

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:

  1. If you write an abstraction layer, you're going to start leaking very specific functionality from the specific third-party library into the rest of your codebase.
  2. If there is a breaking change in the third-party library's API, then there will be a breaking change in your abstraction layer, where they have to be updated in lockstep.

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.

Thread Thread
jfbrennan profile image
Jordan Brennan

This is why I stopped.

In order to get it "right" and have a robust and lasting solution, this layer ends up taking a lot of time and effort, which I never got back in the short or long term. Also, no one has said a word yet about the necessary tests or documentation that come with this approach. And then there's also the constant frustration of the middle layer not being able to do something you need (or isn't as ergonomic), but the 3rd-party library can and devs are stuck thinking, "If I could just use the library directly I could get this done right now!"

I've found being really picky about a 3rd-party solution's focus and design and adherence to relevant standards and then using it directly has the better ROI.

Thread Thread
peerreynders profile image
peerreynders

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

Choosing good names is hard.

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.

So he's really arguing against doing abstraction badly.

Perhaps focusing on abstraction is a misdirection.

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.

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

Object-oriented development is good at providing a human oriented representation of the problem in the source code, but bad at providing a machine representation of the solution. It is bad at providing a framework for creating an optimal solution, …

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:

Unit testing does not improve quality just by catching errors at the unit level. And, integration testing does not improve quality just by catching errors at the integration level. The truth is more subtle than that. Quality is a function of thought and reflection - precise thought and reflection.

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:

  • the application exists at the core
  • the application dictates the interfaces the "adapters" have to implement (not to be confused with the Adapter Pattern).
  • the "adapters" can be replaced with test doubles to accurately and quickly test "the application".

So going back to the out-of-context over-generalization:

Wrapping third-party APIs in a thin layer of abstraction

  1. Define the client facing contract/protocol "the application" actually needs first.
  2. Select a third-party API capable of fulfilling those needs.
  3. Implement the contract(s) on top of the chosen third-party API

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.

  • contract tests document the understanding of the contract/protocol as used by "the application".
  • contract tests are run infrequently against the "adapters" and the actual API to verify that all assumptions about the API still hold.
  • contract tests are used to guide and verify the expected behaviour of the test doubles of the various "adapters" used for testing the application core.

Is it easy to identify optimal boundaries?

Of course not.

The design process is an iterative one

Andy Kinslow, Software Engineering 1968, p.21

Suddenly one week it dawned on us what was wrong. Our model tied together the Facility and Loan shares in a way that was not appropriate to the business. This revelation had wide repercussions. With the business experts nodding, enthusiastically helping—and, I dare say, wondering what took us so long—we hashed out a new model on a whiteboard.

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.

Thread Thread
mindplay profile image
Rasmus Schultz

Both of those ideas aren't criticisms of abstraction though, those are criticisms of doing abstraction badly.

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.

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.

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:

  1. 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.

  2. 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.

Thread Thread
peerreynders profile image
peerreynders

If I put my own very similar units in front of some library units, any ideas of being decoupled is really just an idea.

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.

In my experience, the most successful projects are always the ones with less complexity.

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.

So it has to be a conscious, case-by-case decision, in my opinion.

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):

Well, PI means clean, ordinary classes where you focus on the business problem at hand without adding stuff for infrastructure-related reasons.
Jimmy Nilsson, Applying Domain-Driven Design and Patterns 2006, p. 183

"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

… and then you find out that there are messaging libraries that abstract the underlying transport whether it be Azure Service Bus or Rabbit MQ, etc. …

Rabbit MQ has no business being inside the application boundary.

To use J. B. Rainsberger's terminology:

  • Rabbit MQ has to live in the Horrible Outside World (HOW)
  • The application exists in the Happy Zone (HZ).
  • The HOW and HZ are separated by the DeMilitarized Zone (DMZ; where the "adapters" live; narrowing API, pattern of usage API)
  • The HZ can depend on the HZ
  • The DMZ can depend on the DMZ
  • The DMZ can depend on the HZ
  • The HOW is permitted to depend on the HZ
  • The DMZ may depend on the HOW
  • The HZ cannot depend on the DMZ
  • The DMZ is allowed to talk to the HZ by implementing an HZ interface.
  • The HZ cannot depend on the HOW (… and yet this is exactly what many frameworks encourage you to do — I call it the creepy hug problem — it's nice to be hugged unless the hug is a little bit too long and a little bit too tight by somebody you don't know that well)
  • The HOW is allowed to talk to the HZ by implementing an HZ interface.

FYI: Mock Roles, not Objects

Quote

Now again the reason you're abstracting these things is because you want to isolate them. And you might want to isolate them because you don't want to depend on them directly. Again most people say so I can swap it out.

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:

The cost of the first few features is actually quite a bit higher than it is doing it the "not so careful way" … eventually you reach the point where the cost of getting out of the blocks quickly and not worrying about the design is about the same as the cost of being careful from the beginning … and after this being careful is nothing but profit.

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.

Collapse
jfbrennan profile image
Jordan Brennan

Temporal can’t come soon enough!

Collapse
mindplay profile image
Rasmus Schultz

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.

Collapse
thawkin3 profile image
Tyler Hawkins Author

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.

Collapse
peerreynders profile image
peerreynders • Edited on

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.

Perhaps that article's author simply needed a more appropriate title:

It's probably time for me to stop leaning on Clean Code

Kevlin Henny stated it this way in reference to the Dreyfus model of skill acquisition:

… the point here is not simply that somebody who has skill in something learns by doing more of the same, they actually learn in a fundamentally different way.

Now one of the most interesting things that we get out of this is the fact that guidance or advice for the novice and the advanced beginner tends to be phrased in absolutes; it tends to be context-free — oh, you should always do this; oh, never do that — but as you ascend through this [levels of skill], what you find is that your experience tells you, there are these interesting little edge cases — these edge cases that don't seem to follow the rule, they are exceptions to the rule.

As you reach the expert end of the scale what you realize is that these are not exceptions to the rule, they are their own rules.

They are rules with well understood context which is one of the reasons that people with a lot of experience tend to come back at you with — you know you ask some question and they come back at you it depends.

They're not just being awkward — they might be that as well but they're not just being awkward — what they're doing: given, based on what you've told me, there's three possible solutions; there's three possible ways of doing this — I need more information because it depends.

There are few absolutes — in other words it's highly contextual.

So where do the SOLID principles live? I do use the phraseology sometimes as "the SOLID principles" — with only a couple of exceptions they live at this end [Novice, Advanced Beginner].

If you're still following the SOLID principles and you regard yourself as a competent developer you need to start giving them up

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".

Collapse
dvddpl profile image
Davide de Paolis

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:

Learn the rules like a pro, so you can break them like an artist.

Collapse
thawkin3 profile image
Tyler Hawkins Author

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

Collapse
peerreynders profile image
peerreynders

Keeping in mind:

All models are wrong; some models are useful

Perhaps you'll enjoy Dan North's Dreyfus Squared pattern/hypothesis.

Collapse
energeticpixels profile image
Anthony Jackman

Very good read!

Collapse
phantas0s profile image
Matthieu Cneude • Edited on

Thanks for the summary! Now, to balance things a bit, here's what I don't like about the book:

  1. 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...

  2. 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?

  3. 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.

  4. 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.

  5. 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.

Collapse
peerreynders profile image
peerreynders

DRY is not about code duplication, it's about duplication of knowledge.

Yet another reference:

every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Aside: Development by Slogan

The code shouldn't have one responsibility, but enough responsibility to be coherent.

Ironically:

Gather together those things that change for the same reason, and separate those things that change for different reasons.

Perhaps at times he gets more caught up in his slogans than the essential messages.

I mean what's a "SetupTeardownIncluder"?

Kevlin Henney regularly likes to haul this one out for some of his talks.

I would recommend The Pragmatic Programmer or Refactoring (from Fowler) more than Clean code.

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:

For anyone not familiar with Dave Thomas:

Fowler about Workflows of Refactoring. The 2nd edition uses JavaScript:

Choosing JavaScript was deeply ironic for me, as many readers may know, I'm not a fan of it. It has too many awkward edge cases and clunky idioms. ECMAScript 2015 (ES6) introduced a rather good class model, which makes many object-oriented refactorings much easier to express, but still has annoying holes that are built into the fabric of the language from its earliest days. But the compelling reason for choosing it over Java is that isn't wholly centered on classes. There are top-level functions, and use of first-class functions is common. This makes it much easier to show refactoring out of the context of classes.

… 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).

Collapse
thawkin3 profile image
Tyler Hawkins Author

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.

Collapse
azula profile image
Azula

A lot of debate around the book seems to be in its context

  • Many modules and small files : Works for OO languages. There are many C projects which are just a single header file due to ease of integration into other projects and being easy to inspect/modify
  • Making incremental changes is often a better choice than rebuilding from scratch - Again, works for OO languages. Array oriented languages like APL/J/K turn this advice on its head due their terseness and expressivity
  • First make it work, then make it right - This is not valid for projects which deal with security and formal verification e.g cryptographic libraries, theorem provers, mathematical libraries
  • A good test suite eliminates your fear of breaking the app during refactoring - Valid for dynamically typed languages. Strongly typed functional languages (e.g Haskell, Idris) have a type system expressive enough for fearless refactoring as long as it compiles
  • 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 - i have seen this taken to an extreme where there are dozens of classes for doing simple stuff. I have also seen the other extreme "Helper" classes which have >100k LOC
  • Use dependency injection to give developers the flexibility to pass any object with a matching interface to another class - This is more due to the clumsiness of early Java and C++. Most modern languages have remedied this since
  • Formatting - Use an agreed upon linter or better still, the language default e.g gofmt and save a lot of time and energy on pointless debates related to this!
  • Third-party libraries help you ship your product faster by allowing you to outsource various concerns. - In recent times, we are seeing the cost of this in terms of security issues in dependencies. Javascript ecosystem has seen multiple issues of either malicious codeshipping or build breakages due to package issues

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

Collapse
pinotattari profile image
Riccardo Bernardini

This reminded a lot of the Ada style guide (yes, it is a thing!)

About functions

  1. Functions should be pure, meaning that they don’t have side effects and don’t modify their input arguments.

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

Embrace pragmatism, not dogmas.

Collapse
thawkin3 profile image
Tyler Hawkins Author

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.

Collapse
mkambankhani profile image
Emmanuel Mkambankhani

Thanks for the summary, very informative and useful.

Collapse
thawkin3 profile image
Tyler Hawkins Author

You’re welcome, thanks for reading!

Collapse
dvddpl profile image
Davide de Paolis

Amazing summary. Thank you for taking the time to write it. Very useful.

Collapse
thawkin3 profile image
Tyler Hawkins Author

Thank you Davide!

Collapse
rsram27 profile image
Ronaldo

Uncle BOB. Is it.

Collapse
learningtocode profile image
LearningToCode54

Do you think it's still worth it to buy the book?

Collapse
thawkin3 profile image
Tyler Hawkins Author

Yes, absolutely!

Collapse
rodrigobsimon profile image
Rodrigo Simon

Thank you!

Collapse
thawkin3 profile image
Tyler Hawkins Author

Thanks for reading!

Collapse
petrof21 profile image
Bojan Petrović

Awesome summary! Thanks a lot!

Collapse
thawkin3 profile image
Tyler Hawkins Author

Thanks Bojan!

Collapse
psysolix profile image
PsySolix

Great summary!

Collapse
thawkin3 profile image
Tyler Hawkins Author

Thank you!

Collapse
thawkin3 profile image
Tyler Hawkins Author

Sure thing, thanks for reading!