Clean Code vs. A Philosophy Of Software Design
github.com
github.com
Who else has had to deal with idiots who froth at the mouth when you exceed an 80 line character margin?
And it's not just programming styles, patterns and idioms. It's arguably even worse when it comes to tech stacks and solution architecture.
It's super-frustrating when I'm dealing with people in a professional setting and they're quick to point out something they read in a book, or even worse - a blog - with very little else to add.
This was especially bad during the NoSQL and Microservice hype. Still somewhat feeling it with PAAS/SAAS and containerization. We have so many really really basic things running as Function Apps or lambdas, or simple transformations running in ADF or Talend that add zero value and only add to the support and maintenance overhead.
Always keep in mind that sometimes the only difference between yourself and the person writing the book/blog/article is that they actually wrote it. And that their opinions were written down don't make them fact. Apply your own mind and experience.
Well said!
Honestly, no better indication of a very mediocre developer
People then develop fastidious code formatting rules because they realize well formatted code is easier to read and extend.
Then people realize it’s the organization of the code, not the rules themselves. They have preferences, but don’t treat those preferences as “the one true way”.
So people with fastidious rules are in that middle ground of becoming less bad, and that’s a wide swath of abilities.
Yup, I agree with this
It's not about, let's say, where your curly braces stay, but being consistent
Though in some cases I'd argue that things like a very strict 80 character limit actually results in worse code (or at least worse to read code)
Smarter people write shittier code.
Clean code is for stupider people.
Think about it. It’s because smart people don’t need clean code. It’s so trivial to them and so readable that they really don’t need things to be ultra clean and well formatted.
So the tendency to have this ocd need to write clean code among smart people is random. They either have it or they don’t give a shit.
But among stupid people it’s not random. They need clean code because they are not smart enough to understand code that isn’t clean.
Smart people don’t often know how smart they are and don’t realize how unreadable their code is until code review time and the stupid person points out what the smart person considered “obvious”
That being said what I talk about lives on a gradient. The smarter you are the greater degree of tendency you have to write shitty code.
The cleaner your code the higher possible chance you are stupider. It’s not a definitive sign but there is a correlation.
It was from Charles Simonyi, talking about how as he got older, his prodigious ability to juggle large amounts of information in his head declined, and as a result, he started writing better code. Do you know it?
Also, I half agree with your point, but I see it happen in two different ways. When writing ad hoc code for research purposes, I see very gifted people write seemingly sloppy, to-the-point code because it's the quickest way to the result. I say seemingly sloppy because another programmer would see an intricate mechanism that in so many places is a hair's breadth from being wrong, and they would want to reorganize it to make it more obvious that the code is correct. The savant who wrote the code is like, it's already 100% obvious, how could any change make it more obvious than that?
In the software development context, I sometimes see very gifted people write incredibly complex code because they enjoy flexing their intellectual muscles and seeing the ornate towers they can create. But I also see average programmers and dumb programmers do the same thing, the only difference being that the gifted people can get away with more before it starts to hurt them. What's more, I see very good engineers, gifted but not as gifted, try to follow the example of a savant and end up accomplishing far less than they could if they cut themselves a break and wrote plain code without all the flourishes and ornaments. A gifted programmer usually gets tired of this and grows out of it, but some of them enjoy it so much they commit to fooling themselves and other people that it's the right way to write software.
> So the tendency to have this ocd need to write clean code among smart people is random
It is if they work entirely alone and their work doesn't depend at all on the success of others using their code. However, when it comes to big software projects, my experience is that it's not random: the smartest people do end up writing good code, unless they have ulterior motivations or a severe social blind spot.
Shitty code doesn't run or doesn't do what the author thinks it's supposed to do. You can't write genuinely shitty code and be smart.
I've seen smart people get caught in trying to write "clever" code. Abusing features of a language to make the code "look" smart. And I've never seen someone I've considered smart write completely unformatted code where it matters.
I may not agree with all of their choices, but the smartest people I've worked with tend to have the structure of the code reflect the structure of the problem as they see it in their head. And yes, that tracks, you begin to use the code itself as an assistant to your own thinking. You don't think about where things are because they are where they should be.
Forcing yourself to remember a bunch of pointless minutiae in order to write software isn't a mark of intelligence, it's a mark of someone who wants to be seen as intelligent.
> Then you've never been around the smartest people. Likely you've been around smarter then average people.
This is essentially using your own belief as proof that your belief is correct. You say I haven't been around the smartest people because I say the smartest people don't do what you claim. You are saying "I'm right therefore you are wrong". Maybe you haven't been around the smartest people.
> Yeah and the smartest people structure the problem in their head in a way normal people can't easily understand. They can hold much more in their head so the structures can be complex.
Complex is easy. Simple is hard. And yes, some things are inherently more complex than others. But the goal is to hold the important things in your head. Offload as much as you can so you can focus on what matters.
> Isn't that my point? Formatting rules are a bunch of pointless minutiae to intelligent people. It doesn't assist them in readability because their intelligence allows them to parse even the shittiest code with complete ease. And I mean aesthetically shitty, not intrinsically shitty.
No. It's not the point you are making.
Also, look at to everything I said. Strict adherence to any one style is not a marker of intelligence. I explicitly said that strict adherence is essentially for people in a wide range of skills. But the best have preferences, but realize that they are more guidelines and readability matters more than the rules.
And the rules should be logical and essentially second nature. Like indenting is completely optional in most languages. But proper indenting allows you to better visualize the flow of the code. Nobody reads/writes minified JavaScript.
I have quantitative evidence of this. There IQs were above 150.
>Complex is easy. Simple is hard. And yes, some things are inherently more complex than others. But the goal is to hold the important things in your head. Offload as much as you can so you can focus on what matters.
complex is not easy. And simple is not necessarily always hard. The story is obviously more complex then this.
>No. It's not the point you are making.
It is. It was a rhetorical question.
I looked at everything you said. First off I never said anything about strict adeherence to a style. Smart people have there preferences.
>And the rules should be logical and essentially second nature. Like indenting is completely optional in most languages. But proper indenting allows you to better visualize the flow of the code. Nobody reads/writes minified JavaScript.
I've seen smart people who can do this. They don't even really care.
This is totally unfalsifiable. I claim that the smartest people around always wear clown shoes to work. If you disagree, it's simply because you haven't met any of the people I'm talking about. QED
It's like proving the ground exists when you jump off the bed in the morning. The overly nerdy and stupid HNer can't even move an inch off the bed until the scientific method is employed in attempt to falsify whether the groun doesn't exist. OoooooOOh.
I'm just offering my anecdota opinion here. If you can't listen to normal conversation and you can only read research papers (which mind you suffers from a replication crisis) then more power to you.
So, code is harder to read than it is to write.
So if you write code that uses your full intellect to write it out, you are therefore by definition too dumb to read it.
They are likely writing code with a fraction of their intellect and that code is still too complicated for normal people to comprehend.
His code was really anal.
I have come to learn that there are no "rules," only heuristics.
For example, there's a large chunk of code following an if statement, but it's indented the same as the body of the if. The dev overlooks the closing brace and puts the logic in the wrong place. Additionally, there is a nested if statement whose body is indented less than the surrounding code. It's hard to read, and error prone.
I can't imagine a "good" developer putting up with that, although I admit you don't have to be "fastidious" to prevent this type of thing.
> It still blows my mind how dogmatic some people can be about things like this. I don't understand why anyone takes these things as gospel.
IMO, this is one of the key differences between the two books. CC has a vibe of hard and fast opinion-based rules that you must obey, whereas APoSD feels more like empirically-derived principles or guidelines.That seems like an unnecessarily harsh way to refer to people.
It reminds me of the standard joke about veganism: "how do you know someone is vegan? Don't worry they'll tell you"
It's a very ironic joke, because the people I hear talking about veganism the most are non-vegans complaining about veganism. In this thread too. All I see is people complaining about how dogmatic clean code people are, and I see no examples of that in this thread. The only strong and absolute language I see is from those who are complaining about CC people.
Bear in mind, I don't have a dog in this fight. I'm not vegan, my methods are sometimes longer than four lines, and I do occasionally write a comment. But if this thread is anything to go by, the clean code folks seem a lot nicer to work with than the reactionaries.
I prefer readable code, but I wouldn't call myself a "clean code" person.
I think most of this pushback (certainly all of mine) is about it creeping into production environments and exacerbating personality traits that tend to be problematic in team settings, which does not invalidate the abstract idea and should instead be scoped to its practice.
I didn't read all 416 comments in this thread, but I've definitely worked with some people who religiously follow Clean Code to the point of toxicity. Perhaps the most bizarre example I encountered was the person who insisted that they're a backend dev, that they should never have to open the UI to test anything as tests should always be sufficient, and would continue pushing bugs in to production because the early "startup days" had produced some pretty crappy code with limited (or no!) tests. It was completely unworkable. The team literally had a party after he left. That was after two people had quit in large part due to this person.
I have also worked with some people who generally liked Clean Code but didn't follow it religiously in spite of what works for this specific project. That's fine: that's just a normal type disagreement like Python vs. Ruby or where to put the braces or whatever: you talk to each other, and reach some solution that reasonably works for everyone.
And the "reactionaries" are mostly just normal folks who want to get stuff done.
The entirety of the complexity was essential and dictated by an external data model used nation-wide for interoperability.
Based on my experience and the OP dialogue, Uncle Bob is a vanity-driven narcissist and an infectious fraud.
But that difference is actually huge. I think you are downplaying the value of the writing process. Assuming that writer is acting in good faith and truly tries to provide the best possible information.
But when you start writing, you start noticing that this idea might not be that good after all. Maybe I need to read more about this? Did you note everything? This idea conflicts with this other topic than I just wrote? What is correct? And the list goes on. When you structure all your thoughts as written text, it is easier to detect all conflicting ideas and mistakes. Not all writers are that good, but you should understand what I mean.
I'm fond of saying that anything that doesn't survive the compilation process is not design but code organization. Design would be: which data structures to use (list, map, array etc.), which data to keep in memory, which data to load/save and when, which algorithms to use, how to handle concurrency etc. Keeping the code organized is useful and is a part of basic hygiene, but it's far from the defining characteristic of the craft.
Comments about code style are usually subjective, and, they can be easily dismissed as a personal preference, or, in the case of a Jr dev, as a lack of skill.
Until they bring up "The Uncle Bob book". Now, suddenly, a subjective opinion from a Jr dev looks like an educated advice sourced from solid knowledge. And other people now have a reason to listen up.
All of this is totally fabricated, of course. But it's like the concept of money. It's valid only because other people accept it as valid.
I usually tend to take people who add prefixes like senior to their function title, less seriously. I've seen young devs who write better code than so called senior devs.
I'm with you, but I don't think it makes sense to elevate one absolutely over the other as the "defining characteristic." Either one can tank the development of a piece of software and prevent it from coming into being in a useful way.
Arguments about which aspects of software are more important than others usually arise between people who have personally suffered through different ways that projects can fail. Any aspect of software development will feel like the "defining characteristic" if it threatens to kill your project.
That does not make sense to me. There can be thousand things that can kill project. One has to consider what are the odds for them.
No matter how long I work (twenty-five years so far) I think my personal experience is only enough to know that if I've seen something, it probably happens fairly often. If I've never seen something, it still might be common for all I know.
Programming languages can (should!) be amazing tools for thought rather than just tools for making computers do things; using these tools to figure out what we're doing is a critical part of effective development. The highest-leverage software engineering work I've seen has involved figuring out better ways of thinking about things: developing better tools and abstractions. Tools and abstractions compound since they fundamentally impact everything built on top of them. A good high-level design is the difference between a team that can add some specific capability in a day, a team that would take six months and a team that would say it cannot be done.
It's so easy to get it spectacularly wrong and end up in a mess.
And it's seems so deceptively pointless at the start. It's easy to throw together a greenfield project and get something working, but make a complete mess of organisation. But then it becomes so expensive so quickly to then make changes to that code.
I've joined quite a few projects after 1/2 years of someone else making the project. And so often it's such an imposing mass of code that basically does sod all. Bad architects who don't understand why they're even using the patterns they are or mid/junior-level coders making projects is basically a recipe for the project just grinding to a halt just when it looks like you're getting near the end.
It's when 1,000s of lines are easily refactored to 100s that you start thinking, how can these people honestly believe they have the ability to lead a project? They are so clearly completely out of their depth it's depressing.
We seem, as an industry, to have a complete inability for management to distinguish genuine senior developers from people who will never be.
It's also the code documentation.
Having documentation that is legible is good, right? And so a reviewer is reasonable to say "this is hard to read" since it's failing at its primary purpose.
Most of software is about assigning big words and over complicated nomenclature to concepts and these things masquerade as things with deeper meaning when in reality it’s just some made up opinion.
Software design is an art. It is not engineering and it is not science. That’s why there’s so much made up bullshit. The irony is we use “art” to solve engineering problems in programming. It’s like ok we don’t actually know the most optimal way to program a solution here so we make up bs patterns and philosophies. But then let’s give this bs pattern some crazy over complicated name like Scientology or inversion of control and now everyone thinks it’s a formal and legitimate scientific concept.
Well cats out of the bag for Scientology. Not yet for a lot of the bs in software. A “philosophy” is the biggest utter bullshit word for this stuff I’ve ever seen.
Over time we get to find patterns, common issues and ways to fix them, etc. It doesn't have to be strict patterns but overall strategies.
If we don't do that then it's just vibes right? Where's the engineering part?
I write very little unit tests as my coding style that employs static checks as viciously as possible doesn’t necessitate it.
I would say only 30 percent of patterns are good and shared. The other stuff is just artistry and opinion. Like method name length or comments or OOP.
I don't think patterns as a whole are good but there are known issues and structures to existing problems so boiling it down to art seems reductionist imo.
I can code and not test behavior and have that behavior work reliably without tests. Key word is the unit trst. Typically IO and things that live outside these boundaries need integration tests.
Most of web programming today actually doesn’t need much unit testing. You’re not doing much processing. The web layer functions as a router and that layer is a meta layer that writes code that executes somewhere else.
Over half the code executes as sql. Integration tests are by far more important.
Boiling it down to an art is not reductionist. It’s true. Where is the scientific method in programming? How was a pattern deduced using the scientific method? If it was not deduced using the method then was it created from axioms and logic like math? Is it a theorem?
No. It’s all just made up. And we have no quantitative way of verifying why one design is better than another design. That’s why software technology often moves horizontally. There’s no way to verify the current design was better than the last.
Even both you and I have a disagreement and are at a stalemate. Can you prove your unit testing is superior to my static testing? Not really. Actually tbf static checking is provably better if you don’t count the dimension of effort required to use dependent types.
Oh, I agree that there usually isn't a scientific method to programming. I think there could be though. Not for everything of course, some things will always be up to personal taste and interpretation but the cursor could probably be moved with some effort in analyzing existing codebases at scale, doing surveys, internal testing of different approaches in large companies. Something more akin to what you see in social sciences, even if it might be a bad word in some circles!
We probably won't agree but I legitimately enjoy hearing about how people code.
The other language is rust. Though it's type system is not as expressive as typescript it is strict meaning nobody can really cheat their way out of it. In general Rust code requires less unit tests then typescript because of this.
The other language is Idris and Haskell. But these languages are rarely used.
This article can shed some insight into what I'm talking about: https://wiki.haskell.org/Why_Haskell_just_works#:~:text=The%...
Since you're doing your own parsing rather then using schema validators and existing formats like json, yes your code is doing A LOT of data processing and thus requires a lot of unit tests. Most of the time developers can trust the parsing libraries.
Parsing is kind of… everywhere. Path piece instances? Parsing. Forms? Parsing. The whole point of smart constructors is parsing. Deserialising from the persistence layer? Parsing. Sure, JSON and Protobuf also, but even when relying on a robust library like aeson, we still write tests. Why wouldn’t you? The types you define can be serialised in different ways, and the way you deserialise needs to roundtrip with the way you (or an external system) serialise(s), which also necessitates more tests.
Here is a clear example of what I am talking about.
https://jezenthomas.com/2022/12/at-least-roundtrip-serialisa...
No. It’s all just made up. And we have no quantitative way of verifying why one design is better than another design. That’s why software technology often moves horizontally. There’s no way to verify the current design was better than the last."
Thanks for saying this. I've never stopped rolling my eyes at the assertion that web programming is an engineering discipline.
I don't think patterns as a whole are good but there are known issues and structures to existing problems so boiling it down to art seems reductionist imo.
There is optimization and there is modularity. All 3 of these dimensions are intimately tied and correlated.
Coding is about building a computable model of some facet of existence, usually for some business. When it comes to model building, comprehension and communication are paramount. Performance and other considerations are also important but these are arguably accidental features of machines and, in an ideal world, would not actually affect our model.
Similarly, in an ideal world, we wouldn't even need programming languages. We'd be able to devise and explain computational systems in some kind of perfect abstract language and not need to worry about their realization as programs.
I think a lot of these blanket philosophies confuse people by not emphasizing the higher level aspects of the activity enough. Instead people get hung up on particular patterns in particular paradigms/languages and forget that the real goal is to build a system that is comprehensible to the community of maintainers that need to work with it.
For instance, abstraction is good and short methods are good to some extent (who wants to read a 2000-line function?), but as John points out in the article, these can be taken too far, where they create new and perhaps worse problems.
It seems there's a pendulum that swings back and forth. We go from big up front design, to Extreme Programming, to a pervasive object-oriented design culture, back to other paradigms.
Its incredibly simple to just follow whatever someone is telling you to do, sometimes I wish I could live like this so I didn't have to fight with the people that do all the time.
In programming it's the Wild West. Many claims are made based on nothing at all. It's very rare to see any kind of sensible research when it comes to the science part of CS. But following rules makes life easier. Even if rules are bad. That's kind of why conservatism exists as a political idea.
Have you discovered the work of Victor Basili?
https://www.goodreads.com/review/list/21394355-william-adams...
His clean code work is certainly pretty dogmatic. As I recall, he says that Java is not object oriented.
But if my memory serves me correctly, his book about C++ (Designing Object-Oriented C++ Applications Using the Booch Method) has some excellent parts. His description of the difference between a class and an instance is one of the better ones.
Then there is the famous sudouko puzzle incident, in which a student trying test-driven development can't get the solution. It is a very instructive incident which illustrates the TDD is unlikely to help you solve problems that are beyond incremental changes. Peter Norvig's solution makes that very clear. Uncle Bob does not seem to realize that.
> Who else has had to deal with idiots who froth at the mouth when you exceed an 80 line character margin?
But I admit in my youth, I was pretty dogmatic about languages and development practices, so I've been that guy.
Thank you for expressing this niggling problem with TDD. Personally I just cannot use it for "new stuff", I need to explore and create direct with "real" code for anything non-obvious.
I think "code is carefully designed AND has a bunch of decent tests" places a codebase ahead of many tbh, even now, regardless of how it's produced
Develop, Ship, Tell your boss you tested and documented.
I want this to do X when I ask it with Y param. If you write out your spec's before you sit down to code.
When a user enters a bad password... print this to screen
TDD works great for new stuff when viewed as paper replacement.
I think that says more about the person at the keyboard and their lack of familiarity with the solution space than anything about TDD per-se. You still need insight and design with TDD, blind incrementalism was never a good idea.
I have used TDD professionally in several development teams. It's useful in the right team. TDD works well when you are not too dogmatic about it. As with everything, you need people in the team that are experienced enough to know when and where. I think the same is true for any tool, coding standard, best practice or what have you. You have to know when to deviate.
I've also held entry courses at university level teaching introductory programming. I believe that TDD can be a good tool teaching programming. Students tend to sit down and write a complete program and then start debugging. TDD teaches them to write small bits at a time and test as they go.
Java technically isn't OO in the strictest sense (Smalltalk, Ruby). It is OO in the modern sense (where modern >= 1980s, C++). Though I am not sure if this is what Bob is referring to - I don't have any respect for the man or his ideas, so my biased guess is his definition of OO is shared only between him and his fans.
If my memory serves me correctly, Grady Booch himself had a book with roughly the same title, except that his name would not be in the title, of course, but would be there as the author. I think I read a good amount of it long ago, and liked it.
Edit: I googled, the book is mentioned here under the section Booch method :
Although I can see why you might consider him more "aggressive", I personally think it matters much more that he was, in general, far more descriptive of his reasoning.
Merely having an opinion is the easy part; being able to clearly articulate the reason(s) why one has a particular opinion is far more important, especially in this kind of conversation, and I in that regard I repeatedly found UB lacking.
In 2006 Ron Jeffries wrote four blogs about solving Sudoku in Ruby with TDD. His blogging effort ended before he completed the solution. I think he got interested in something else and left the whole thing hanging.
That same year Peter Norvig wrote a Sudoku solver in Python using a constraint based approach. You can see their respective documents here. https://ronjeffries.com/categories/sudoku/… https://norvig.com/sudoku.html
The anti-TDD lobby, at the time, hailed the two documents as proof that TDD didn't work. Ha Ha, Nya Nya Boo Boo.
I was aware of this silliness, but never bothered to study it. I had better things to do. Until March of 2020. Then I thought I'd use Sudoku as a case study for Episode 62 in http://cleancoders.com.
I had not read either of the previous documents and decided to maintain that ignorance while writing the solver in Clojure using TDD. It turned out to be a rather trivial problem to solve. You can see my solution in http://github.com/unclebob/sudoku
I don't know why Ron stopped blogging his Sudoku solver in 2006; but he picked it up again in 2024 and has written much more about it.
The trick I used to solve Sudoku with TDD was to consider the degenerate cases. Sudoku is usually a 3x3x3x3 grid. Let's call this a rank-3 problem. I started with a rank 1 problem which is trivial to solve. Then I moved on to a rank 2 problem which was relatively simple to solve; but was also very close to a general solution. After that I could solve rank N problems.
The TDD strategy of starting with the most degenerate case (rank 1) and then gradually adding complexity may not have been well known in 2006. TDD was pretty new back then. If you explore Ron's first four blogs you can see that he briefly considered rank 2 but opted to go straight into the rank 3 case. The sheer number of variables for each test (81) may have played a role in his loss of interest. In my case (rank 2) I had far fewer variables to deal with.
Made every engineering planning session a pain in the ass.
Was often quite wrong and always infuriating.
"akshually this should try to follow more SOLID principles"
But, coming from a formal engineering background, I thought this is what it meant to be a professional software engineer. Little did I know these "principles" were just the musings of a consultant lol. Turns out most folks have good intentions and want a standardized way to write code, but for some reason it always results in code that looks like the Enterprise FizzBuzz meme repo.
I wonder if that's because in a lot of cases (depending on the domain) the space of possible valid/working solutions is near infinite, and if you don't have hard requirements that are backed up by measurements you're free to concieve of any valid system structure and justify it as 'better' without that ever being something that can be observed and measured.
The secondary problem is that book authors have become extremely good at inventing pseudo-evidence to support their claims. It most commonly takes the form of “I talked to X companies with Y total number of employees over Z years and therefore I know what works best”.
If you cut out all of the grandstanding, it’s nothing more than “just trust me” but in a world of social proof it sounds like it’s undeniable.
Which results in the idea of 'good practice', 'best practice' and 'bad practice', and nobody wants to be seen as the person doing things that are considered bad practice, because that would imply that you're a bad developer.
And I almost always hesitate to use the term 'engineer' because as far as I know engineering is considered to be a practice/process that uses measurements and results to drive decision-making, unlike various areas in software. Can you imagine if the same kind of thinking was applied in civil engineering? "This new material has a lot of stars on GitHub and everyone is saying the old materials are bad practice."*
* Which is a thing of course (see: Asbestos) but only in the places with measurable observable outputs.
The problem I'd say is the reactions from our community to such pleas. My degree and original profession is Electrical Engineering, and Engineers not as personally attached to the tech as software developers are, nor they conduct themselves like this.
Other than with civil engineering where each bridge failure will be studied by the whole community, in programming most people shrug it off as "software error there is nothing we can do" and move on.
If we truly want to progress in terms of code quality we need to anchor our guiding principles in the lessons learned from failed projects and check all new positive principles against those.
Not once in my 11 year career. But almost every codebase I've worked on has had debilitating maintainability issues because the only principle other engineers seemed to follow was DRY, at the sacrifice of every principle in SOLID.
The point is not about being DRY, on itself, though. The point is that the code then has better abstractions which are easy to reason about.
UB seems to take abstractions a lot too far, replacing e.g. 2 lines of very clear code with some cleartotals abstraction.
Of course, a method is but one of several ways you could capture that information, and not always the best one.
I've had one which violated DRY and every SOLID principle…
Well, Liskov might not have been violated, but it was hard to tell what with all the other nonsense in the 120 kloc of copy-pasted pantheon of god-classes that showed flagrant disregard for things so fundamental that you wouldn't think anyone even could even get them weird, e.g. the question of "how are properties defined" being "solved" by having an array which was indexed by named constants… and because it was a god class, which items in that array ever got instantiated depended on which value was passed to the constructor.
Eventually, I found they'd blindly duplicated an entire file, including my "TODO: deduplicate this method" comments, rather than subtype — and their excuse when called out on this was the access modifier, as if changing "private" to "public" was hard.
I love reading books for different perspectives.
However, I’ve come to despise people who read books and then try to lord their book knowledge over others. These are the people who think that they have the upper hand in every situation because they read some books. They almost always assume you haven’t read them. If you point out that you have also read them, they switch the subject to another set of books they read because they don’t like when someone tries to undermine their book knowledge superiority.
It’s even worse when the person reads books outside of their domain and tries to import that book knowledge into the workplace. The absolute worst manager I had was a guy who read a lot of pop-psychology books and then tried to psychoanalyze each of us according to those books.
Everything should follow SOLID and clean principles and be implemented using design patterns.
Maybe it's a right of passage to burn yourself badly enough over-architected solutions, where you finally start to understand you don't need all the complexity. Write the code for humans, as simple as you can. Keep large performance issues in mind, but only code around them when they become a problem or are extremely obvious. If anything, it's helped me to steer junior developers away from complex code, while encouraging them to try it out in their own time. Go ahead and figure things out on your own, but let's not do it on a shared codebase, please?
A 42" 4k TV (got it for $2-300 at the start of the pandemic) gives me four 80-90 column text windows on a mid-tier chromebook. You could not pay me enough to do that same work on a laptop, even a $4k MBP.
(But yes, even with lots of real estate 80 columns is still a net win)
It goes along with being into BJJ, chess, vim, keto, linters, and “the dominance hierarchy”.
It’s annoying, but most everyone went through it. If you didn’t know better, how could they?
;-)
For example, even with widescreen monitors, it is still useful to limit line length. Why? Because many people will have multiple source files side-by-side on one of those widescreen monitors, at which point it makes sense for them to not run on indefinitely.
And of course, that is just a guideline, one that I break regularly. However, if it's a method with many args, I'll break the args onto their own lines.
However, the overriding concern is that an organisation works to code towards a common style, whatever that may be, so that unfamiliar code is predictable and understandable.
Full of terrible advices, he never wrote anything significant (in scope and notoriety) during his time as a software engineer like many other prominent authors at the beginning of the agile era. The success is only the result of a wave of junior devs searching for some sort of guidance, something that there is a never-ending need for.
Horrible recommendations that produced a lot of code that is a pain to work on with the abundant amount of indirection it has. Really painful guys.
1: I'm aware it's a software engineering book, but since there are very few B.S. Software Engineering programs out there, You Know What I Mean ™
The issue is that, some never grown out of it. I interviewed with companies where they give the book to any new intern/junior. Then, during the hiring process, they don't even ask if you read it, they straight up ask questions about your knowledge of it. Like "What does Uncle Bob says about X in his book Clean Code?". And they constantly refers to it. Some people go as far as quoting it in PR.
The worst part being that once they leave their company, since they don't know anything else, they'll apply the same stuff elsewhere & convert their new company to it.
I agree entirely. My encounters with Uncle Bob were as a junior developer receiving advice [no "s"] from other junior developers.
And yes, I too find it suspicious how many mavens of the "Agile era" never really managed to ship anything.
Like, I personally prefer the bare assert style of testing (like pytest), but the junit style is basically everywhere now.
Look, both authors are very smart people who have great insights into development that we can all learn from ... but both also have the failing of being way too in love with their own ideas.
It blinds them to the flaws in those ideas, and makes it so when you read their work you have to be skeptical and evaluate each individual idea on their own.
Because it works. Have you tried it?
Yeah, but to be fair I would argue that in the limit, real code reviews end up looking a lot like pair programming, and you can avoid a bunch of back and forth by both people being present during development.
Obviously you still need code review (for regulatory reasons in a lot of cases), but the amount of times I've ended up on a Zoom talking through mine and others PRs makes me believe that pair programming would help here.
I don't like pair programming for personal and cultural reasons (eg: I am uncomfortable with the _process_, but not the results), but the people in the pro-pair-programming camp will say the quoted bit above is one of its strengths. You back/forth and fix or change things early in the process.
Doing a code review is "too late", since these early decisions that could have been made differently/better effect all the code that follows it into a further less-optimal space.
Those ideas do have flaws, and most of us are looking to improve how we right code. So if you aren't blind to those flaws, please, write a book or a blog or whatever on the best ways to write software so that we can all learn.
I think these paradigms and patterns often operate on the wrong layer of abstraction, while mostly ignoring the things that matter the most, like efficiency, error handling and debugging.
But getting good at these things requires a lot more blood, sweat and tears, so there's no easily teachable recipe for that.
Of course error handling and debugging are part of maintenance. However there is a lot more than those two that need to be considered as well.
There is reason to hate Clean Code, but the worst adherents to the rules are still producing far better code than some of the impossible stuff that happened before. "Goto considered harmful" is one of the early steps in fixing all the bad things programmers used to do (and some still do), but you can follow the "rules" of goto considered harmful and still produce really bad code so we need more.
- third party dependencies, compatibility issues, breaking changes
- code that is bloated with abstractions and indirection
- performance issues, especially when worked around via caching etc.
- bad error handling
- inconsistent data
Simpler code that can be followed and stepped through in a straight forward manner avoids 3/5 of these from the get go. Patterns and abstractions that emerge over time are sometimes beneficial. Legacy code or third party code that overuses abstractions is really more of a hindrance and significantly slows down how fast I can understand, own and fix things.
I hope you can come up with a good answer to fix the problems you cite. I haven't seen anything I have confidence in.
- Premature abstractions that are subsequently worked around around the edges.
- Automation built on top of wishful thinking instead of how people actually prefer to work.
- Inconsistent naming and file structure.
- Adopting many advanced or hyperspecific styles and techniques instead of leaning on more basic ones that already solve a problem.
I find it way easier, to go from more primitive, consistent, "inlined" code to more sophisticated abstractions that clearly solve emergent problems at a later point in time, than changing the direction of already factored, high level code.
To me it's a matter of mental capacity and friction as well. This stuff is hard, time consuming and requires deliberate effort. I rather accept that code goes through different phases of maturity and that using and maintaining the actual programs over time, gives me a much clearer path forward than applying patterns and principles in advance on an abstract level.
A caveat here is that many of these inconsistency factors come from adopting third party dependencies. In my experience this is the largest chunk of liability or tech debt.
---
However, a large factor that is often overlooked in these discussions, is also organizational scale. When you have to own and understand things from A-Z you can get away with different things than if you are one part of a much larger team and vice versa.
I'm firmly in the former category, so I have to optimize for being able to take a much larger slice of responsibility. But I can also get away with leaving code in a more raw state for longer. I assume this balance looks different the more people you add to a project or organization.
I think we need to take this into consideration in these public discussions. It's ultimately a trade off and programmers from different backgrounds will have different sensibilities.
Depends. If you reach the codebase at the point where it's already accumulated subtle differences between codepaths that should be the same, that's far harder than inlining and rearranging something that's been factored badly, IME.
> A caveat here is that many of these inconsistency factors come from adopting third party dependencies.
I find the exact opposite. Developing too much internally creates a lot of inconsistency. If you hit an incompatible change in a third party dependency, odds are a lot of other people have hit the same thing and can tell you what to do.
https://github.com/unclebob/fitnesse
You can see how all his ideas come together into a ball of hundreds of almost empty classes, and gems such as "catch Throwable".
https://github.com/unclebob/fitnesse/blob/master/src/fitness...
Yup, OK, got it. The FitnessContext looks a bit rough, but no big deal.
https://github.com/unclebob/fitnesse/blob/master/src/fitness...
That's completely readable, I get it.
https://github.com/unclebob/fitnesse/blob/master/src/fitness...
Again, looks fine.
The main issue with all of these files is that there is quite a bit of boilerplate code, but that's Java's fault, not the code's fault.
I'm sorry, but I disagree. Looking for the most substantial pieces of code that I could find, and they look really good. I'm sure there are some little utils or something that look strange out of context, but I would be thrilled if I were called to work on legacy code and it was this nice.
For example, I am writing some driver software for a USB device right now. It is so easy to get the device into a bad state, even when staying within the documented protocol. Every time I implement a workaround, or figure out exactly how the device expects a message to appear, I put in a comment to document it. Otherwise, when (inevitably) the code needs to have features added, or refactoring, I will completely forget why I wrote it that way.
The prime number example is a self-contained, deterministic algorithm. While I did find it far easier to parse with comments, I could still spend the time to understand it without them. In my USB device driver, no amount of review without comments would tell another person why I wrote the sequence of commands a certain way, or what timings are important.
The only way around that would be with stupid method names like `requestSerialNumberButDontCallThisAfterSettingDisplayData` or `sendDisplayDataButDontCallTwiceWithin100Ms`.
Nobody practically communicates with big words. A long function name only pops up when needed.
Yep. Method names make terrible comments. No spaces, hard to visually parse, and that's before acronyms and ambiguity enter the conversation.
As the person who often writes borderline-essay-length comment blocks explaining particularly spooky behaviors or things to keep in mind when dealing with a piece of counterintuitive/sensitive/scary code, my reason for mega-commenting is even simpler: all the stuff I put in comments should absolutely instead live in adjacent documentation (or ADRs, troubleshooting logs, runbooks, etc). When I put it in those places, people do not read it, and then they do the wrong things with the code. When I put it in comments, they read it, as evidenced by the rate of "that bug caused by updating the scary code in the wrong way happened again"-type events dropping to zero. It's easier to fix comment blocks than it is to fix engineers.
Which is why snake_case or kebab-case (if the language allows it) is much better than PascalCase or camelCase.
Even worse when camelCase enters into JSON because people want to automate the serde but are too lazy to make the actual interface (the JSON Schema) easy to read and debug.
I really like the approach and as other comments already mentioned, this is a nice way to capture the "why" of specific decisions that go beyond the "how" in the code.
Here is a good starting point for people not familiar with ADRs: - https://cognitect.com/blog/2011/11/15/documenting-architectu... - https://adr.github.io/
This is the main reason for comments. The code can never tell you "why".
Code is inherently about "what" and "how". The "why" must be expressed in prose.
Can't we just say "comments are useful here" without trying to make it into a case for $methodology?
That's not to say embed nothing into the names. I'm quite fond of the "Long Names are Long" blog post[1]: names need to clearly refer to what the named thing does, and precise enough to exclude stuff it doesn't do. Names can certainly get too short, e.g. the C "sprint fast" function `sprintf` is probably too short to be easily understood.
[1] https://journal.stuffwithstuff.com/2016/06/16/long-names-are...
I believe in general there is a case for this (your case sounds like a perfect candidate). The implementation of Dtrace is another example[0] full of good description, including ASCII diagrams (aside: a case for knowing a bit of Emacs (though I'm sure vim has diagramming too, which I would know if I pulled myself out of nvi long enough to find out)).
[0] https://github.com/opendtrace/opendtrace/blob/master/lib/lib...
It’s annoying to look at but when you actually read the function you know what it does. A more elegantly named function is less annoying to read but less informative and doesn’t provide critical information.
The name just looks ugly. But it’s like people have this ocd need to make things elegant when elegance is actually detrimental to the user. Can you actually give a legitimate reason why a method name like that is stupid other then its “hard to parse”. Like another user said… use snake case if you want to make it easier.
I didn't feel like my code became better or easier to maintain, after reading other programming advice books, including "Clean Code".
A distant second recommendation is Programming Pearls, which had some gems in it.
It’s like a secret unlock code for domain modeling. Or deciding how long functions should be (5-20 lines, with exceptions).
I agree, hugely usual principle.
If you start there, you have room for your abstraction to bend before it becomes too brittle and you need to refactor.
Almost never is an interface worth it for 1 implementation, sometimes for 3, often for 5-20, sometimes for >20.
The trick is recognizing both a “unit of complexity” and how many “units” a given abstraction covers. And, of course, different units might be in tension and you have to make a judgement call. It’s not a silver bullet. Just a useful (for me at least) framing for thinking about how to manage complexity.
The sin() function in the C standard library covers 2⁶⁴ cases, because it takes one argument which is, on most platforms, 64 bits. Are you suggesting that it should be separated into 2⁶⁰ separate functions?
If you're saying you should pass in boolean and enum parameters to tell a subroutine or class which of your 5–20 use cases the caller needs? I couldn't disagree more. Make them separate subroutines or classes.
If you have 5–20 lines of code in a subroutine, but no conditionals or possibly-zero-iteration loops, those lines of code are all the same case. The subroutine doesn't run some of them in some cases and others in other cases.
Rarely, a function with a single line or an interface with a single element or a class hierarchy with a single parent and child is useful. Mostly, that abstraction is overhead.
Often, a function with 5-20 lines or an interface 5-20 members or a class hierarchy with 5-20 children is a useful abstraction. That’s the sweet spot between too broad (function “doStuff”) and too narrow (function “callMomOnTheLandLine”).
Sometimes, any of the above with the >20:1 complexity ratio are useful.
It’s not a hard and fast rule. If your complexity ratio falls outside that range, think twice about your abstraction.
Do I need 5-20 non-trivial test cases to cover the range of inputs this function accepts?
If yes, function is probably about the right level of behavioral complexity to add value and not overhead.
If I need only 1 test or if I need 200 tests it’s probably doing too much or too little.
I don’t think you need only 20 test cases for open(). Sometimes, more than 20 is valid because you’re saving across some other dimension of complexity. That happens and I don’t dispute it.
But the fact that you need >20 raises the question: is open() a good API?
I’m not making any particular judgment about open(), but what constitutes a good file API is hotly contested. So, for me, that example is validation of the principle: here’s an API that’s behaviorally complex and disputed. That’s exactly what I’m suggesting would happen.
Does that help clarify?
No, 20 test cases is not enough for open(). It's not even close. There are 36 error cases for open() listed in the Linux man page for it.
What constitutes a good file API is not hotly contested. It was hotly contested 50 years ago; for example, the FCB-based record I/O in CP/M and MS-DOS 1.0, TOPS-20's JFN-based interface, and OS/370's various access methods for datasets were all quite different from open() and from each other. Since about 35 years ago, every new system just copies the Unix API with minor variations. Sometimes they don't use bitwise flags, for example, or their open() reports errors via additional return values or exceptions instead of an invalid file descriptor. Sometimes they have opaque file descriptor objects instead of using integers. Sometimes the filename syntax permits drive letters, stream identifiers, or variables. But nothing looks like the I/O API of Guardian, CP/M, Multics, or VAX/VMS RMS, and for good reason.
IMO, a function _can_ be an interface in the broadest sense of that term. You’re just giving a name to some set of code you’d like to reuse or hide.
Clean Code pushed things in a better direction, but it over-corrected. In many ways APOSD (published in 2018) is a correction against the excesses of Clean Code (published in 2008).
Will people swing too far back, to giant methods, deeply nested conditionals, etc? I don't know. But probably.
Other examples are TDD. Forcing myself to write tests for everything for a period has made all my code since better, even though I don't practice TDD now.
You can just scroll down and see what happens in a linear fashion.
It's all to do with how your brain processes what it's seeing, and the planning processes involved in getting to the next bit of information it needs. If that information is off-screen, then the mechanisms for stashing the current state and planning to move your hands in whatever way necessary to bring it onscreen will kick in, and that's a sort of disfluency.
Similarly with tokens too far from whatever you're currently focused on. There's likely to be a region (or possibly a number of tokens) around your current focal point within which your brain can accurately task your eyes to scan, and outside that, there's a seeking disfluency.
I think this is why you get weird edge cases like k and j, where they pride themselves on having All The Code in one 80x24 buffer, and it actually works for them despite breaking all the rules about code legibility.
The result was surprisingly pleasant, and it changed how I feel about this sort of thing. I think the Clean Code approach makes a lot of sense when you are working on a big project that contains lots of code that other people wrote, and you rely on IDE features like jumping to definition, etc. But if you can write code that fits on one screen without scrolling, something special happens. It's like all the negative aspects of terse code suddenly vanish and you get something way simpler and overall easier to work with and understand. But you really have to work to get it to that point. A middle ground (terse code but still spread out over lots of files, lots of scrolling) would be the worst of both worlds.
There have been some pendulum swinging around things like monoliths/microservices, but even then the amount of people that those things effected is actually much less than the larger community of programmers as a whole.
Having read Clean Code back in college as it was assigned reading, it was absolutely the vibe I got from Uncle Bob generally. See any number of lines at the same indentation level, select them, extract method, name it vaguely for some part of what it does, repeat.
I honestly think that it comes from this type of school of thought that a function should be X lines rather than a function achieving a function. Thinking about this now, it's sort of the difference between "subroutines" and "functions".
Working on their code, I thank god for modern IDEs ability to inline. I often go through and restructure the code just to understand the full scope of what it's doing, before restoring what I can of the original to make my changes as minimal as possible.
Are we talking about the same thing and you'd still find that hard to understand?
On the meta level, these exchanges, while mildly interesting, have the vibe of debating how many angels can dance on the head of a pin. I'm reminded of the old saying: "Writing about music is like dancing about architecture." If you want to write good code, read good code. Develop a taste that makes sense to you. I don't think I'll ever read a book about code composition again.
As a junior in the field working at a small company, I often rely on this community for guidance, and this seems the most sound advice on this thread.
It is a bit sad but I think with the advent of LLMs some of the stylistic quirks of programmers past will become a bit anachronistic. Still, they will present opportunities for code archeology.
What I mean by that: Good code is code that has proven itself by surviving quietly in a long-living project that has changed a lot over many cycles of new engineers (experienced or otherwise) being onboarded. The less you hear people complain about it but the more you find people using or relying on it in some way, the better the code. If people are loud about how much they like it, it’s either new, or it’s something they’ve convinced themselves to like but know in their hearts is bad. It’s the stuff that just works that’s good - it’s so good people don’t even notice it.
Conversely, looking for the parts of a codebase with the highest churn will tell you immediately what all the devs on that codebase will complain about, if you ask them. This has worked for me extremely well across a number of projects.
Surely you don’t just presume the quality of code based on the person who wrote the code, right?
This reminds me of what Dijkstra said (paraphrasing): the computation is the important thing, not the code.
Some other heuristics:
* Every if statement is a chance of a bug because the code has two or more paths to follow. Keep the choice making at the business/requirements level of the code, not hidden inside lower level decomposition.
* A switch statement that is not exhaustive (ie covers all possible values) is a change of a bug, especially if there is no default case.
Modern languages with better type systems make the second point less relevant because they require exhaustive pattern matching.
This is known as the cyclomatic complexity of a program: https://en.wikipedia.org/wiki/Cyclomatic_complexity
A corollary to this is that it is also beneficial to converge separate paths as quickly as possible (e.g. using non-nullable types and default values) or converge them all to the same place (e.g. nonlocal exception handling).
You can also read critical reviews of someone else's work, compare them with the work in question, and see if the critic's punches land or if they look like misses.
^ This, I thought, was a good takedown of Clean Code, highlighting some cases where Bob Martin made too many overly thin functions that lacked meat and made it hard for the reader to gain context for what the function was trying to do. [1]
I would also say, reading "the same code" in different programming languages might get you a feel for if you prefer code to be more verbose or more terse, more explicit or more implicit. e.g. https://rosettacode.org/wiki/Globally_replace_text_in_severa...
[1] sometimes derisively referred to as "lasagna code" or "baklava code" -- https://www.johndcook.com/blog/2009/07/27/baklav-code/
Lambda calculus, basically :)
Brilliantly phrased metaphor, thank you.
Other than people going overboard with empty classes and inheritance Ive not really seen a problem of people breaking down functions too far.
Which parts are important to grow out of?
X
1111111111111111111111111
1111122222333334444455555666667777788888999990000011111222223333344444
35791357913579135791357913579135791357913579135791357913579135791357913579
!!! !! !! ! !! ! !! ! ! !! ! !! ! ! ! ! !! !! !
3 |||-||-||-||-||-||-||-||-||-||-||-||-||-||-||-||-||-||-||-||-||-
5 |||||||||||-||||-||||-||||-||||-||||-||||-||||-||||-||||-||||-
7 |||||||||||||||||||||||-||||||-||||||-||||||-||||||-||||||-||||||-
11 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||-||||||||||-
13 ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
...
113||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
That's verbatim, I'm not unfairly clipping context away from it. Like, what the hell is that supposed to tell someone?! Wouldn't it just be easier to drop a link to the algorithm, or briefly explain the algorithm, or just give a name of the algorithm so someone can look it up? Instead he just talks about taking a bike ride to understand it and making a weird picture. He also has bizarre arguments that if something can't be expressed in a programming language, it's the fault of the programming language (what?!) and that code is more understandable than English. I really find it hard to believe that he thinks these are actually good arguments, I just get the impression he does not want to concede that he was wrong about this.Especially the buildup from how bizarre the understanding of UB of comments is to actually seeing one “in the wild”.
Yes you are, you didn't attach the surrounding code where this comment was found. That comment would make a lot of more sense even just with the function name.
> If you need the code to understand the comment, the comment is a failure.
Nope, not all comments are for the public API. Some are very context-specific and can only be understood in that context. Fully providing all the excessive details in the comments that it becomes context-independent is pointless and time-wasting.
just not only primes ^^
I liked this bizarre comment. It was like seeing a physical geometry proof for trig. Or like thinking about primes while riding a bike for an hour.
In 10 - 15 seconds this comment offered a flash of appliable intuition into primes I'd not appreciated before.
Granted, the bulk of that was first gathering that the top 3 rows of digits were a series turned sideways (printing them rotated would have made that instant). Joys of plaintext.
But then the pattern popped, and the code, including the optimization, made sense, but now from the "grok", with MTOWTDI.
Neither their commentary nor their function names and comments, caused the grok. I could "accept" the assertions, but to me neither naming nor comments were intuitively self-evident the way the diagram was.
Both of them commented on having to dwell on what the code was doing to consider refactoring. Once this flash happens, one no longer needs reference code at all, it's just another property of primes.
I now comment on method or function basis, describing what the method does. The how should be evident in the body itself.
He doesn't seem to often concede to being wrong.
However, if you study that image, you might come to the same insight that I (on my bike ride) came to; and that the english comments never helped me with.
Also Ousterhout's book on design is very easy to read and I guess I liked it because I mostly just nodded in approval while reading and there were very few things that made me stop.
When used correctly, strong static type checking make certain kinds of bugs impossible, spare you from writing many kinds of tedious tests that often get in the way of refactoring, serve as documentation, and make refactoring/maintenance an order of magnitude faster and safer. Even when a type checker isn't available, avoiding dynamic behaviour is very often the safer way to go so learning how to think in that way is still beneficial.
Most of these minor topics like how big a function should be, what to name your variables, or even if you write tests before/after coding... it's like trying to come up with general rules on how to write essays, creating graphic designs, or how to cook. "It depends" on the context and juggling different priorities each time. It's the kind of thing you only learn properly through practice (https://en.wikipedia.org/wiki/Tacit_knowledge), so there's only so much to gain in reading about it or discussing it after you've defined the no-brainer things to always do and always avoid.
If you mean the additions here https://web.stanford.edu/~ouster/cgi-bin/book.php, I read these and it still sounds like general rules of thumb you'll only really learn and understand by practicing a lot e.g. "In my experience, the sweet spot is to implement new modules in a somewhat general-purpose fashion" "Having good taste is an important part of being a good software designer".
Moreover, it is the students' inexperience which give this text credence --- since it results in their making errors and poor architectural/design choices it affords the chance of correction.
I think it is remarkable that the author switched from "modules should be specialized" to "modules should be generalized" (rough paraphrasing, mailed my copy to Brazil and waiting to buy a replacement).
If you know of other books which you merit recommendation and which have a similar or better context for their authorship and exposition, I would be glad to hear of them.
When considering coding advice, I just don't find credentials or the results of a coding experiment matter much, especially if it involves students. I run through the scenarios in my head and pick the option that makes sense given the context and competing priorities.
> I think it is remarkable that the author switched from "modules should be specialized" to "modules should be generalized" (rough paraphrasing, mailed my copy to Brazil and waiting to buy a replacement).
The link I posted has a PDF with the content I think. I guess I don't find the limited example involving a few function signatures compelling (a student assignment to write a basic text editor) or understand why I need to pick whether to trend towards modules being specialized or generalized... you treat it on a case-by-case basis, pick the one that has the most benefits given the context and be open to changing later when it makes sense? Outside of a few rules, it's boring but "it depends" really is the answer to most of these debates. Similar with how long functions should be, writing comments and how to name variables.
> If you know of other books which you merit recommendation and which have a similar or better context for their authorship and exposition, I would be glad to hear of them.
Software Engineering at Google is probably relevant for large projects: https://abseil.io/resources/swe-book
The rest probably best comes from practice, that's how I learned most of what I know here. I'm didn't for example become a fan of strong static types because of advice based on credentials or experiments, but from experience coding with and without types.
At a quick glance, that book seems to align well with APoSD --- what aspects do you believe run counter to it?
What book that covers a similar range of topics as APoSD would you suggest?
Since then, everyone and their brother is on the static typing train. And from that lens you're right. It seems like an omission. Give it another 10 years and people will probably think the opposite.
I do expect the pendulum to swing against type systems at some point soon for the same reasons it swung against OOP: Too much heavy lifting done by something that's hidden from the programmer, encouraging people to be "too clever," etc. Like OOP, algebraic types are a tool that have to be used well, and the current users are people who really like type systems and do use them well. It's only a matter of time before the tool gets into the hands of the average programmer, and then we will see how terribly a great type system can hurt you.
It is all as bad as you imagine. Functionality is spread out all of the place so it is very difficult to reason about how it all hangs together.
Eventually, your code has to do something. Get that thing in one place where you can look at it in its whole.
He did an example in the article of:
void concurrentOperation() { lock() criticalSection(); unlock() }
So if you subsitute criticalSection with a lot of operations, such as open file, read lines, find something, close file. I think you have a better representation of an over bloated function.
Scala has the langauge support to show what this could look like.
What you're doing in that method is starting a critical section, doing something, and then ending a critical section. It's a good suggestion to break that with:
def criticalSection(f: () => Unit) { lock() f() unlock() }
How you have a single method that does one thing and is easy to understand. Also it's reusable.
The original code would be used as:
criticalSection { _ => doSomething() }
That replacement is now longer dependent on locking. Locking is layered in.
Or worse, I've seen people break functions apart in such a way that you now need to maintain some sort of class-level state between the function calls in order to get the correct behaviour. This is almost impossible to meaningfully test because of the complex possible states and orders between those states - you might correctly test individual cases, but you'll never cover all possible behaviours with that sort of system.
I think that's actually fine. Particularly if you're doing a testing pyramid style approach where you do a lot of tests of some piece of low level logic and then a few tests of the higher level piece that makes use of that lower level logic, I don't see any problem with the higher level test covering a codepath that's also used in the lower level test. If anything I find it makes it easier to understand and debug failures - you know that the behaviour of the lower level piece hasn't changed because otherwise the lower level test would have failed, so the bug can only be in the higher level component itself.
When you have small expressions that have incremental results stored in variables, you can see the result in a debugger so you can see each stage.
Uncle bob doesn’t mention this.
createspecialString(y) =
Capitalizefirstletter .
MakealllowerCase .
AddNumberSuffix .
removeLetterA .
removeLetterB .
ConcatwithWord(x)
CapitalizeFirstLetter(a) = a[0].upper() + a[1:]
MakeAllLowercase(a) = map(a, (t) => t.lower())
Addnumbersuffix(a) a + 3.toString()
RemoveLetterA(t) = filter(t, (s) => s.lower() == “a”)
RemoveLetterB(t) = filter(t, (s) => s.lower() == “b”)
ConcatenateWithWord(x) = (y) => y + x
There see? It’s mostly doable in pure functional composition where the dot represents function composition. I program like this all the time. No way anyone can pull this off while mutating state and instantiating objects. F . P = (x) => F(P(x))
Forgive some inconsistent formatting and naming im typing this on my phone.People who complain about this style tend to be unfamiliar with it. If you had knowledge about procedural coding styles and a function composition approach like this then usually this style is easier as the high level function literally reads like English. You don’t need to even look at the definitions you already know what this complicated string formatting function does.
No comments needed. And neither author tells you about this super modular approach. They don’t mention the critical thing in that this style requires functions to be pure.
Thus to get most of your code following this extremely modular and readable approach… much of your code must be minimizing IO and state changes and segregating it away as much as possible.
The Haskell type system, the IO monad is pushing programmers in this direction.
Again neither author talks about this.
createSpecialString = createFormattedString . createNewString
createFormattedString =
Capitalizefirstletter .
MakealllowerCase .
AddNumberSuffix .
createNewString(y) =
removeLetterA .
removeLetterB .
ConcatwithWord(x)
Or put it all on one line. createspecialString(y) = Capitalizefirstletter . MakealllowerCase . AddNumberSuffix . removeLetterA . removeLetterB . ConcatwithWord(x)
The amount of lines becomes off topic once you get into this style. It's a completely orthoganol concept as it's completely irrelevant to readability and modularity.Lines doesn't makes sense for pure non imperative functions. Lines ONLY make sense for imperative functions because each line represents an instruction.
In the first example there is one layer. In the second there is 2 layers of abstraction formed by composing the primitives into a tree.
My own experience is that with an IDE that can collapse a new scope in the middle of a function, you can make large functions that accomplish a lot and are very clear by writing a comment and starting a new scope.
If something is going to be called multiple times a new function makes sense, but this idea that anything that can eventually return a single value needs to be it's own function is a giant pain that creates more problems than it solves.
Just makeing a new scope in the middle of the functions lets you use all the variables in the outer scope, do transformations without introducing new variables and ultimately "return" a new variable to the outer scope.
I've never understood why polluting namespaces with dozens or hundreds of names (most of which may not be very descriptive since naming a hundred small things is already going to be confusing) is seen as a good idea. You look at a list and you have no idea what is important and what was being shoved in there to satisfy some do nothing public speaker's arbitrary rules.
Too many arguments can become a problem. Nested functions can help here, because they allow you to move their implementation “out of the way” while still having access to shared variables. And sometimes a collection of parameters can sensibly become its own class.
IDE affordances are fine, but I’m opposed to requiring reliance on them for reading and understanding code, as opposed to writing.
What does independent mean? I would just collapse them all because they were meant to be collapsed in the first place.
you have to analyze the unfamiliar code first in order to know which sub-scopes you might want to collapse
Unfamiliar? I wasn't refactoring and collapsed them all.
The benefit of extracted functions is that you know that they are independent of the implementation details of the caller (i.e. can’t possibly depend on or modify local variables of the caller other the ones passed as arguments).
That's true to an extent, but this is more of a way to make monolithic functions simple, which then makes the program simpler over all because you can avoid lots of tiny little functions. What you can end up with is programs that do non trivial things but don't have tons of functions confusing the issue.
Pragmatically this isn't really a problem. The whole "it isn't exactly a 1:1 replacement" isn't the point. You can still put in comments and const references if you really want to.
IDE affordances are fine, but I’m opposed to requiring reliance on them for reading and understanding code, as opposed to writing.
Why would it be required? The alternative is that you still have these commented sections with their own scope but they aren't collapsed. You can always work on it without the IDE and when you go back to the IDE it still works.
The reality of it is that you can see a broad overview of a function then see details one section at a time and you don't even have to go skipping around to other parts of the file or other files to do it.
However, a friend of mine was a professional Smalltalk programmer. He claims that his median line count of methods, over his 17 year career, was 4.
It is harder to do in other languages--it seems that C would be on the order of 10.
Clearly it is a rule that can lead to complexity of too many methods, compromising whatever gain smaller methods give you.
Auto-generate getters and setters for every instance variable and that will drag the average down. (Maybe a lot of those getters and setters should not have existed.)
Name: MyClass
Superclass: Core.Object
Instance Variables: anInstVar anotherVar
Create methods:
Accessors
Initializer
Subclass responsibilities
"Accessors" aka getters. ~
See 1996 "Smalltalk with Style"page 113 get method
page 117 set method
https://rmod-files.lille.inria.fr/FreeBooks/WithStyle/Smallt...
~
"After the creation of the class StUser, it is highly recommended that you perform automatic creation of instance variable accessors. One possible way to do this in Squeak is to use the context menu on a class among the entry more.... There, you can find create inst var accessors. Select this command to have all accessors created."http://www.hpi.uni-potsdam.de/hirschfeld/seaside/tutorial?ch...
etc etc
I've never read CC, but I've read some of the take downs[1]. I was worried that the take downs were attacking a strawman, but no, Uncle Bob believes this stuff, including that comments are evil and you just need to read all the code and keep it in your head.
Even if that were true, the code I write is better for having written the comments, especially interface comments, because the writing helps my thinking. Moreover, it helps my code reviewers—without written interfaces. If all you have is the code and not a description of what the code is supposed to do, how can you know if it is correct? I think most code reviewers are verifying the code against what they infer the interface to be. It helps us both to just be explicit.
There is no objective measure of code cleanliness. So if "clean code" is your goal, then you have no meaningful criteria to evaluate alternatives. (Including those pitched by Bob Martin.)
It gets worse, though. There's a subconscious element that causes even more trouble. It's obviously a good thing to write "clean code", right? (Who's going to argue otherwise?) And to do otherwise would be a moral failing.
The foundation on which "Uncle Bob" tries to build is rotten from the get-go. But it's a perfect recipe for dogmatism.
When I encounter an Uncle Bob devotee, I'm nearly always disappointed with the sheer rigidity of their approach: everything must be reduced thus, into these kind of pieces, because it is Objectively Better software design, period. Sure, standard default approaches and best practices are important things to keep in mind, but the amount of dogma displayed by folks who like Martin is really shocking and concerning.
I worry that his approach allows a certain kind of programmer to focus on like ... aesthetic, dogmatic uniformity (and the associated unproductivity of making primarily aesthetically-motivated, dogmatic changes rather than enhancements, bugfixes, or things that other coders on a project agree improves maintainability) instead of increasing their skills and familiarity with their craft.
Maintainability/appropriate factoring are subjective qualities that depend a lot on the project, the other programmers on it, and the expectations around how software engineering is done in that environment.
Pretending that's not true--that a uniform "one clean code style to rule them all" is a viable approach--does everyone involved a disservice. Seasoned engineers trying to corral complexity, new engineers in search of direction and rigor, customers waiting for engineering to ship a feature, business stakeholders confused as to why three sprints have gone by with "refactor into smaller methods" being the only deliverable--everyone.
Maybe people like him are the reason why we have to do leet code tests (I don't believe he would be capable of solving even an easy problem).
Also, you do understand that people worked as software engineers even before Github became popular, or open sourcing to begin with, do you? So if someone is 60+ year old, chances are that most of his work has never been open sourced, and his work was targeting use cases, platforms, services which have no utility in this age any more.
Which have all nothing to do with how good a software engineer someone is.
And finally, do you have any proof that he never shipped any code into production?
Somewhat ageist? I'm 72 and have produced a number of FOSS tools.
John Ousterhout is 70 years old and one of the open source pioneers. We don't know what Uncle Bob shipped or did not ship but his friendly opponent in this discussion definitley did ship high profile projects.
This level of pointless obfuscation would not survive a code review at any sane dev team.
Rigidity and "religious" zeal in software development is just not helpful I'd agree.
I do however love consistency in a codebase, a point discussed in "Philosophy of Software Design", I always boil this down to, even if I'm doing something wrong, or suboptimal, if I do it consistently, once I realise, or it matters I only have one thing to change to get the benefit.
It's the not being able to change regardless, in the face of evidence, that separates consistency and rigidity (I hope)!
I'm also a fan of detailed doc comments on every module and function, and many fields/variants as well. And, anything that needs special note, is unintuitive, denotes units or a source etc.
I am also okay with 1000 line functions where appropriate. Making me jump around the code instead of reading one line at a time, in a straight line? No thanks!
Sure, dogmatism is never perfect, but when you have nothing, a dogmatic teacher can put you in a good place to start from. I admired that he stuck to his guns and proved that the rules he laid out in clean code worked to make code more readable in lots of situations.
I don't know anything about him as a person. I never read his other books, but I got a lot out of that book. You can get a lot out of something without becoming a devotee to it.
EDIT: I think even UB will agree with me that his dogmatism was meant as an attitude, something strong to hit back against a strong lack of rigidity or care about readable code, vs a literal prescription that must be followed. See his comment here:
> Back in 2008 my concern was breaking the habit of the very large functions that were common in those early days of the web. I have been more balanced in the 2d ed.
And maybe I was lucky, but my coding life lined up pretty neatly with the time I read Clean Code. It was an aha moment for me and many others. For people who had already read about writing readable code, I'm sure this book didn't do much for them.
That looks more like a communication style difference than anything else. Uncle Bob's talks and writing are prescriptive -- which is a style literally beaten into me back when I was in grade school, since it's implied just from the fact that it's you doing the speaking that you're only describing your opinions and that any additional hedging language weakens your position further than you actually intend.
If you listen to him in interviews and other contexts where he's explicitly asked about dogmatism as a whole or on this or that concept, he's very open to pragmatism and rarely needs much convincing in the face of even halfway decent examples.
> animus toward comments
Speaking as someone happy to drop mini-novels into the tricky parts of my code, I'll pick on this animus as directionally correct advice (so long as the engineer employing that advice is open to pragmatism).
For a recent $WORK example, I was writing some parsing code and had a `populate` method to generate an object/struct/POCO/POJO/dataclass/whatever-it-is-in-your-language, and as it grew in length I started writing some comments describing the sections, which for simplicity's sake we'll just say were "populate at just this level" and "recurse."
If you take that animus toward comments literally, you'll simply look at those comments and say they have to be removed. I try to be pragmatic, and I took it as an opportunity to check if there was some way to make the code more self-evident. As luck would have it, simply breaking that initial section into a `populate_no_recurse` method created exactly the documentation I was looking for and also wound up being helpful as a meaningful name for an action I actually wanted to perform in a few places.
That particular pattern (breaking a long method into a sequence of named intermediate parts) has failure modes, especially in the hot path in poorly optimized runtimes (C#, Java, ..., Python, ...), and definitely in future readability if employed indiscriminately, but I have more than enough experience to be confident it was a good choice here. The presence in my mind of some of Uncle Bob's directionally correct advice coloured how I thought about my partial solution and made it better.
> other animus
- Stylistic refactors that induce performance regressions can be worth it. As humans, we're pre-disposed to risk avoidance, so let's look at an opposite action with an opposite effect: How often are you willing to slow down feature velocity AND make the code harder to maintain just to squeeze out some performance (for a concrete example, suppose there's some operation with space/time/bandwidth tradeoffs which imply you should have a nasty recursive cte in your database to compute something like popcount on billion-bit-masks, or even better just rewrite that portion of the storage layer)? My job is 80% making shit faster and 10% teaching other people how to make shit faster, but there are only so many hours in the day. I absolutely still trade performance for code velocity and stability from time to time, and for all of those fledgeling startups with <1M QPS they should probably be making that trade more than I do (assuming it's an actual trade and not just an excuse for deploying garbage to prod).
- The "tortured method names" problem is the one I'm most on the fence about. Certainly you shouldn't torture a long name out of the ether if it doesn't fit well enough to actually give you the benefits of long names (knowing what it does from its name, searchability), but what about long names which do fit? For large enough codebases I think long names are still worth the other costs. It's invaluable to be able to go from some buggy HTML on some specific Android device straight to the one line in a billion creating the bug, especially after a couple hiring/firing sessions and not having anybody left who knows exactly how that subsystem works. I think that cutover point is pretty high though. In the 100k-1M lines range there just aren't enough similar concepts for searchability to benefit much from truly unique names, so the only real benefit is knowing what a thing does just from its name. The cost for long names is in information density, and when it's clear from context (and probably a comment or three) I'm fine writing a numeric routine with single-letter variable names, since to do otherwise would risk masking the real logic and preventing the pattern-recognition part of your brain from being able to help with matters. HOWEVER, names which properly tell you what a thing does are still helpful (the difference between calling `.resetRetainingCapacity()` and `.reset()` -- the latter you still have to check the source to see if it's the method you want, slowing down development if you're not intimately familiar with that data structure). I still handle this piece of advice on a case-by-case basis, and I won't necessarily agree with my past self from yesterday.
> "Uncle Bob devotees" vs "Uncle Bob"
This is maybe the core of your complaint? I _have_ met a lot of people who like his advice and aren't very pragmatic with it. Most IME are early-career and just trying to figure out how to go from "I can code" to "I can code well," and can therefore be coached if you have well-reasoned counter-examples. Most of the rest IME like Uncle Bob's advice but don't code much, and so their opinions are about as valuable as any other uninformed opinion, and I'm not sure I'd waste too much time lamenting that misinformation. For the rest of the rest? I don't have a large enough sample I've interacted with to be very helpful, but unrelenting dogmatism is pretty bad, and people like that certainly exist.
I think a lot of engineers hear "there's a time and a place" or "in context" and assume that I'm saying that the approach to coding can or should differ between every contribution to a codebase. Not so! It's very important to have default approaches to things like comments, method length, coupling, naming, etc. The default approach that makes the most sense is, however, bounded by context, not Famous Author's One True Gospel Truth (or, in many cases, Change-Averse Senior Project Architect's One True Gospel Truth). The "context boundary" for a set of conventions/best practices is usually a codebase/team. Sometimes it's a sub-area within a codebase. More rarely, it's a type of code being worked on (e.g. payment processing code merits a different approach from kleenex/one-off scripts). Within those context boundaries, it's absolutely appropriate to question when contributors deviate from an agreed-upon set of best practices--they just might not be Martin's best practices.
Rather, the core of my critique is that Martin's approach lacks perspective. Perspective/pragmatism--not some abstract notion of "skill level in creating well-factored code according to a set of rules"--is the scarce commodity among the intermediate-seeking-senior engineers that Martin's work is primarily marketed toward and valued by.
From there, I see two things wrong with Martin's stance in the Osterhout transcript:
"Out of touch" was not an arbitrarily chosen ad-hominem. When Osterhout pressed Martin to improve and work on some code, Martin's output and his defense of it were really low-quality. I can tell they're really low quality because, in spite of differing specific opinions on things like method length/naming/SRP, almost everyone here and to whom I've showed that transcript finds something seriously wrong with Martin's version, while the most stringent critique of Osterhout's code I've seen mustered is "eh, it's fine, could be better". That, and Martin's statements around the "why" of his refactors, indicate that the applicability of his advice for material code quality improvements in 2025 (as opposed to, say, un-spaghettification of 2005 PHP 5000-line god-object monstrosities) is in doubt. On its own, that in-applicability wouldn't be a massive problem, which brings me to...
Second, Martin is a teacher. When you mention '"Uncle Bob devotees" vs "Uncle Bob"' and I talk about the rigidity I see in evidence among people that like Martin, I'm talking about him as a teacher. This isn't a Torvalds or Antirez or Fabrice Bellard-type legendary contributor discussing methodological approaches that worked for them to make important software. Martin is first and foremost (and perhaps solely) a teacher: that's how he markets himself and what people value him for. And that's OK! Teachers do not have to be contributors/builders to be great teachers. However, it does mean that we get to evaluate Martin based on the quality of his pedagogical approach rather than holding the ideas he teaches on their own merit alone. Put another way, teachers say half-right things all the time as a means of saving students from things they're not ready for, and we don't excoriate them for that--not so long as the goal of preparing the students to understand the material in general (even if some introductory shortcuts need to later be uninstalled) is upheld.
I think Martin has a really poor showing as a teacher. The people his work resonates the most strongly with are the people who take it to the most rigid, unhealthy extremes. His instructorial tone is absolute, interspersed with a few "...but only do this pragmatically of course" interjections that he himself doesn't really seem to believe. His material is often considered, in high-performing engineering departments, to be something that leaders have to check back against being taken too far rather than something they're happy to have juniors studying. Those things speak to failures as a teacher.
Sure, software engineers are often binary thinkers prone to taking things to extremes--which means that a widely regarded teacher of that crowd is obligated to take those tendencies into account. Martin does not do this well: he proposes dated and inappropriate-in-many-cases practices, while modeling a stubborn, absolutist tone in his instruction and responses to criticism. Even if I were to give his specific technical proposals the greatest possible benefit of the doubt, this is still bad pedagogy.
It was always like that. And Fowler the same thing with his criticism of anemic domain model. But software-engineering is no exceptions to having a mass of people believing someone without thinking by themselves.
What leads you to disagree with the fact that anemic domain models are an anti-pattern?
https://martinfowler.com/bliki/AnemicDomainModel.html
I think it's obvious that his critique makes sense if you actually take a moment to try learn and understand what he says and where he comes from. Take a moment to understand what case he makes: it's not object-oriented programming. That's it.
See,in a anemic domain model, instead of objects you have DTOs that are fed into functions. That violates basic tenners of OO programming. It's either straight up procedural programming or, if you squint hard enough, functional programming. If you focus on OO as a goal, it's clearly an anti-pattern.
His main argument is summarized in the following sentence:
> In essence the problem with anemic domain models is that they incur all of the costs of a domain model, without yielding any of the benefits.
Do you actually argue against it?
Listen, people like Fowler and Uncle Bob advocate for specific styles. This means they have to adopt a rethoric style which focuses on stressing the virtues of a style and underlining the problems solved by the style and created by not following the style. That's perfectly fine. It's also fine if you don't follow something with a religious fervor. If you have a different taste, does it mean anyone who disagrees with you is wrong?
What's not cool is criticizing someone out of ignorance and laziness, and talking down on someone or something just because you feel that's how your personal taste is valued.
Why would focusing on OO be a goal? The goal is to write good software that can be easily maintained. Nobody outside of book writers are shipping UML charts
Often people do this to deliver higher quality software. Most languages still have some OO features, and people don't use them because they know they lead to bad code. Inheritence (a core OO feature) comes to mind. Most professionals nowadays agree that it should not be used.
OO designs are often over-abstracted which makes them hard to understand and hard to change. They lack "locality of behavior". Trivial algorithms look complicated because parts of them are strewn across several classes. This is why more modern langues tend to move away from OOP.
My guess is that im the long term, what we will keep from OO is the possibility to associate methods with structs.
That's circular logic. I wouldn't focus on writing OO code because I know from experience that the result is usually worse. If I had to use a language that was oriented towards writing OO code, I'd still try to limit the damage.
> There are programming paradigms for a reason
Nah. A lot of them are just accidents of history.
It should be the best solution to the problem direct whether or not use of OO is best, not the language.
I'm paid for efficiently solving business problems with software, not using a particular paradigm. If an FP solution is more appropriate and the team can support it, then that's what I'll use.
See, this is the sort of lazy ignorance that adds nothing of value to the discussion, and just reads as spiteful adhominems.
Domain models are fundamentally an object-oriented programming concept. You model the business domain with classes, meaning you specify in them the behavior that reflects your business domain. Your Order class has a collection of Product items, but you can update an order, cancel a order, repeat an order, etc. This behavior should be member functions. In Domain-Driven design, with its basis on OO, you implement these operations at the class level, because your classes model the business domain and implement business rules.
The argument being made against anemic domain models is that a domain model without behavior fails to meet the most basic requirements of a domain model. Your domain model is just DTOs that you pass around as if the were value types, and have no behavior at all. Does it make sense to have objects without behavior? No, not in OO and elsewhere as well. Why? Because a domain model without behavior means you are wasting all development effort building up a structure that does nothing and adds none of the benefits, and thus represents wasted effort. You are better off just doing something entirely different which is certainly not Domain-Driven design.
In fact, the whole problem with the blend of argument you are making is that you are trying to push a buzzword onto something that resembles none of it. It's like you want the benefit of playing buzzword bingo without even bothering to learn the absolute basics of it, or anything at all. You don't know what you're doing, and somehow you're calling it Domain-Driven design.
> Why would focusing on OO be a goal?
You are adopting a OO concept, which the most basic traits is that it models business domains with objects. Do you understand the absurdity of this sort of argument?
They are absolutely not. In fact, they are not even specific to even just programming, let alone OOP.
They are not.
> You model the business domain with classes, meaning you specify in them the behavior that reflects your business domain.
I have better tools for doing that.
> In Domain-Driven design, with its basis on OO, you implement these operations at the class level, because your classes model the business domain and implement business rules.
You're still not explaining the "why". You're just repeating a bunch of dogma.
> a domain model without behavior means you are wasting all development effort building up a structure that does nothing and adds none of the benefits, and thus represents wasted effort.
I know from experience that this is completely false.
> You don't know what you're doing, and somehow you're calling it Domain-Driven design.
I don't call it domain-driven. You can call it domain-driven if you want, or not if you don't want. I don't care what it's called, I care whether it results in effective, maintainable software with low defect rates.
This is what it is about. All the other things that have been invented need to be in service of this goal.
For example?
This is how to fuck up OO and give it a bad name:
order.update(..) // Now your Order knows about the database.
order.cancel(..) // Now your Order can Email the Customer about a cancellation.
order.repeat(..) // Now your Order knows about the Scheduler.
What else could Order know about? Maybe give it a JSON renderer .toJson(), a pricing mechanism .getCost(), discounting rules .applyDiscount(), and access to customer bank accounts for .directDebit(); Logging and backup too. And if a class has 10+ behaviours you've probably forgotten 5 more.An Order is a piece of paper that arrived in your mailbox. You can't take a sharpie to it, you can't tell it to march itself into the filing cabinet. It's a piece of paper which you.read() so that you.pack() something into a box and take it to the post office. You have behaviours and the post office has behaviours. The Order and the Box do not. At best they have a few getters() or some mostly-static methods for returning aggregate data - but even then I'd probably steer clear. For instance: if the Order gave me a nice totalPrice() method, it simplifies things for later right? Well no, because in TaxCalculator (not order.calculateTax()) I will want to drill down into the details, not the aggregate. Likewise for DiscountApplier.
> Does it make sense to have objects without behavior? No, not in OO and elsewhere as well.
It does, just like in the Domain (real-world Orders). Incidentally, I believe objects-without-behaviours is one of the core Clojure tenets.
Since this is HN's monthly UB-bashing thread, I should point out that I learnt most of this stuff from him. (It's more from SOLID though, I don't think I have much to say on about cleanliness.)
The above examples violate SRP and DI.
"Single reason to change": If order.cancel(..) knows about email, then this is code I have to change if the cancellation rules change or if the email system changes. What if we don't notify over email anymore? Order has to become aware of SMS or some other tech which will cause more reasons for change.
"Dependency inversion": People know what Orders are, regardless of technical competence. They can exist without computers or any particular implementation. They are therefore (relative to other concerns here) high-level and abstract. Orders are processed using a database, Kafka and/or a bunch of other technologies (or implementation details). DI states that abstract things should not depend on concrete things.
- Cat is the subject noun
- Eats is the verb
- Rat is the object noun
In object-oriented programming, the subject is most often the programmer, the program, the computer, the user agent, or the user. The object is... the object. The verb is the method.
So, imagine the sentence "the customer canceled the order."
- Customer is the subject noun
- Canceled is the verb
- Order is the object noun
In OOP style you do not express this as customer.cancel(order) even though that reads aloud left-to-right similarly to English. Instead, you orient the expression around the object. The order is the object noun, and is what is being canceled. Thus, order.cancel(). The subject noun is left implicit, because it is redundant. Nearly every subject noun in a given method (or even system) will be the same programmer, program, computer, user agent, or user.
For additional perspectives, I recommend reading Part I of "Object-Oriented Analysis and Design with Applications" (3rd edition) by Grady Booch et. al, and "Object Thinking" by David West.
---
That said, I think you're right about the single responsibility principle in this example. A class with too many behaviors should usually be decomposed into multiple classes, with the responsibilities distributed appropriately. However, the object should not be left behavior-less. It must still be an anthropomorphized object that encapsulates whatever data it owns with behavior.
> - Customer is the subject noun
And this is wrong. Because the customer did not cancel the order. The customer actually asked for the order to be canceled. And the order was then canceled by "the system". Whatever that system is.
And that is the reason why it is not expressed as customer.cancel(order) but rather system.cancel(order, reason = "customer asked for it").
> Thus, order.cancel(). The subject noun is left implicit, because it is redundant.
Ah, is that so? Then, I would like you to tell me: what happens if there are two systems (e.g. a legacy system and a new system, or even more systems) and the order needs to be sometimes cancelled in both, or just one of those systems? How does that work now in your world?
This is why many object-oriented programmers prefer to talk about message passing instead of method calling. It is indeed about asking for the order to be canceled, and the order can decide whether to fulfill that request.
In my world of thinking, orders don't make decisions. If I go to the business team and say "the order decided to" they'll look at me funny. And for good reasons.
I highly recommend reading the two books I recommended for further perspective on the topic. OO is predicated not on the idea that data is a bag of dead bits on which operations are performed, but that data is embodied within and encapsulated by anthropomorphic objects with their own behavior.
It is possible to get to that world of thinking from where you are now. But it is a different world. A different way of thinking.
Passive language like "the order couldn't be" might be fine in some real world situations where I don't care about who caused the action. But in code I do care. Because somewhere in code the action has to be made. And yeah, you can put that logic into the Order entity, but then we are back to square one where "the order made the decision".
If we are talking about some event that happend, then sure, "the order was canceled" is perfectly fine. So making an "OrderWasCancelled" (or "OrderWasNotCancelled") object and storing it somewhere is intuitive. But we were talking about the action happening and that is a different thing.
Also, just to make that clear, I'm not talking just theoretically here. I started my career during the OOP hype time. I actually read books like head first design patterns and others about OOP. But ultimately, I found it's not productive at all, because it doesn't reflect how most people think - at least from my experience.
Therefore, I tend to write my code in the same way that non-technical people think. And it turns out, OOP is very far from that.
From "Object-Oriented Analysis and Design with Applications" (3rd edition) by Grady Booch:
p.52: Separation of Concerns
We do not make it a responsibility of the Heater abstraction to maintain a fixed temperature. Instead, we choose to give this responsibility to another object (e.g., the Heater Controller), which must collaborate with a temperature sensor and a heater to achieve this higher-level behavior. We call this behavior higher-level because it builds on the primitive semantics of temperature sensors and heaters and adds some new semantics, namely, hysteresis, which prevents the heater from being turned on and off too rapidly when the temperature is near boundary conditions. By deciding on this separation of responsibilities, we make each individual abstraction more cohesive.If the system is new and there's only one way to do it, it's not worth sweating over it. But if a new requirement comes up it makes sense to choose a way to handle that.
For example, an order may be entered by a salesperson or maybe by a customer on the web. The cancellation process (a strategy, perhaps) might be different. Different users might have different permissions to cancel one order or another. The designer of the website probably shouldn't have to code all that in, maybe they just should have a cancel function for the order and let the business logic handle it. Each order object could be configured with the correct strategy.
If you don't want to use OO, that's fine, but you still have to handle these situations. In what module do you put the function the web designer calls? And how do you choose the right process? These patterns will perhaps have other names in other paradigms but the model is effectively the same. The difference is where you stuff the complexity.
Exactly. If it were so simple, why not just put everything in one big file / class? I guess we both agree that this very quickly leads to an unmaintainable mess.
So my rule of thumb is: can a feature theoretically be removed without touching the Order entity at all? If so, then NONE of the features parts can live in the Order entity (or even be referred by it).
That means: the Order entity must know nothing about customers, sales, how it stored or cached, how prices and taxes are calculated, how an order is cancelled or repeated or orders can be archived and viewed.
Because any of those features can be removed while the others keep working and using the exact same Order entity.
> The order is the object noun, and is what is being canceled. Thus, order.cancel()
You've just restated the position I argued against, without an argument.
> The argument being made against anemic domain models is that a domain model without behavior fails to meet the most basic requirements of a domain model. Your domain model is just DTOs that you pass around as if the were value types, and have no behavior at all. Does it make sense to have objects without behavior? No, not in OO and elsewhere as well. Why? Because a domain model without behavior means you are wasting all development effort building up a structure that does nothing and adds none of the benefits, and thus represents wasted effort. You are better off just doing something entirely different which is certainly not Domain-Driven design.
I have barely any idea what you're saying, but I will agree that I'm probably better off without DDD.
> You are adopting a OO concept, which the most basic traits is that it models business domains with objects. Do you understand the absurdity of this sort of argument?
Except I'm not, because I don't care about DDD? My argument is simply: caring how much your code adheres to some third party methodology doesn't matter, what matters is if you're writing good code or not.
Yes, exactly. And this "classical" object-oriented programming is an anti-pattern itself.
(That being said, OOP is not well defined. And, for example, I have nothing against putting related data structures and functionality into the same namespace. But that's not what OOP means to him here)
You used the example of an "order" yourself, so I'll built upon it.
I would never combine functionality to update an order with the data and structure of the an order. The reason is simple: the business constraints don't always live inside the order.
Here's an example why such an approach inevitably must fail: if the business says that orders can only be made until 10000 items have been ordered in a month, then you cannot model that constraint inside of the order class. You must move it outside - to the entity that knows about all the orders in the system. That would be the OrderRepository or however you want to call it.
Remember, here is what you said in your other post:
> Your Order class has a collection of Product items, but you can update an order, cancel a order, repeat an order, etc. This behavior should be member functions.
So your Order should have a repeat function? But how can the order know if it can be repeated? It might violate the max-monthly-items constraint. The only way for the Order to do it is to hold a reference to the OrderRepository.
And this is a big problem. You have now entangled the concept of an OrderRepository and of an Order. In fact, Orders could totally live without an OrderRepository alltogether, for example when you build an OrderSimulation where no orders are actually being executed/persisted. But to do so, now you have this OrderRepository, even if you don't need it.
The rule of the thumb is: if the business says "we don't need feature A anymore, remove it" then you should be able to remove that feature from the code without touching any unrelated feature. If you now remove the OrderRepository and cause a bug in the Order class due to your code changes, the business will probably wonder how that could be, because while the OrderRepository cannot exist without Orders, Orders can exist without an OrderRepository.
And if that seems a bit unrealistic, think of users: A user can easily exist without a UserRepository, but not the other way around.
That makes clear, that you the rich domain model is an unsuitable and generally suboptimal solution to modeling the domain of a business. The anemic domain model on the other hand matches it perfectly.
And one more thing: even natural language disagrees with the rich domain model. Does an order repeat itself? No! An order is repeated and that is, it is repeated by something or someone. This alone makes clear that there is an entity beyond the Order that is responsible for such action. And again, the anemic domain model is a great solution for expressing this in code.
But if you disagree, I'd like you to explain what you believe the disadvantages of the anemic domain model are.
By searching for that term, you'll easily find lots of other takes on the matter.
I can search for it of course but results that aren't about OOP purism appear to be rare.
The slide at 52:14 is on the SOLID principles, the first one is on SRP which gives pretty understandable advice about whether Order should have behaviours.
In fact I find this type of accidental / unneeded coupling is the number one cause of problems, bugs and limitations of re-use and thus development velocity in any software product. Concepts where a single way dependency is turned into a cycling dependency are really hard to evolve, maintain, test and understand.
In fact I'd go as far as to say that as a general rule of thumb if you have a situation where your class A depends on class B that depends on class A you've made a big doo doo and you should really seriously re-consider your design.
(Adjacent to this rule is that classes that exist in the same level of of the software hierarchy and are thus siblings should also not know about each other).
In fact when you structure your code so that the dependencies only go one way you end up with a neat lasagna code base and everything can easily slotted in. (Combined with this a secondary feature which is to eliminate all jumps upwards in the stack, i.e. callbacks)
> Here's an example why such an approach inevitably must fail: if the business says that orders can only be made until 10000 items have been ordered in a month, then you cannot model that constraint inside of the order class. You must move it outside - to the entity that knows about all the orders in the system. That would be the OrderRepository or however you want to call it.
It's not that hard.
If the constraint of your example is a domain constraint, and so it's *always* valid, then when you hydrate an Order entity, other than the Order data itself, you also need to provide the total number of orders.
```
// orders is the repository, and it hydrates the entity with the total number of orders for this month
order = orders.new()
// Apply the check
order.canBeCreated()
```
Where the `canBeCreated` method is as simple as:
```
if this.ordersInAMonth > TOTAL_NUMBER_OF_ORDER_IN_A_MONTH ...
```
Fixed.
It's the same as using the OrdersRepository to query the number of orders directly before creating one, but here, the logic is just in the class.
Now, your example is pretty stupid, so I know it must not be taken literally but...
PS: I'm all but not a DDD advocate.
And second, because my original critics still holds: you now have some kind of order-entity-unrelated information inside the order (or inside its `canBeCreated()`). This will force you to touch the order-entity when removing a business constrain that is unrelated to the (single) order-entity. Because otherwise, where does "ordersInAMonth" come from? It must be able to talk to the database or something.
> Now, your example is pretty stupid, so I know it must not be taken literally but...
No no, you absolutely can take it literal. It might not be very realistic, but that doesn't change the fact that we can use it to discuss pros and cons of different designs.
> It's the same as using the OrdersRepository to query the number of orders directly before creating one, but here, the logic is just in the class.
As with my two issues that I mentioned above, the problem is the "just" in your sentence. It appears that your assessment is that the code living in a different place is merely a problem of the code being in a different place with no effect on productivity. But to me, having the code in a "wrong" place becomes a really big problem over time, especially in a big code base.
Also, we can extend this example. Let's say we have two or more entities. Like orders, users and stores and there are constraints that span and impact the state and/or creation of all of them at the same time.
Now let's compare the different approaches of us. In my case, it's rather easy: there must be some "system" or "service" the lives above all the entities that are constrained by a business rule. So if there is a business rule that touches entities A, B and C, then there must be some "system" or "service" that knows about all A, B and C and can control each of them. In other words, there cannot be a "system" or "service" that controls just A anymore. The logic to ensure the constraint then lives in that service.
With your approach, how and where do you put the code for that constraint?
And let's, just for the sake of the argument, assume that you cannot push the constraint into the database. Because that basically would be such an uber-service as described by me above. In reality, we might employ the database to (also) enforce constraints. But for the sake of the discussion, let's say we use a database where we cannot.
Looking forward to your response!
What strikes me about the advice in Clean Code is that the ideas are, at best, generally unproven (IE just Martin's opinion), and at worst, justify bad habits. Saying "I don't need to comment my code, my code speaks for itself" is alluring, but rarely true (and the best function names can't tell you WHY a function/module is the way it is.) Chopping up functions and moving things around looks and feels like work, except nothing gets done, and frankly often strikes me as being the coding equivalent of fidget spinners (although at least fidget spinners dont screw up your history). Whenever Martin is challenged on these things he just says to use "good judgement", but the code and advice is supposed to demonstrate good judgement and mostly it does not.
Personally I wish people would just forget about Clean Code. You're better off avoiding it or treating it as an example of things not to do.
Then for the rest of the talk he would speak just as confidently about coding. Why would I believe anything he has to say when his confidence is clearly not correlated to how well he understand the material?
From what I can deduce, his major coding work was long in the past, and maybe in C++.
I hired a friend who was a huge Uncle Bob mark, and he kept trying to flex his knowledge during interviews with other people in the company. I didn't really think much of it and told the other interviewer that it was just his personal quirk and not to worry much.
I had him work with some junior devs on a project while I took care of something more urgent. After finishing it, I went over to take a look at how it was going on his end. I was horrified at the unnecessary use of indirection; 4 or 5 levels in order to do something simple (like a database call). Worse, he had juniors build entire classes as an interface with a database class that was "wrong".
No practical work was done, and I've spent the past 4 weeks building the real project, while tossing out the unnecessary junk.
I liked Clean Code when I read it, but I always assumed a lot of it was meant for a specific language at a specific time. If you are using it verbatim for a Python project in 2025, why?
I think you're talking out of ignorance. Let's take a moment to actually think about the arguments that Uncle Bob makes in his Clean Code book.
He argues in favor of optimizing your code for clarity and readability. The main goal of code is to help a programmer understand it and modify it easily and efficiently. What machines do with it is of lower priority. Why? Because a programmer's time is far more expensive than any infrastructure cost.
How do you make code clear and easy to read? Uncle Bob offers his advise. Have method names that tell you what they do, so that programmers can easily reason about the code without having to even check what the function does. Extract low-level code to higher level methods so that a function call describes what it does at the same level of detail. Comments is a self-admission you failed to write readable code, and you can fix your failure by refactoring code into self-descriptive member functions.
Overall, it's an optimization problem where the single objective is defined as readability. Consequently, it's obvious that performance regressions are acceptable.
Do you actually have any complain about it? If you read what you wrote, you'll notice you say nothing specific or concrete: you only throw blanket ad hominems that sound very spiteful, but are void of any substance.
What's the point of that?
> Maintainability/appropriate factoring are subjective qualities that depend a lot on the project, the other programmers on it, and the expectations around how software engineering is done in that environment.
The problem with your blend of arguments is that guy's like you are very keen on whining and criticizing others for the opinions they express, but when lightly pressed on the subject you show that you actually have nothing to offer in the way of alternative or guideline or anything at all. Your argument boils down to "you guys have a style which you follow consistently, but I think I have a style as well and somehow I believe my taste, which I can't even specify, should prevail". It's fine tha you have opinions, but why are you criticizing others for having them?
This may be true for some cases, but I don't see a non-contrived way for code to describe why it was written in the way it does or why the feature is implemented the way it is. If all comments are bad, then this kind of documentation needs to be written somewhere else, where it will be disconnected from the implementation and most probably forgotten
I have to call bullshit on your argument. Either you aren't even looking because you have the misfortune of only looking at bad code written by incompetent developers, or you do not even know what it looks like to be able to tell.
The core principles are quite simple, and are pervasive. Take for example replacing comments with self-descriptive names. Isn't this something obvious? I mean, a member function called foobinator needs a combination of comments and drilling down to the definition to be able to get a clue on what it does. Do you need a comment to tell what a member function called postOrderMessageToEventBroker does?
Another very basic example: predicates. Is it hard to understand what a isMessageAnOrderRequest(message) does? What about a message.type == "command" && type.ToUpperCase() == "request" && message.class == RequestClass.Order ? Which one is cleaner and easier to read? You claim these examples are contrived, but in some domains they are more than idiomatic. Take user-defined type assertions. TypeScript even has specialized language constructs to implement them in the form of user-defined type guards. And yet you claim these examples are contrived?
I'm starting to believe all these vocal critics who criticize Uncle Bob or Eric Evans or any other author are actually talking out of sheer ignorance about things they know nothing about. They read some comment in some blog and suddenly they think they are an authority on a subject they know nothing about.
So much noise.
Which event broker? Will I get a response via some callback? What happens if the sending fails, is there a retry mechanism? If yes, how many retries? What happens when the retry count is exceeded? Will the order‘s ID be set by this method? Is the method thread-safe? Which errors can be thrown? „Don‘t call this before the event broker has warmed up / connected“. Etc etc
Obviously not and the comment you're replying to hasn't asserted otherwise. They say, clearly, that comments should explain the _why_, postOrderMessageToEventBroker explains only the _what_ (which is reflected in the verbiage of your question). Fortunately comments are practically free and we're not limited to doing the reader one-favor-per-statement, we can explain _both_ the why with a comment (when it's not obvious) and the what with clear function names.
Instead, a comment can clearly and succintly tell me why this implementation is seemingly more complex than it needs to be, link to relevant documentations or issues etc.
I've been programming for 60 years in many different fields. I was programming long before he was. It is possible that I have written more code than he has in more languages (36 at last count), so my criticism of his are based on real-world experience.
This mentality sounds like a recipe for building leaky abstractions over the inherent traits of the von Neumann architecture, and, more recently, massive CPU parallelism. Bringing with it data races, deadlocks, and poor performance. A symptom of this mentality is also that modern software isn‘t really faster than it should be, considering the incredible performance gains in hardware.
> Comments is a self-admission you failed to write readable code
I‘m not buying this. It’s mostly just not possible to compress the behavior and contract of a function into its name. If it were, then the compiler would auto-generate code out of method names. You can use conventions and trigger words to codify behavior (eg bubbleSort, makeSHA256), but that only works for well-known concepts. At module boundaries, I‘m not interested in the module‘s inner workings, but in its contract. And any sufficiently complex module has a contract that is so complex that comments are absolutely required.
No,not really. Just because you think about how to name functions and what portions of your code should be easier to read if the were extracted to a function,that doesn't mean you are creating abstractions or creating problems.
The rest of your comments on von Neumann architecture etc is pure nonsense. Just because your code is easy to read it doesn't mean you're writing poetry that bears no resemblance with how the code is executed. Think about what you're saying: what is the point of making readable code? Is it to look nice at the expense of bugs, or to help the developer understand what the code does? If it's the latter, what point do you think you're making?
Every extra function call and object instantiation has a real cost, and abstracting ourselves away from the bare metal means we need to pay the price in terms of performance. Some very nicely readable algorithms are just sub-par in all dimensions except readability. We should optimize for performance and correctness, and readability comes second.
The one specific response I have is: it's not that I
> say nothing specific or concrete: [I] only throw blanket ad hominems that sound very spiteful
...rather, it's that I'm criticizing Martin's approach to teaching rather than his approach to programming. I expand on that criticism more in an adjacent comment, here: https://news.ycombinator.com/item?id=43171470
This assumes that code runs on corporate infrastructure. What if it runs on an end user device? As a user I certainly care about my phone's battery life. And we aren't even talking about environmental concerns. Finally, there are quite a few applications where speed actually matters.
> Comments is a self-admission you failed to write readable code, and you can fix your failure by refactoring code into self-descriptive member functions.
Self-explaining code is a noble goal, but in practice you will always have at least some code that needs additional comments, except for the most trivial applications. The world is not binary.
# This is not the way I wanted to do this, but due to bug #12345 in dependency [URL to github ticket] we're forced to work around that.
# TODO FIXME when above is done.
Oh no, I so failed at making self-descriptive code. I'm sorry, I totally should've named the method DoThisAndTHatButAlsoIncludeAnUglyHackBecauseSomeDubfuckUpstreamShippedWithABug.
Funny, I find the opposite. In my experience people that are willing to take a "dogmatic" position on code style are those who are able to actually get on with implementing features and bugfixes. It's the ones who think there's a time and place for everything and you need to re-litigate the same debates on every PR who tie themselves in knots getting nothing done.
Do I agree with absolutely everything Martin writes? In principle, no. But I'd far rather work on a codebase and team that agrees to follow his standards (or any similar set of equally rigid standards, as long as they weren't insane) than one that doesn't.
My experience has taught me that you never want to be the first person to recommend a rewrite. Since I am a mere contractor on this one, I am strongly inclined to let it unwind on its own. There seems to be a lot of ego embedded in those pointless data access layer wrappers. I'd hate to get on someone's bad side right now. The market is quite rarified.
when i found a copy of clean code in a bookstore, it only took me a few minutes to put it back. I had read John Ousterhout's book prior.
Oh and TDD?! Ah yes those perfectly defined unit cases you write for implementation details. The best comment I read recently (sorry I can’t find it) something akin to “The first unit I write is to validate the intended side effects through properly exercising associated mocks”
As with everything there is no “best way” to do something, but in software engineering… there are far more bad “best ways” than best “best ways”
By modelling the business you raise the business logic up to a 1st class element.
By implementing the business objects you encapsulate their functionality in the business.
The words "Account" or "Outstanding Balance" have business meanings. Modelling them allows you to express the business logic explicitly.
It also allows you to create tests that are related to that business logic, not the implementation.
You can still "build what you need and deal with the consequences of inevitable change later".
Model what you need to build, the business is going to have to make changes to that model to implement their changes, IT systems are a detail.
Change by extending and changing the DDD models.
To reverse the question, how do you write code that "does what you need" without understanding the domain?
When I first read the book I'd already been programming for 10 years, but I was in my first job out of college. I'd heard a lot about the book and so I trusted what it had to say. I let it override how I would have written code because I figured coding professionally was just far different than what I would consider the best way to write code.
Interestingly, 5 years sounds about right for how long it took me to finally start trusting my own judgement. I think it was a combination of being more confident in myself but also because I was doing larger projects and it was more frequent that I was putting down a project and then coming back a couple months later. That's how I was able to see how bad the code was to work with once my mental model of it had flittered away.
Now I take a much less strict approach to my code and I find it a lot better to work with later.
I see this a lot, especially among more junior programmers. I think it likely stems from insecurity with taking responsibility for making decisions that could be wrong. It makes sense, but I can’t help but feel it is failing to take responsibility for the decisions that are the job of engineering. Engineering is ultimately about choosing the appropriate tradeoffs for a specific situation. If there was a universally “best” solution or easy rule to follow, they wouldn’t need any engineers.
2. master follows the rules because he understands them
3. guru breaks the rules because they don't apply
There was an era where every just-above-newbie PHP developer would start writing their own authoritative blog posts and frameworks, I strongly feel that had a big impact on the fragmentation and reputation of the PHP ecosystem as being insecure (because a lot of these authoritative blog posts demonstrated how to introduce SQL injection vulnerabilities)
This is false and hopefully no one takes you seriously when they read that. There are books about empirical methods for software engineering, for example, which actually seek to find real evidence for software engineering techniques. See Greg Wilson's work, for example.
There are lots of other architecture/design books that use real world systems as examples. "Evidence" is definitely lacking in our field, but you can find it if you try.
edit: wow, in his project "It will never work in theory" he's fairly sober about the ability of "the industry" to reflect on "the craft"
https://neverworkintheory.org/
> about the project:
> People have been building complex software for over sixty years, but until recently, only a handful of researchers had studied how it was actually done. Many people had opinions—often very strong ones—but most of these were based on personal anecdotes or the kind of "it's obvious" reasoning that led Aristotle to conclude that heavy objects fall faster than light ones.
in the 2024 retrospective:
> Conclusion
> The comedian W.C. Fields once said, “If at first you don’t succeed, try, try again. Then quit. There’s no point in being a damn fool about it.” Thirteen years after our first post, it is clear that our attempts to bridge the gulf between research and practice haven’t worked. We look forward to hearing what actionable plans others have that will find real support from both communities.
>only a handful of researchers had studied how it was actually done
I am pretty sure that there are more than 5 references to papers in APoSD.
I think you grow out of that advice very soon, because it's not very practical, it feels out of touch. The result is not code that is easier to read, quite the contrary. I think the Java world has been influenced for worse by him.
But I don't have anything against him, as other comments say, the problem is dogmatism and trying to follow these authors blindly instead of thinking about it.
So, some kudos to Robert for accepting criticism and discussing it, but no cigar for downplaying his own advice when confronted - I also recall a different discussion, where someone confronts his statement "you don't practice tdd you're not a professional", and his answer "it was not meant to be taken seriously".
These people had great ideas but they should be more critical of themselves, eg "here's when not to apply this", "here's where to bend this", not "you're doing it wrong" or "don't take it literally".
Multiply that across the industry and that probably easily reaches in hundreds of millions of dollars productivity wasted.
Ps: Not advocating cowboy coding or spaghetti code either.
I don't like the guy very much, but for web development even just mostly following Clean Architecture does so much to keep things from devolving into chaos long term.
1. the online documentation of the function being called
2. the instruction documentation for an instruction being generated, inserting
3. the issue that the code fixes
4. the specification of what the function is trying to implement
Then I fixed my text editor to enable click on those links.
How would you unit test those methods? You'd have to directly set the field values, then call the method, then assert on the fields. If the answer is "you don't unit test private methods" then that's completely fine, because I agree with that (perhaps this is implicit from the private keyword, I don't know Java). But I'm struggling to imagine how you would get to those private methods with such a strict adherence to TDD as Bob recommends. Methods like increaseEachPrimeMultipleToOrBeyondCandidate() are quite complex, and would be tricky to build up using TDD if you couldn't exercise them directly.
If nothing else, surely Bob's approach is not thread safe. Call PrimeGenerator3.generateFirstNPrimes() concurrently and they'll trample all over each other. John Ousterhout's stateless version doesn't have that problem.
No, it’s an actual debate between the actual John and Bob. Them debating each other. It’s an amazing read.
Both books are bad, but APOSD is my most disliked technical book ever. CC is at least interesting as an exercise to see that critics are way too uncharitable. Kernighan and Pike's The Practice of Programming is far better than either. And https://antirez.com/news/124 is one of the few good discourses on comments out there, something as a profession we care way too much about when the cost of doing it "wrong" is typically so low.
The book struck me as giving mostly reasonable advice, none is which was overly prescriptive. None of the things I disagreed with struck me as egregious.
What's agreeable is mostly only so because it's such a straightforward platitude. "Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed." Ok? Anyone want to argue to the contrary? This is not teaching or learning anything new or of value, it's not even inviting argument like CC makes it easy to do. I was also hoping that with the book being so short it would be concise, but alas, it's full of this sort of stuff. The single page summary of design principles at the end is similar. A few of them you could quibble about, but arguments would likely just be in fully understanding the meaning of the terminology and what background contexts are assumed. Much advice is dependent on context! Context is something not really called out much in this book. As one example there was only a very slight hint that the author is aware that writing for the code reader means a reader from a particular audience, often your co-workers, and that gives you certain affordances you wouldn't have for say random blogger.
Elsewhere, not in the book, the author once wrote "The strong typing of object-oriented languages encourages narrowly defined packages that are hard to reuse. Each package requires objects of a specific type; if two packages are to work together, conversion code must be written to translate between the types required by the packages." This is actually a nuanced point and is good to discuss. The context of whether you have static types or dynamic types or a half-baked OOP system or a full-baked OOP system is very important context. But it seems a completely absent point of consideration from his "philosophy", even when you'd think it'd be appropriate to go over in the final chapter where he highlights OOP as a "trend".
A lot of the author's rants seem to be snipes at Java. Fine, whatever, though Java has answers to the complaints. (Especially modern Java.)
Lastly, and originally my first complaint because it's about the very beginning of the book (including the cover art), he's on shaky foundations with its definition of simplicity/complexity by conflating it with the subjective easy/hard. I was hoping for a post-Hickey (of Clojure/"Simple Made Easy" talk fame for anyone unaware) understanding that complexity is objective, but alas. It's not like Hickey invented that understanding, but in current year, I think it's quite questionable to disagree. So, the book: "For the purposes of this book ... complexity is anything related to the structure of a software system that makes it hard to understand and modify the system." Sorry, that's not a useful definition of complexity, and now the whole book is harder to read/easier to misinterpret because of the custom definition. Well, at least it's explicit that it's custom.
You can count the things, and count the twists. When a set of things has fewer twists (or even knots) than another set of things, it's simpler. When you pull on something, if it's attached to other things by twists, you are dealing with complexity. When you intentionally entwine things, you are creating complexity. You might say you are "complecting" things together, and once done they are "complected" together.
This is relevant from the smallest details of programming like state (being a more complex twist of value and time, compared to simpler immutable values that are timeless) to the largest issues of modularity (being a property of systems composed of smaller things; when you can disconnect such things without needing to untwist them from each other, you have achieved a simpler design).
This is separate from being easy or hard, though one could assert that a simpler system will tend to be easier to change, because you don't necessarily have to deal with as many things twisted together at the same time. But this isn't a given, because we programmers learn and get better at complex things such that they can feel quite easy, and we also love making tools to try and wrangle sources of complexity, either those inherent to a problem domain, or those we unnecessarily inflict on ourselves, and it can be quite easy to make changes to really complex systems once you've learned some of these tools. Complex things can also be very helpful from time to time, especially when they claim to solve a problem and you just want the problem solved yesterday without caring so much how. But regardless, whether something is simple or complex is a property that remains the same no matter who looks at it. Under APOSD's definition, something basic like immutable collections in a program would make it harder to understand because most people aren't taught about them as part of basic education, and many languages don't offer them as part of the standard library. They're unfamiliar, essentially. Even when you do get used to them, they can still be a bit difficult to work with depending on what you're trying to do. But are immutable collections more complex than mutable ones? No.
Recommended watching: https://www.youtube.com/watch?v=SxdOUGdseq4
>For the purposes of this book ... complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
>Complexity: things twisted together. You can count the things, and count the twists.
Presumably twisting makes things harder to understand and having more things requires a greater effort at understanding?
Not seeing that mutable vs. immutable plays into the APoSD definition --- if a system was suited to being represented by immutable collections and if the structure of the software system was designed to make use of immutable collections in its representation that would not make it harder to understand or to modify.
This is exactly the presumption that is wrong. Sometimes it's right, but often it's not. Programmers are addicted to complexity in part because in many circumstances producing more of it is so easy and convenient, especially right now -- it may make things more difficult in the long run, but not always and anyway not everything has to suffer from the tradeoff of long-run considerations. (e.g. many video games are still ship-and-move-on.)
And yes, mutable vs. immutable doesn't fit nicely with the custom APOSD definition either. Immutable is strictly simpler because it no longer twists together the value with the current time of the program. It's just a value. Another example would be (non-Common Lisp) classes: a (non-Common Lisp) class twists together state (values+time) with behavior (methods) and typically also namespaces and a data type. The alternatives you can use for simpler designs are immutable values, pure functions, and explicit first-class namespaces. It might not be easier, especially at first if you haven't gotten practice using such simple tools together in a non-twisty way, or if you design your program in such an obtuse way or the domain is so inherently stateful that the tradeoffs for the simpler approaches lead to unacceptable effects (try writing a game with no compromises on a pure functional style, it's not easy!). But there are still benefits. The more honest definition means that simplicity isn't an unalloyed good that always leads to more ease, but is just another (important) element to consider in the various tradeoffs programmers have to make.
Thank you for taking the time to discuss this --- looking forward to reading the Google book you recommended --- hopefully it will come up as a point of discussion here at some point in the future.
Ultimately the problem he is (and all of us are) facing is that "good software design" can't really be measured with the right linting ruleset or static analysis. So if you're trying to break the concepts down each level, while still maintaining a scope that should include all software, that probably means it's impossible not to come off as squishy and non-specific at several points. I still think he strikes a really good balance in general here.
I agree that there could be more discussion around context and audience. Ousterhout says "if you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex", but then what can possibly be done if everyone on my team was replaced with new hires who had next to no experience writing code? Did the same codebase go from simple to complex?
Anyway, the favored book I did highlight, The Practice of Programming, shares some things with APOSD: it's also not academic, is also quite short (maybe 70 pages longer), and is also more productively read earlier in one's career or study but it's still appreciable by those with more experience. You'll learn things about design. But it has so much more than APOSD: you'll learn things about implementation and debugging and considerations for libraries for yourself or others rather than just applications, and so much more in so few pages; just lots of things central to writing programs, which is the fundamental task at the end of the day, more so than just "designing" things.
I guess another complaint is that APOSD just doesn't have enough code in it. And perhaps an implicit philosophy I have is that you can't actually master good design without writing good code. Learning from the feet of masters is a good way to learn, but they actually have to teach by example. To that end, The Practice of Programming has many programs as examples (like a markov chain text generator, written in multiple languages with performance and effort-of-writing comparisons) and invites the reader to do many various exercises (like commenting on comments, or rewriting part of an example to use a different implementation decision and compare the different approaches).
When that book happens to make a claim I agree with, I don't tend to also just dismiss it as a platitude, because it's better argued and reasoned (or argued and reasoned at all), and supported and contains even more information to consider. Let's expand the bit I quoted about interfaces from APOSD, it's actually from the section on exceptions.
"Defining away exceptions, or masking them inside a module, only makes sense if the exception information isn't needed outside the module. ... However, it is possible to take this idea too far. In a module for network communication, a student team masked all network exceptions: if a network error occurred, the module caught it, discarded it, and continued as if there were no problem. This meant that applications using the module had no way to find out if messages were lost or a peer server failed; without this information, it was impossible to build robust applications. In this case, it is essential for the module to expose the exceptions, even though they add complexity to the module's interface. With exceptions, as with many other areas in software design, you must determine what is important and what is not important. Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed (Chapter 21 will discuss this topic in more detail)."
I find the student example here pretty weak, but it'd be stronger if the actual code was shown and developed, especially if done in a context where it's understandable how the students might have thought it was a good idea at first, rather than just making an obvious mistake because they're students. Chapter 21 does discuss things in more detail, but not much more, and again there are no code examples much beyond pointing back to a prior chapter's dozen lines of strawman Java. It starts off with:
"One of the most important elements of good software design is separating what matters from what doesn't matter. Structure software systems around the things that matter. For the things that don't matter as much, try to minimize their impact on the rest of the system. Things that matter should be emphasized and made more obvious; things that don't matter should be hidden as much as possible."
Does that not read to you as terribly verbose and information sparse? Capable of eliciting a "duuuuuh" even from a beginner programmer? Almost tautological even? The rest of the chapter is similar and doesn't actually give much more information at all. Sure there are a few tidbits of use in there, like the idea of "leverage" and what that means as an approach, and a throw-away line that deserved more elaboration about shallow classes needlessly increasing what seems "important". (Yegge's "Execution in the Kingdom of Nouns" post is a good expansion of that and other things, if it's at all helpful to understand examples of what I find valuable in comparison to this book.)
Let's compare now some similar bits from The Practice of Programming. This comes as a partial summary after a worked section on designing an interface for parsing CSV files in C and C++ with many design decisions detailed and discussed.
"Good interfaces follow a set of principles. These are not independent or even consistent, but they help us describe what happens across the boundary between two pieces of software. *Hide implementation details.* The implementation behind the interface should be hidden from the rest of the program so it can be changed without affecting or breaking anything. There are several terms for this kind of organizing principle; information hiding, encapsulation, abstraction, modularization, and the like all refer to related ideas. An interface should hide details of the implementation that are irrelevant to the client (user) of the interface. Details that are invisible can be changed without affecting the client, perhaps to extend the interface, make it more efficient, or even replace its implementation altogether. The basic libraries of most programming languages provide familiar examples, though not always especially well-designed ones. The C standard I/O library is among the best known: a couple of dozen functions that open, close, read, write, and otherwise manipulate files. The implementation of file I/O is hidden behind a data type FILE*, whose properties one might be able to see (because they are often spelled out in <stdio.h>) but should not exploit."
If you squint, kind of says much the same thing, right? But it's richer, includes whys, and points to a real-life example, not a student project. It also criticizes the C I/O library right after because of its exposure of publicly visible data.
More on the topic of exceptions, the book takes a rather classic approach that I don't fully endorse ("Use exceptions only for exceptional situations"), but one unique bit is a more thorough treatment of handling errors without having to alter control flow, and why that might be important. In the markov generator program, one worry is that there might not be enough input to start the algorithm. One could exit prematurely (with a special value or an exception) but the book chooses instead to do some padding to ensure the problem goes away. Emphasis mine:
"Adding a few NONWORDs to the ends of the data simplifies the main processing loops of the program significantly; it is an example of the technique of adding sentinel values to mark boundaries. As a rule, try to handle irregularities and exceptions and special cases in data. Code is harder to get right so the control flow should be as simple and regular as possible."
You don't have to take this rule as given, you immediately see it in action, and an exercise later invites you to re-implement without a sentinel value to compare.
APOSD has an entire chapter on errors, but this idea is only barely hinted at in the whole chapter on errors with the idea of defining errors out of existence (it uses a more controversial example, I think, from TCL) and this bit that clarifies that by "exception" he doesn't necessarily mean a stack-unwinding thing: "However, exceptions can occur even without using a formal exception reporting mechanism, such as when a method returns a special value indicating that it didn't complete its normal behavior. All of these forms of exceptions contribute to complexity."
It's just such a shallow treatment, and I think that last bit is more focused on the other basic idea that Practice of Programming spells out:
"Exceptions should not be used for handling expected return values. Reading from a file will eventually produce an end of file; this should be handled with a return value, not by an exception."
That's followed by a code example showcasing said behavior that doubles as a less-strawman swipe at classical Java. (The Java code loops in.read() until it's -1, and has separate exception handlers for a file not found exception, which the book thinks isn't all that exceptional, and a generic IOException.) But to APOSD, it doesn't seem to matter, they all just contribute to complexity. Maybe they contribute to different degrees? (This would require an objective definition of complexity that lets you count the twists, though.) Maybe leveraging the type system (if you have such a language) to define away errors should be mentioned? Maybe (though this one is truly a rhetorical fever dream wish) acknowledgement of Common Lisp's condition system as yet another powerful alternative should be given?
Apparently, there is something of a tension at Stanford in that freshmen are being taught to keep methods/functions short, while the course on software design has as a pre-requisite CS140 which in turn requires CS 107 or EE 108B and CS107 requires CS106B, so it probably couldn't be taken until almost halfway through a four-year degree (and there is a note on the course page that preference will be given to those graduating in the near term).
That said, there is value in laying out basic principles and premises, _and_ the experiences which in turn support them. Reading through your link, it seems to line up well with my understanding of recommendations for comments in APoSD, which makes one wonder how it could be made to work as a text for an introductory course in some language which was approachable by beginners.
On the one hand, the books are popular because a lot of people reading them think it makes a good point, and share that view. On the other hand, just because something is popular doesn’t make it right! I think this is where AI gets so much wrong. GIGO! If you base all your code on whatever is most common, are you really really sure that common pattern is really the best? AI, and these book evangelist, often have no clue. Just parroting others.
I’d rather deal with “principles” as opposed to “rules” every time. Glean the principles from the books, and at least try to write clean code!
- Code Complete
- The Pragmatic Programmer
https://en.wikipedia.org/wiki/Code_CompleteI remember it being pretty decent back in the day. I can't remember any takes that were too hot in it. Honestly, I can only remember a general sense of satisfaction(?) with the book. If you were to ask me what exactly I took from Code Complete and applied in my job today, I couldn't tell you.
What would you classify as "snake oil" in it? Do they recommend Hungarian notation or something weird?
- All the time wasted on the dumb "construction" analogy.
- The total lack of attention to open-source software, possibly because he was misled by his own "construction" analogy. There's no such thing as a freely redistributable cabinet that's extra reliable because your house shares it with the local nuclear reactor, or a rotten floor joist you can't fix without negotiating a source license. (He does discuss buying libraries, just as you can buy cabinets instead of building them.) Though this was surely also lacking in the first edition, it was a more forgivable oversight in 01994.
- Very little attention given to automated testing; we don't get to "developer testing" until chapter 22, and even that's mostly about manual testing, though there are a few offhand remarks in §4.4 and §9.4 about unit testing and test-first programming, with no explanation of what that means. Even when he tries to explain "test-first programming" in §22.2, there's no hint that we're talking about automated testing. And we finally see mentions of "test code" and "JUnit" in §22.4, and then §22.5 and §22.6 have information about actual automated testing, though without any actual test code. The advice on test-case design is still excellent.
- Also, very little about source control. I know this was a deficiency in the first edition because I remember the revelation of learning about RCS a couple of years after I read it. I think it actually got better in the second edition; there's a little "Version Control" section in §30.2 which refers you to §28.2, "Configuration Management", which talks a little bit about the problem but doesn't mention Subversion (first released October 02000), CVS (01990), RCS (01982), SCCS (01973), or even Visual SourceSafe (01994). Instead, it mostly describes implementing similar processes manually, through bureaucracy, because this is the "Managing Construction" chapter. But there is technically half a page on p. 668 singing the praises of version-control software, calling it "indispensable on team projects", which manages to not mention a single program you could use for it. An understandable oversight in 01994, unforgivable in 02004, but again, incompetence rather than snake oil.
- Although he pays lip service at the beginning of the book to the independence of project phases and project activities, he often conflates them later, often presuming a quasi-waterfall model (when he isn't outright advocating it), where a requirements-analysis phase is followed by an architecture phase, then a detailed design phase, then a "construction" phase, then a testing phase, and then finally a maintenance phase. This is obviously completely unlike the reality of projects like Microsoft Windows, Emacs, Linux, GCC, and Facebook. When did Facebook mostly move from detailed design to construction? Would it have been a better social-networking website if it had spent a year or two on architecture before beginning "construction"? He does kind of go back and forth on this a lot, though, sometimes advocating more incremental approaches and then contradicting himself a page later.
- Relatedly, he advocates a division of labor where "the architect consumes the requirements; the designer consumes the architecture; and the coder consumes the design." (Traditionally, though he doesn't say this, the QA tester then consumes the code.) This division of labor has been tried many times, and the companies that have tried it have been mostly outcompeted by companies with less dysfunctional divisions of labor; they mostly survive only in niches where they have legally enforceable monopolies, such as DoD cost-plus prime contractors. None of them have been able to produce products of quality comparable to things like Linux, GCC, and Facebook. I think this is the snake-oiliest part of the book.
- Code Complete's table of "Average Cost of Fixing Defects Based on When They're Introduced and Detected", table 3-1, is convincing, compelling, thoroughly footnoted with decades of literature, and completely made up. See https://softwareengineering.stackexchange.com/questions/1637... https://web.archive.org/web/20121101231451/http://blog.secur... https://www.lesswrong.com/posts/4ACmfJkXQxkYacdLt/diseased-d... https://gist.github.com/Morendil/258a523726f187334168f11fc83.... This made-up data is McConnell's major justification for advocating waterfall-like models. More recent research that investigates the question empirically instead of relying on made-up hearsay finds, by contrast, "We found no evidence for the delayed issue effect; i.e., the effort to resolve issues in a later phase was not consistently or substantially greater than when issues were resolved soon after their introduction." https://arxiv.org/pdf/1609.04886 https://agilemodeling.com/essays/costofchange.htm https://buttondown.com/hillelwayne/archive/i-ing-hate-scienc....
- The section about "user interface design" is cringe-inducingly bad. He thinks you can design a good user interface up front without having working software ("The user interface is often specified at requirements time. If it isn't, it should be specified in the software architecture,") rather than incrementally responding to usability feedback from people using a working system. It's a very short section, and that in itself is eyebrow-raising; usability is a central concern of most kinds of software, and one of the most challenging aspects of software. Really, almost everything in most software should be driven ultimately by user experience and grounded out in usability testing. Games, websites, browsers, and even compilers live and die on usability. But McConnell treats it as one minor detail among many.
- The section about the "architecture prerequisite" sounds like it was written by IBM mainframe programmers in 01978, then decorated with some OO and WWW jargon. Yes, clearly the architecture should "describe the major files and table designs to be used". That makes sense. Yes, "Input/output (...) is another area that deserves attention in the architecture. The architecture should specify a read-ahead, read-behind, or just-in-time reading scheme." I mean, seriously? Note that words like "client", "server", "tier", "cache", "protocol", "network", "message", "queue", and even "process" (as in a running instance of a program) are completely missing here. It's not that he uses different terms for them; he just doesn't talk about them at all, using any words.
- He tries to discuss "fault tolerance" with a totally nonsensical example "the square root of a number", necessarily making complete hash of the topic as a result. He doesn't mention any of the techniques that actually work for achieving fault-tolerance, such as statelessness, idempotence, the end-to-end principle, transactions, journaling, fail-stopness, checksums, disk mirroring, hardware trimodular redundancy, watchdog timers, ECC, anomaly detection, network timeouts, monitoring, alarms, etc. The only exception is that he sort of mentions granular restarts. I'm restricting myself to techniques that were well-known when he wrote the first edition of the book here, excluding things like Paxos, eventual consistency, and Merkle graphs.
- There are a lot of cases where he repeats something he's heard that he evidently doesn't understand. The muddled attempt to explain fault tolerance above is one example, but we could also mention, for example, his attempt in §4.1 to describe Fortran programmers writing Fortran in C++, which completely misses the actual major difficulty (it's mostly about structuring the data as arrays, not the control flow), or his remark, "Assembler is regarded as the second-generation language," devoid of the historical context to provide any meaning to it.
- One problem that I think is actually new in the second edition is its presumption that all software is object-oriented (despite paying lip service to the fact that Visual Basic [6] was the most popular language among professional programmers, many people were still programming in Ada and assembly and Cobol and C and Fortran, etc.) and that looks a bit snake-oilier from our perspective now than it did at the time. I think OO is a useful approach to software design, but if I'm writing a generic tutorial on how to design a program, I wouldn't have a step in it called "Level 3: Division into Classes" as McConnell does in §5.2, because that makes my book completely inapplicable to programming in C, Go, Fortran, Rust, Racket, VB6, or Clojure, and inapplicable to much of what people do in Python, PHP, JS, Octave, and R. The snake oil here is not object-orientation but a totalizing ideology that everything must be OO; the Chapter 6 introduction says, "In the twenty-first century, programmers think about programming in terms of classes.". The way I remember it, the first edition didn't have this problem.
- This totalizing OO outlook is somewhat exacerbated by the fact that he doesn't really understand object orientation at all, so he gives a lot of bad advice, like, "A large percentage of routines in object-oriented programs will be accessor routines, which will be very short," §7.4. His whole chapter 6 is about designing classes, but he never mentions the actual core concept of object-orientation, which is polymorphic message sends, presumably because although he knows they exist, he isn't really comfortable with them and doesn't understand how central they are to the OO worldview. Instead he treats classes as a newfangled synonym for CLU's "clusters" or Ada "packages". Much of the chapter is devoted to workarounds for shortcomings of C++. This isn't really "snake oil," just incompetence.
- Another problem that I'm pretty sure wasn't present in the first edition is the counterproductive recommendations of worthless IEEE management process standards in virtually every chapter. This is kind of snake-oily; these standards are of abominable literary quality and contain no useful information that could conceivably help anyone to improve the software they write. As a representative emetic example, check out IEEE 1028, recommended in Chapter 20 and again in Chapter 21. http://profs.etsmtl.ca/claporte/english/enseignement/cmu_sqa.... Unlike some other IEEE management process standards I've had the misfortune of reading, it at least doesn't seem to contain any misinformation, but that's because it manages to spend 47 pages saying nothing at all about software.
There's still much material in the book that's solid, and lots of references to good information, but it's mixed with a lot of serious misinformation, misleading analogies, and embarrassing incompetence. And it's a slog to get through so much verbiage. But it's certainly better than Clean Code. Still, now that The Practice of Programming, The Pragmatic Programmer, and A Philosophy of Software Design are out, I think there's no longer any reason to recommend Code Complete.
Although in your excellent break down of the shortcomings of the book, you kind of make me want to go back and re-read it myself. Just to experience it from my current vantage point. To see where I disagree with the book now.
> Code Complete's table of "Average Cost of Fixing Defects Based on When They're Introduced and Detected"
This section really hits me because that's one thing I can confidently say I've internalized over the years. To know that it's been based on fictional data is disappointing.
You, and I, and the entire software industry, it turns out.
https://buttondown.com/hillelwayne/archive/i-ing-hate-scienc... is the best summary I've found on that.
> Are Late-Stage Bugs More Expensive?
> (...) While there's no smoking gun, I think the body of research so far tentatively points in that direction, depending on how you interpret "late-stage", "bugs", and "more expensive". This is a newsletter, not a research paper, so I'll keep it all handwavey. Here's the rough approach I took to reach that conclusion:
> Some bugs are more expensive than others. You can sort of imagine it being a Gaussian, or maybe a power law: most bugs are relatively cheap, a few are relatively expensive. We'd mine existing projects to create bug classifications, or we'd interview software developers to learn their experiences. Dewayne Perry did one of these analyses and found the bugs that took longest to fix (6 or more days) were things like feature interaction bugs and unacceptable global performance, in general stuff that's easier to catch in requirements and software modeling than in implementation.
> I've checked a few other papers and think I'm tentatively confident in this line of reasoning: certain bugs take more time to fix (and cause more damage) than others, and said bugs tend to be issues in the design.
As I see it, this is not so much about how long the bug has been in the system but about what kind of bug it is. Unacceptable global performance or fundamentally incompatible requirements often kind of require you to restart your system from scratch, which means throwing away a lot of your code. But that doesn't mean that it's trivial to detect them (necessary to fix them) before you've written a lot of code, and throwing away all your code and design documents doesn't mean you're actually starting from scratch, because programming is theory building, not building construction. If writing "the same program" a second time takes you 30% of the time it took you to write it the first time (but the right way), in a sense you retained 70% of the work you did from the first time, even if not a line of code is the same. (Of course, a "bug" that costs 30% of the total project budget is a pretty big bug...)
Also, with iterative development, a requirements or design bug can be introduced very late in the project. Emacs has been around for 40 years but only got native-code compilation for Elisp (a huge requirements and design change containing, probably, many requirements and design bugs) in the last 6 years. 85% of Emacs development was before that. And people were testing it within months of embarking on adding it.
That makes sense. Even as I was working out in my head why a bug would be more difficult to fix later, I came across the same general thought. That the problem is that by the time the bug gets to production, there's a lot of underlying support for it. And often, fixing that bug means fixing the underlying support as well.
And it's something you often don't discover until you're bitten by it.
And these bugs will naturally be found later because they are more difficult to find in the first place.
Then we get to the bit that essentially reinforces the adage from The Mythical Man-Month: Plan to throw one away, you will anyhow.
And it makes sense. It is obvious when laid out as such. And it is obvious how that section of Code Complete is at odds with The Mythical Man-Month.
But the SOLID and Clean Architecture principles inform me almost daily.
It is probably OK to be thinking on issues related to a field (i.e., software engineering) without being a practitioner in the field, but producing fads-du-jour and selling them as solid (pun intended) theories and expecting to be taken seriously is just ludicrous to me.
Even if Uncle Bob thinks tiny methods are great, why would he introduce the pseudo-constructor "initializeTheGenerator" and make everything static if he needs state? If the helper methods were instance methods instead, the static "generateFirstNPrimes" method could simply construct a new instance to store the state.
Code that will change or can’t ever be considered final, is the real challenge.
Overly cutting code into methods makes code just rigid. This could be the point, I guess, but if you need to change the methods name in order to reflect the methods intent, than you just wrote the classic unhelpful comment of:
// check a is not null
if (a != 0) { … }
Overuse of comments has the same issue as overuse of methods.
Without rigor, comments and methods names will start to lie.
Because their content / name weren’t necessary to understand the code. And should just not exist in the first place.
This explains everything that's wrong with modern software.
When you design a Formula 1 race car engine, the purpose of engine design is not to "make the engine easier to modify". It's to win races. And that depends on the race - a funny car engine, a formula 1 engine, a LeMans engine, Nascar engine, etc, are all different because the races are different.
Another example: when you design a building, the goal isn't to make it easier to understand the building. The goal is to meet the requirements of the building, its uses, requirements, environment, etc. Sometimes a better building is just more complicated, and making the architect or builders' jobs easier, while nice, isn't the goal.
Some things aren't supposed to be easy to understand, because ease of understanding is not the goal of the thing. Focus on the real goal of the thing, and achieving that; don't get distracted by ancillary goals.
Obviously, writing code such that a first year comp sci student can understand what's happening and can start contributing immediately is absurd, but at the same time nobody builds anything in a vacuum. There's a certain legibility required within any context you're designing something for.
Ease of modification was one of the top priorities in the design of the Model T Ford, because cars break down and must be repaired, and a car that is difficult or impossible to repair will cost its owner large sums of money. Software doesn't break down (though online services do) but for other reasons modification is a high priority.
Perhaps short-lived buildings don't need to be easy to modify, especially if the architects have a very good understanding of the needs of the users over their lifetimes. Often that is not the case, though, and Christopher Alexander was famous in large part because much of his career was devoted to figuring out how to enable inhabitants of buildings to modify them more easily, so that their needs would eventually be met even if the architects guessed wrong decades in the past. Centuries-old stone farmhouses exist, too, and ease of modification is crucial for them; if they cannot be modified they cease to function in only a century or two at most.
Whatever the goal of your software, it is crucial for the people who are modifying it over time to achieve that goal to be able to understand it.
My favourite story from a previous job was working with high-speed scanners, OCR, data quality, typo correction - that kind of thing.
The pipeline did its job well enough. But later we got a new contract where we accepted email submissions (where the customer had scanned stuff themselves). The guys couldn't hook that into the pipeline, so they set up a new pipeline - to print out the emails so they could then be scanned into the existing pipeline.
I still get a good laugh about that to this day.
1) UB said he reads the code in its full from left to right with the if(isTooHot) example. I only resort to reading code in that way as a last resort if I really can't figure out what the code is doing. I mean I look at a block or row and take it in more as a whole.
2) UB said comments are annoying because he has to read them and keep the whole of the comment text in his mind. This again says he reads everything left to right, and he can likely store everything that he has read up to a certain amount.
My mind works nothing like that. I can hold very few words in my working memory but can instead hold concepts/ideas. For that to work good I need to see as much of the involved code as possible and my mental image evaporates if I have to navigate too far where I started.
I accept that non-fiction books on anything will oversell the value of their way, and try to take what I can at a more moderate level. Through that lens, Clean Code didn't give me much, but Clean Architecture did. The Clean Coder is also an interesting read on professionalism in software, and Clean Agile is an interesting read on Agile roots. I don't know anyone that practices "true" agile (nor do I care to do so myself), but there are some really solid ideas in there.
I get that Clean Code kind of had a cult-like following in that people followed it blindly, but damn some of these comments are just rude about Uncle Bob. I still think he's a pretty good author and has given me some advice through his other books that helped me a lot as a fresh faced dev.
[1] https://www.goodreads.com/book/show/405790.Beautiful_Code
John sounds like he is about to start building a new type of database, and Bob sounds like he's knee deep in a 20 year old code base for a logistics company. Both of their positions are reasonable, and both optimized for specific contexts.
I found Bob's responses more measured (which I value a lot), with John's at times being more compelling. I do agree that over-composition is a real problem that Bob is on the wrong side of the line on. But to be fair, Bob and Clean Code comes from a time where it was the opposite and his position on this feels like a philosophy that has an over-correction (albeit - not necessarily a flaw) at it's core.
private static boolean
isMultipleOfNthPrimeFactor(int candidate, int n) {
return candidate ==
smallestOddNthMultipleNotLessThanCandidate(candidate, n);
}
Not only is the method itself completely pointless, it also happens to have side effects! Who would expect this from the method name? So much for self-documenting code... Ousterhout rightfully calls him out on this bullshit.In fact, Ousterhout makes such great points that I really want to read his book. Conversely, I'm now even less inclined to read Clean Code.
I haven't read the whole article when I wrote that comment. Turns out that Ousterhout provides a rewritten version of "PrimeGenerator" that does exactly this. At least UB concedes that this version is indeed much better.
Are there any other such reads in the software engineering field?
He has argued in the past that the concept of Literate Programming
http://literateprogramming.com/
is the most important work he has done, and I highly recommend his various collections of lecture notes/papers including:
https://www.goodreads.com/book/show/112245.Literate_Programm...
The architecture of Concurrent Programs, Per Brinch Hansen
Literate programming--see http://www.literateprogramming.com/
I don't recommend Design Patterns, as these are elements needed if your programming language is inadequate.
There are many more.
What's the value of extracting a function that is used only once or twice. It's probably very limited. It's debatable whether that even should be a public function and whether you should encourage more use. And then we can look at the function declaration as well. Does it have a lot of parameters? Is there any complexity to its implementation? Does it have tests? Are there going to be lot of uses of the function? If the answer to all those questions is no, you could probably inline it without losing much. But the flip side is that you wouldn't gain much by doing so. A small function that is used a lot is probably somewhat valuable.
And there's a third thing that needs to be considered: does a function increase the API surface of your module. Having lots of private functions makes your module hard to understand. Having lots of public functions, makes the API less cohesive.
So, there's a grey area here. Languages like Kotlin give you a additional options: make it a nested function, make it an extension function, put it in a Companion object, etc. You can put functions in functions and those can help readability. The whole point of doing that is preventing usage outside the context of the outer function. Nested functions should probably be very short. And their only goal should be to make the outer function logic more readable/understandable. It's not something I use a lot but I've found a few uses for this. There's no point to using nested functions other than for readability.
And speaking of Kotlin, it's standard library is full of very small extension functions. Most of them are one or two lines. They are clearly valuable because people use them all the time. You get such gems as fun List.isNullOrEmpty(): Boolean which helps make your if statements a lot more readable and less flaky. Also works on Java lists. Stuff like that is a big part of why I like Kotlin.
I tend to dumb down a lot of advice like both are debating here to cohesiveness and coupling. In the context of functions, you get coupling via parameters and side effects (e.g. modifying state via parameters) instead of return values. And you lose cohesiveness if a single function starts doing too many not so related things. High coupling and low cohesiveness usually means poor testability. You'll find yourself mocking parameters just to be able to test a function. Improving testability is a valid reason for extracting smaller, easier to test functions.
This is to the point that, in order to program and do the things I want to do, I have to essentially write nearly everything out longhand, to the point of unrolling things in repetitive fashion, and organizing things in blocks of code separated by comments about what's being done in each block. I can do this so predictably and regularly that my code gets parsed by other people's more Clean Code and ingested as sort of blocks of program behavior to be used in other software, to the point where it's an 886-star repo with 79 forks: not Bob-scale, but then I haven't written books or revolutionized corporate coding.
I've had to learn useful things about where my approach doesn't take advantage of its hypothetical strengths: heedlessly unrolling everything doesn't give you speed boosts, and I've had to learn to declare variables nearer to where they're used. But I've also had to learn that I could do the opposite of Clean Code for performance gains. Back in the day, you could assign variables for calculations to avoid Repeating Yourself, but on modern processors it turns out… in addition to techniques like running calculations in parallel on wide data words that contain different data processed together… you can even take advantage of how eager CPUs are to do math, to avoid creating extra variables. It can be more efficient to just do the math a couple times rather than create a whole new variable just to skip the math.
This world makes sense to me. It acts like assembly language, except it's C (not even C++). I don't know to what extent there are other people who think this way, or struggle to keep track of even simple abstractions.
It's just the context with which I see Bob acolytes, rather than just declaring a variable to not do the math twice, breaking it off into about twelve different methods for seemingly purely semantic reasons, and insisting anything else is stupid. And there I am, producing and re-using reams of shockingly primitive code that seems to work and where I can return to it, even a couple decades later, and have no trouble figuring out what I did.
There's something to be said for being SO stupid that your work just works.
For the topic: the discussed topics are relatively trivial surface level stuff, mostly I agree with POSD, but these will be handled by AI anyway. I guess humans will use the spare brain capacity to deal with the real deep design questions (for a while).
import itertools
def generate_n_primes(n):
"""
Generate n prime numbers using a modified Sieve of Eratosthenes.
The algorithm keeps track of a list of primes found so far,
and a corresponding list of 'multiples', where multiples[i] is a multiple of primes[i],
(multiples[i] is initially set to be primes[i]**2, see the optimisations section below).
The main loop iterates over every integer k until enough primes have been found,
with the following steps:
- For each prime found so far
- While the corresponding multiple is smaller than k, increase it by steps of the prime
- If the multiple is now the same as k, then k is divisible by the prime -
hence k is composite, ignore it.
- If, for EVERY prime, the multiple is greater than k, then k isn't divisible by any
of the primes found so far. Hence we can add it to the prime list and multiple list!
There are a few optimisations that can be done:
- We can insert 2 into primes at the start, and only iterate over every odd k from there on
- When we're increasing the multiple, we can now increase by 2*prime instead of 1*prime,
so that we skip over even numbers, since we are now only considering odd k
- When we find a prime p, we add it to the prime and multiple list. However, we can instead add
its square to the multiple list, since for any number between p and p**2, if it's
divisible by p then it must be divisible by another prime k < p
(i.e. it will be caught by an earlier prime in the list)
"""
# Insert 2 into primes/multiples
primes = [2]
multiples = [4]
# Iterate over odd numbers starting at 3
for k in itertools.count(3, 2):
# If we've found enough primes, return!
if len(primes) >= n:
return primes
# For each prime found so far
for i in range(len(primes)):
# Increase its corresponding multiple in steps of 2*prime until it's >= k
while multiples[i] < k:
multiples[i] += 2 * primes[i]
# If its corresponding multiple == k then k is divisible by the prime
if multiples[i] == k:
break
else:
# If k wasn't divisible by any prime, add it to the primes/multiples list
primes.append(k)
multiples.append(k ** 2)
return primes
Some might find the docstring as well as comments too much - I find the comments help relate the code to the docstring. Open to suggestions!Turns out you can just pass immutable data in and get immutable data out. Who would have guessed? The whole 90s - 00s Java OOP garbage still gives me nightmares
I think this missed the point entirely. If i had to read the entire code to understand the behavior of that method, then is it really cleaner? Side-effects are evil
> Would that we had such a crystal ball
And then it seems like he actually found his crystal ball, because in the very next question he refers to things that have not yet occurred in the conversation:
> interpreting your rewrite (below)
And later:
> In your solution, which we are soon to see below
This makes it somewhat confusing to read, with answers being based on counterpoints that will only have been made in the future. (Which, I suppose, is similar to the problem Ousterhout has with UB's PrimeGenerator example.)
I'm in the middle of designing a course for a client on teaching software engineering best practices for data scientists (and folks who live in Jupyter all day).
There seems to be a huge lack of material for these types that aren't "programmers", don't live in an "IDE", and are essentially writing code all day.
Ousterhout to Uncle Bob:
> maybe you were surprised that it is hard to understand, but I am not. Said another way, if you are unable to predict whether your code will be easy to understand, there are problems with your design methodology.
This debate is full of treasures like this. What a brilliantly clear and understated way to expose charlatanism!
What is this “world-changing software” I'm saying Ousterhout has shipped? Tcl. (Hold on, now, don't downvote just yet.) Tcl has been a crucial enabling technology for EDA and automated regression testing since literally the 01980s. Probably every VLSI chip in the computer you're reading this on was designed, verified, and tested with workflows involving unholy amounts of Tcl. GCC's test suite is also Tcl. Still.
Automated testing in the 01980s? Yes. It's true that automated testing wasn't very prevalent in the software world until the Agile guys (Uncle Bob and his less incompetent compatriots) popularized it around the turn of the century, but EEs and compiler engineers have been pervasively automating testing a lot longer than that, and Tcl was for a long time the least awful option, believe it or not. And that was John Ousterhout's doing.
Do you know what the SPICE developers did to make SPICE scriptable, before there was Tcl? They linked csh into it. Motherfucking csh. If you've never tried to maintain a large script in csh, you do not know the meaning of suffering.
Good programmers write good software; bad programmers write bad software, or no software. Ousterhout has written one of the few pieces of software that can be called great. (In its historical context. In 01978 csh was great software too.) What software has Uncle Bob written?
Listening to Uncle Bob's programming advice over Ousterhout's would be like listening to your middle-school English teacher's writing advice instead of Stephen King's. It's not that King could never give you worse advice, but if you need your English teacher's advice, generally your judgment will not be good enough to distinguish the rare occasions King gets it wrong.
As for Sprite-LFS, I really enjoyed the Sprite LFS paper and found it inspiring, but my conclusion was that Seltzer's followup BSD-LFS paper falsified some of its more surprising claims, and ultimately the underlying predictions about the relative trends in RAM size and disk size turned out to be wrong, undercutting the key advantages of the LFS approach overall. Vaguely LFS-like approaches are important to SSDs and SMR disks, but WAFL was already about that LFS-like in 01995 (which is admittedly after Sprite-LFS), and SSD FTLs also do some not-very-LFS-like things. So ultimately I don't think Sprite-LFS turned out to be that important.
Sprite as a whole I'm less able to evaluate. I've never been an OS researcher, but I've spent a fraction of my life reading SOSP and HotOS papers and systems dissertations, and I don't remember seeing anything that came out of Sprite except Sprite-LFS. I was thinking maybe doors in Solaris did, but no, that was Sun's Spring, not Sprite. Other side of the Bay, where Ousterhout took Tcl eventually. So it's possible Sprite was a great achievement, but I haven't noticed it. But I think more likely it's one of those things where we tried the "obvious" thing (SSI across a bunch of workstations) and found out why it was bad, which influenced later efforts like PVM, MOSIX, Beowulf, distcc, MapReduce, Ceph, etc., because Sprite stepped on the mines so they didn't have to. There's a nice retrospective (by Ousterhout, natch) at https://web.archive.org/web/20150225073211/http://www.eecs.b....
So I don't think Tk, Magic, Raft, and Sprite-LFS really have the same level of significance as Tcl. Sprite maybe.
I don't think it's bad to spend a lot of time and effort on things that turn out to not be very significant, for two reasons. One is that, after a long enough time, very little indeed remains very significant. (Who, today, can recount the disappointments of the Minoan queens?) The other is that things you could do that are significant—even for a little while—are usually things that will probably fail. So if you spend a lot of time doing things that might be significant, you'll fail at most of them.
But in Ousterhout's case, one of those things did succeed brilliantly, and it was Tcl.
Most programmers know about this in 2025 but they didn't back then. Looks like the authors don't even mention it.
Next step - fashion and belief.
Moreover, the book argues for engineering principles (in pretty much all possible senses of that phrase).
I'm not saying that philosophy is bad. Maybe making software just never meant to become an engineering discipline. I mean making clothes, laws, and music isn't. And it's fine.
But engineering does imply some rule-following.
As Alexander Pyatigorsky famously wrote, "the value of philosophy is in that nobody needs it".
science without philosophy is just scientism. it leads to engineers creating previously unimaginable horrors!
This quote from Uncle Bob is shameful, considering that he has made 100% of his career on writing English, not code.
>If you can visualize a system, you can probably implement it in a
>computer program.... This means that the greatest limitation in writing
>software is our ability to understand the systems we are creating.
Though interestingly it is in marked contrast to a different statement in the "Software Design Book" Google mailing list:
>John Ousterhout, Aug 21, 2018, 12:30:15 PM
>I've never felt that graphs are a particularly useful way of describing software structure.
>The interactions between classes end up so complicated that the graph becomes an unreadable mess.
>Also, I'm not sure that the complexity of a graph representation of software correlates with its
>practical complexity (the graph representation might look very complicated, but the software might
>still be pretty easy to maintain).
and I'd be interested if someone knows of a text/video/interview which resolves that twain, or what sort of visualization is advocated for/recommended.
It was first described by E.W. Dijkstra "Notes on Structured Programming (EWD249), 2nd; 1970; TH Eindhoven". https://www.cs.utexas.edu/~EWD/ewd02xx/EWD249.PDF
And then reformulated by D.E. Knuth "Literate Programming; 1984" http://www.literateprogramming.com/knuthweb.pdf
Both use it to demonstrate their way of deriving and documenting an algorithm.
And then R. Martin used it again in his book "Clean Code; 2008". (Though I'm not 100% certain what he wants to demonstrate with his rather difficult formulation.)
For the sake of this email I am going to call it "Dijkstra's algorithm".
If we look at it from an engineering perspective, then Dijkstra's algorithm is never the best choice (at least not in 2025).
If performance is not the most important aspect, e.g. if we need less than 100000 primes, then the straightforward trial division is just as fast - and soooo much simpler (basically impossible to get wrong). Note that this is different in 2025 than it was in 1970, back then multiplications and divisions were much more expensive so it made sense to minimize them.
If performance is the most important aspect, then the sieve of Eratosthenes is much faster. And you can implement the sieve with a sliding window, which is just as fast (or even a bit faster because of caching) and uses a bounded amount of memory.
Concretely - my implementation of the sliding window sieve of Eratosthenes is about 50 times faster than Dijkstra's algorithm (on the same machine) when generating the first 200 million primes (7.5 sec vs 6 min).
The reformulation - both by Ousterhout and Martin - computes multiples for every new prime found. This will quickly lead to overflow, e.g. the 6543-th prime is 65537, so its square will overflow 32 bit unsigned integers. Dijkstra's formulation on the other hand only computes multiples that are actually used to filter, the multiples are never (much) larger than the candidate.
Note that computing the multiples so eagerly will also make the multiples vector unnecessarily large (wasting memory).
Knuth and Dijkstra both remark that there is a subtle problem with the correctness of the algorithm. When incrementing lastMultiple and then later accessing primes[lastMultiple] und multiples[lastMultiple] it is not obvious that those will already be assigned. In fact they will be, but it is very difficult to prove that. It is a consequence of the fact that for every integer n there is always a prime between n and n*n, which follows from Betrand's theorem, a difficult number-theoretical result.
So if you look at Ousterhout's and Martin's reformulation and think "a yes - now I get the algorithm", then beware: You've missed at least two aspects that are relevant for the algorithm's correctness. ;-)
I also wonder how many people interpret his advice as that of a mindless, pedantic dictator.
My own introduction to UB was from some random YouTube video he made about programming languages, so my first impression of him included his humor and his ability to see both sides of an issue while being unafraid of having a strong opinion. I really enjoy speaking with and listening to people with strong, long-marinated opinions, regardless of whether I agree or not. At the very least it means they've put a lot of thought into it, which makes for better discussion and learning.
I also lack a long history of code commits, being more of a dabbler here and there, so perhaps I have a smaller surface area for UB's jabs to land upon. Still, I acknowledge that a Clean Code Nazi would probably rip me to shreds for some of the practices I've followed and some that I continue to follow. But improvement is a much more achievable goal than perfection, and gleaning valuable information is better than being dogmatic.
In the end I love listening to UB talk. I don't follow all his practices but I do keep them in the back of my mind. If not worth following strictly they are always worth considering, especially the intent behind them.
So when I see his opinions on comments or his opinions on abstraction and variable naming my first instinct is not to lament about how he is poisoning our youth or insulting my code, but rather to ask myself how I can make use of his perspective. I'd encourage others to do the same; it's much more fun that way, not just for programming but for everything.
As for those of you stuck in "Clean Code" hell with oppressive supervisors demanding strict adherence... that sounds like a personal failure, or a personal incompatibility, or both. I would blame the messenger there, not the message.