DEV Community

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

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

Tyler Hawkins on November 29, 2021

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...
Collapse
 
thawkin3 profile image
Tyler Hawkins

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

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.

 
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
 
peerreynders profile image
peerreynders • Edited

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

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

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

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

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

You’re welcome, thanks for reading!

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

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
 
rodrigobsimon profile image
Rodrigo Simon

Thank you!

Collapse
 
thawkin3 profile image
Tyler Hawkins

Thanks for reading!

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

Yes, absolutely!

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

Thank you Davide!

Collapse
 
petrof21 profile image
Bojan Petrović

Awesome summary! Thanks a lot!

Collapse
 
thawkin3 profile image
Tyler Hawkins

Thanks Bojan!

Collapse
 
mrdulin profile image
official_dulin • Edited

It's true after I refactored a bunch of rot projects.

Collapse
 
psysolix profile image
Stefano P

Great summary!

Collapse
 
thawkin3 profile image
Tyler Hawkins

Thank you!

Collapse
 
thawkin3 profile image
Tyler Hawkins

Sure thing, thanks for reading!