553 comments

[ 3.1 ms ] story [ 487 ms ] thread
What a great discussion between two prominent figures in the field of software design. Thank you for posting this!
You guys really hated that I found the discussion interesting?
FWIW, me too comments are usually downvoted.
Makes sense now, they don't add much, or anything. We already have the upvote button for this.
It seemed like a content-free comment. I was no better informed after reading it than before, and it was of no artistic or cultural value. It did not induce me to question any of my assumptions or investigate anything. It expressed your experience, but your experience was not unusual or surprising in any way, except perhaps that you did not know that "Uncle" Bob Martin was an incompetent charlatan. Possibly those were among the reasons people downvoted it.
Fair points! Thank you.
Downvotes are a legitimate expression of disagreement, not hate.
"Hate" was too strong of a word to use.
john ousterhout's book is the only book on how to write software that has any actual evidence behind it. i highly recommend it as the only book to read on how to write code. and uncle bob, well, best to avoid his stuff as much as possible. clean code takes away about 5 years from every dev's life as they think they need to read it to become an intermediate developer and one they realize that is not the way, can they finally grow.
5 years is about right.

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.

In typical HN commenter smugness. It took me less than that to realise it was bullshit. It didn’t make things clear, it made them more abstract and more resistive to change. Similarly with DDD. Just build what you need and deal with the consequences of inevitable change later. No one cares if you miraculously perfectly modelled your “definitely the final form” of your domain from day 0.

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”

DDD is a good way to extract the business logic from the implementation.

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?

There's a huge difference between understanding the domain and modelling it as objects. Accounts and Balances have no behavior.
That book really poisons the mind. Even if there's some good things to learn in there, it's stashed among a lot of advice that is either plain bad or needs asterisks. But there aren't really any asterisks and instead it presents what look like rules that you shouldn't be breaking if you want to be a good programmer.

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.

> instead it presents what look like rules that you shouldn't be breaking if you want to be a good programmer.

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.

I always think of this as the programmer version of "No one got fired for choosing IBM." that was a common phrase about executives back in the day. Do the thing that you can just point to "experts" and blame them.
That is a helpful comparison. I guess it is risk aversion at the core. At some point risk aversion becomes abdication of decision making to others, which seems broken in roles that are specifically hired for making decisions, but that’s even more true of executives.
I completely agree with you, mind I feel the same way about the people who the original comment was talking about. They are paid the big bucks to decide how to spend money to optimize the company.
Which book are you referring to? The parent comment mentioned two books.
1. newbie follows the rules because he is told to

2. master follows the rules because he understands them

3. guru breaks the rules because they don't apply

There's a step between 1 and 2, someone who writes rules because he believes he understands them.

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)

"john ousterhout's book is the only book on how to write software that has any actual evidence behind it."

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.

Greg Wilson indeed is tremendously helpful in facilitating "the industry" to think about our craft:

https://github.com/gvwilson

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.

The annals of the IEEE and ACM would argue against:

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

It's striking to me how out of touch Martin seems to be with the realities of software engineering in this transcript. Stylistic refactors that induce performance regressions, extremely long and tortured method names for three-line methods, near-total animus towards comments ... regardless of who is right/wrong about what, those takes seem like sophomoric extremism at its worst, not reasoned pragmatism that can be applied to software development in the large.

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.

I don't know why people take UB seriously. He never provided proof of any work experience - he claims to have worked for just a single company that... never shipped any code into production. Even his code examples on GitHub are just snippets, not even a to-do app (well, I think that his style of "just one thing per function" works as a self-fulfilling prophecy).

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

Uncle Bob is one of the core contributors to Fitnesse, which had moderate success in the Java popularity era back in the day.

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?

> So if someone is 60+ year old, chances are that most of his work has never been open source

Somewhat ageist? I'm 72 and have produced a number of FOSS tools.

Truly. I know plenty of people in their 60s and 70s who use Git and are still very sharp programmers.
Using Git is unrelated to whether the software you write is proprietary or open-source.
The criticism was that UB worked at a company that allegedly didn’t ship code to production, not that he doesn’t have a corpus of open source projects on GitHub.
> So if someone is 60+ year old, chances are that most of his work has never been open sourced,

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.

I'm 72. As for what I have shipped over the half-century of my career, you can read all about that in part two of my book We, Programmers. Suffice it to say I've shipped a LOT of code.
I'm going to have to admit to never having read Clean Code. It's just never appealed to me. I did read some of UBs articles a fair number of years ago. They did make me think - which I'd say is a positive and along the lines you are putting forwards.

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 not familiar with the Clean Code book etc; my introduction is the article. UB seems to be advocating consistently for patterns that are not my cup of tea! For example: Functions sometimes make sense as 2-3 lines. Often 5-20. Less often, but not rarely, much more than that!

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.

Function length also depends on language. Every line of one language requires three line in another if the former has implicit error handling and the latter explicit. But I find the cognitive load of the two to be similar.

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!

The issue that of function length is irrelevant and incidental. Keep paying attention to what UB is saying.
I didn't get that impression from reading it. I also find the TDD approach discussed to be high inertia.
Another example of not quite pragmatic advice is Screaming Architecture. If you take some time to think about it, it’s actually not a good idea. One of the blog posts I’m working on is a counter argument to it.
I’d love for you to expand on this!
Short version: when designing new software, you don't have its architectural picture in the beginning. So when starting from scratch, the architecture shouldn't be screaming, but rather, it has to be non-committal/non-speculative to allow wiggle room for the future. (How to achieve non-committal architecture is the biggest topic I'm interested in, and I find 1 good tactic every few years). Specifically, the architecture should ephasize entry points and outputs. That's exactly what frameworks like Rails provide. You go by entry points until some sort of custom architecture starts emerging from the middle, which is when it can slowly begin "screaming" over time.
I read Clean Code when I started out my career and I think it was helpful for a time when I worked on a small team and we didn't really have any standards or care about maintainability but were getting to the point where it started mattering.

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.

> sheer rigidity

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

Thanks for the thoughtful response. I generally don't want to get into the specifics of what Martin advocates for. Whether to prefer or eschew comments, give methods a particular kind of names, accept a performance penalty for a refactor--those are all things that are good or bad in context.

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

> It's striking to me how out of touch Martin seems to be with the realities of software engineering in this transcript

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.

> It was always like that. And Fowler the same thing with his criticism of anemic domain model.

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.

"It's not object oriented programming" is only a good case to make if you think object oriented programming is synonomous with good. I don't think that's true. It's sometimes good, often not good.

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

Why would you not focus on writing OO code in an OO language for example? Would you start writing OO code in a functional langugage? No you wouldn't, because it would be pointless. There are programming paradigms for a reason
> Why would you not focus on writing OO code in an OO language for example?

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.

> Why would you not focus on writing OO code in an OO language for example?

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.

> Why would you not focus on writing OO code in an OO language

It should be the best solution to the problem direct whether or not use of OO is best, not the language.

> Why would you not focus on writing OO code in an OO language for example? Would you start writing OO code in a functional language? No you wouldn't, because it would be pointless. There are programming paradigms for a reason

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.

> "It's not object oriented programming" is only a good case to make if you think object oriented programming is synonomous with good. I don't think that's true. It's sometimes good, often not good.

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?

> Domain models are fundamentally an object-oriented programming concept

They are absolutely not. In fact, they are not even specific to even just programming, let alone OOP.

> Domain models are fundamentally an object-oriented programming concept.

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.

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

> I have better tools for doing that.

For example?

I would assume he meant to use the typesystem of his PL.
> 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.

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.

Aw, you described it so much nicer than me. I feel bad now.
We have a disagreement about the core of OOP. In English, a simple sentence like "The cat eats the rat" can be broken down as follows:

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

> So, imagine the sentence "the customer canceled the order."

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

> The customer actually asked for the order to be canceled.

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.

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

Go back and read what I said about subject nouns and object nouns. When converting OO concepts to English for non-programmers, it is indeed confusing to say “the order decided not to”—you say “the order couldn’t be” instead.

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.

> When converting OO concepts to English for non-programmers, it is indeed confusing to say “the order decided not to”—you say “the order couldn’t be” instead.

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.

> I highly recommend reading the two books I recommended

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.
mrkeen mentioned dependency inversion (DI). I think it makes sense in oop for an order to have a cancel method, but the selection of this method might be better as something configured with DI. This is because the caller might not be aware of everything involved, as well.

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.

> 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 comparison to English grammar is unnecessary. I didn't use it in my argument and you said it doesn't work that way either, so when you arrive at

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

I really don't understand this fixation on domain modelling. It looks like a lot of UML mixed with a "*DD" (life-pro tip: pretty much any X Driven Development is something experienced programmers rarely care about. You can borrow good ideas from almost any methodology without becoming obsessed with its primary subject. Being obsessed with the One True Way is a great way to waste a lot of brain cells). Also nobody sane touches UML. Or makes big official charts of classes and their relationships. It's a massive waste of time. You might come up with some core concepts and relationships, like a B-REP, but you don't need some jargon-heavy official way to do this.

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

> it's not object-oriented programming. That's it.

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)

I'll reply here with a very quick example why the anemic domain model is superior in general, no matter if you do OOP or anything else.

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.

Links to any existing articles on this topic would be greatly appreciated.
This is the original article from Fowler: https://martinfowler.com/bliki/AnemicDomainModel.html

By searching for that term, you'll easily find lots of other takes on the matter.

> I'll reply here with a very quick example why the anemic domain model is superior in general, no matter if you do OOP or anything else.

I can search for it of course but results that aren't about OOP purism appear to be rare.

I highly recommend a video if you don't mind the format: https://youtu.be/zHiWqnTWsn4?t=3134

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.

You made a great example here and I absolutely agree with you.

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)

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

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.

I would say that this code is not good (or to be more diplomatic: not optimal). Firstly, because between `order = orders.new()` and `order.canBeCreated()`, it's possible to insert other calls or actions on or with that order, which should actually not be allowed/possible.

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!

I haven't seen a lot of evidence that Martin really has the coding chops to speak as authoritatively as he does. I think when you become famous for giving advice or being an "expert", it can be difficult to humble yourself enough to learn new things. I know personally I've said a lot of dumb things about coding in the past; luckily none of those things were codified into a "classic" book.

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.

I watched some talks he gave 15 years ago and what struck me was that he would use analogies to things like physics that were just objectively incorrect. He was confidently talking about a subject he clearly didn't understand at even an undergraduate level.

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?

> I haven't seen a lot of evidence that Martin really has the coding chops to speak as authoritatively as he does

From what I can deduce, his major coding work was long in the past, and maybe in C++.

The Uncle Bob thing is something I'm experiencing right now.

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 don't see how it is uncle Bob's fault that your friend misunderstood his book.
Judging from this thread, it seems like a lot of people have similar issues with UB's work.
It might just be that the divide of a getting-things-done developer and a bloat developer isn't really caused by Uncle Bob but merely correlates with it. I.e, good developers also agree with Uncle Bob, though apparently with a different interpretation of what he said.
Smart people read a book and critically think about it. The others think it was written by a superhuman and turn everything into religious beliefs.
I meant that the book is interpreted differently by different people. (That no-one takes it as religion, but that some read it as recommending to "create a mountain of unnecessary abstractions", and others read it that "add necessary abstractions".)
Not so much UB himself, but a developer being told a book or person is authoritative / has the final word on a subject isn't healthy.
> It's striking to me how out of touch Martin seems to be with the realities of software engineering in this transcript. Stylistic refactors that induce performance regressions, extremely long and tortured method names for three-line methods, near-total animus towards comments ... regardless of who is right/wrong about what, those takes seem like sophomoric extremism at its worst, not reasoned pragmatism that can be applied to software development in the large.

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?

> 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

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

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

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.

> Do you need a comment to tell what a member function called postOrderMessageToEventBroker does?

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

> Do you need a comment to tell what a member function called postOrderMessageToEventBroker does?

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.

What I was refering to was the "why", not the "what" or "how". This is not a good function name to my eye, but YMMV: get-station-id-working-around-vendor-limitation-that-forces-us-to-route-the-call-through-an-intermediary-entity.

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

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.

Code like Uncle Bob suggests is not easier to read and understand, it is harder, IMO and that of many others. Since the disagreement starts from this any further discussion is impossible.
[flagged]
I wrote IMO for a reason. The issue with your statement is that you are implying I disagree the improvements in clean code aren't improvements. They are for the most part, but they are improvements of those particular examples, and do not generalize. The function size thing in particular is absolutely stupid. There are also different improvements that could be done.

In the real world optimizing for the text of the program is the wrong thing to optimize for. It doesn't matter if the text reads nicely if the behavior is wrong. Then what you want is code optimized for debugging, and code optimized for debugging wants to avoid jumping around since the more information you see in a single stack frame the better.

Similar issues with just OOP style code in general. Isolated state is nice, distributed isolated state is a nightmare. Porting that challenge over from distributed computing makes debugging the program harder, not easier, since you must now understand the history of communication between the objects to understand how a certain global state was reached in aggregate.

Contrast that with a sequence of steps operating in that larger state directly, it's way easier to follow the logic since it is explicitly written down in a single place.

Comments are also much better than convoluted method names. Why comments I think even Bob would agree are important, but How comments are extremely useful. Consider python libraries that write out example code in the documentation string with the outputs (that gets turned into automatic tests no less). Does the method name matter that much then? Not really.

Consider also APL and its fans. While I'm not a fan the proponents make a good point why they like it: you can see much more of your program in one go and sequences of symbols form words with precise meaning.

Basically, mathematical notation and Kanji rolled into one. How does that fit into Bob's Clean Code approach?

> Shit-talking while hand-waving adds nothing to the discussion.

This is not presuming good faith, as he guidelines ask that we do.

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

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.

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

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?

I was quoting you, where you said readability takes precedence over technical concerns. That‘s what I‘m challenging.

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.

I'm not going to spend a long time responding to your comment, since it seems accusatory and rude; if you modify it to be more substantive I'll happily engage more.

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

> What machines do with it is of lower priority. Why? Because a programmer's time is far more expensive than any infrastructure cost.

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.

> 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

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

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

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.

I can just tell that John Ousterhout works with much better developers on average than UB and that probably informs their biases.
He also works with a lot more students, with student-sized projects and problems and code lifetimes. He's used his book for classes, I think it's on a level appropriate for a freshman.

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.

What's there to dislike so much in APOSD?

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.

Here's mostly from what I wrote down after reading it. Indeed, the "reasonableness" is part of the problem.

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.

What definition of complexity in the context of discussing software development and architecture would you put forward instead?
Complexity: things twisted together.

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

I don't see that that is markedly different in function from the definition provided by Ousterhout, or at least both seem to describe to me the same concept, just using different words/terms/analogies.

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

> Presumably twisting makes things harder to understand and having more things requires a greater effort at understanding?

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.

I am unfamiliar with your usage of "twist" in the sense you seem to be using as it relates to complexity --- the APoSD definition seemed far easier for me to understand at least.

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.

He could have chosen a different word besides "complexity" but I don't really think it would have affected much. The subjective easy/hard is specifically what Ousterhout is trying to talk about.

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?

If you dismiss all the parts of APOSD that you agree with as straightforward and trivial, then obviously the only parts left for consideration are the parts you disagree with. APOSD is not an academic paper, it does not claim to be wholly and truly original. You are presumably an expert programmer, so it makes complete sense that much of the content discussed in APOSD appears to be "straightforward platitudes". To you, the content is trivial and obvious. But to the new grad with one year of work experience, the content is novel and informative. Perhaps you should take your own advice and consider the context.
I don't think it's fair to dismiss criticism because of skill. I said I think it's a bad book, and my most disliked, but it's not worthless, and if it's all someone has, they can indeed learn things from it even if they won't learn very much per page. However, there are many other books available, and by my own opinion all of them that I've read are superior. Any value you'd get from APOSD, you'd get from any book aimed at or inclusive of a similar audience, and the other book would give even more value that's absent from APOSD. (As another example, I was introduced to The Pragmatic Programmer in college. I believe it can serve the role of APOSD just fine but I never liked it enough to finish it, so perhaps I'd rank it lower if I did.) I also think you'd get most of the value just by writing more programs.

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 communi­cation, 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 applica­tions. 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 m...

While reading APoSD, one of my thoughts was that it walks up to, but never gets to the point of advocating for Literate Programming, and that resolving how the author feels about that presentation would make for a better and clearer text.

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.

There is an important case for comments that neither of them touched on. Sometimes you are dealing with bugs or counterintuitive processes beyond your control.

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

While I am not a Uncle Bob-style "no comments"er I do love a ridiculous method name. I pay very close attention to that method and the context in which it is called because, well, it must be doing something very weird to deserve a name length like that.
There are a few Haskell functions with names like reallyUnsafePtrEquality# or accursedUnutterablePerformIO, and you know something interesting is going on :P
That’s exactly why you should save that length only for a method that’s indeed doing something weird. If every method is long, the codebase turns into noise. (IOW I agree)
This doesn’t happen in reality. Your program does so many things that practically speaking short names work for a lot of functions in the program. It’s like English. There are big words and there are small words and usually to communicate a combination of big and small words are used.

Nobody practically communicates with big words. A long function name only pops up when needed.

It happened in the article we’re discussing, and seems to be something Robert advocates for.
Happened in a contrived example.
Contrived perhaps, but it was an example that was supposed to demonstrate the benefits to those long names. In my opinion, it did the opposite.
I used to work this way, but I found that every non-trivial method involves edge-cases and workarounds documenting them the method name destroyed readability.
The only point I was making is that short name signals that nothing unusual is going on, and long name signals that you have to pay extra attention. I never suggest to replace a comment with a long name. Here's my 4 reasons to leave a comment: https://max.engineer/reasons-to-leave-comment Comments are crucial for "why", and additional context. Name shouldn't go beyond "what". More on that here: https://max.engineer/maintainable-code (Check the "What" section that focuses on naming).
> The only way around that would be with stupid method names

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.

> Yep. Method names make terrible comments. No spaces, hard to visually parse, and that's before acronyms and ambiguity enter the conversation.

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 will completely forget why I wrote it that way.

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.

And the described use case - USB stuff with very specific exception - makes a strong case for literate programming, that is, more prose than code.
Does everything have to be pushed into a structure-prescribing set of rules?

Can't we just say "comments are useful here" without trying to make it into a case for $methodology?

Literate programming isn't a structure-prescribing set of rules or a methodology. It's just making the prose primary and the code secondary.
Why not put the prose in the name of the function?
Function names are limited. E.g. can't provide a circuit diagram of what you're controlling in a function name. But you can do that in a comment (either with ASCII art or an image link).
Agreed. So why not stuff as much as possible into the name before resorting to a comment? Prose looks ugly as a name but the utility is not diminished.
That embeds the "why" into your API. If it ever changes, the function no longer serves as an abstraction over that underlying reason & changing the function name breaks your API.

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

In addition to that, if the Why ever changes (maybe the issue was in an external dependency that finally got patched), you'd have to update the name or else leave it incorrect. Mildly annoying if just in one codebase, but a needlessly breaking change if that function is exported.
Sounds like you should instead be making these invalid states unrepresentable by encoding them in types and/or adding assertions. Especially if you're exposing them as interfaces, as your example function names would imply.
They're invalid states inside the USB device, not inside the driver code. So nothing you do to the driver code can make them unrepresentable. The best you can do is avoid frobbing the device in the problematic ways.
The GP is making those invalid states unreachable by writing a device driver.
(comment deleted)
> 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.

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

Have you thought about distilling your hard-earned information about the device's behavior into a simulator for the device you could test your code against?
I don’t see anything wrong with those names. A bit hard to parse but the name moves with the function call while a comment does not.

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.

Encoding temporal dependencies (or exclusions) between methods is hard. You can get partially there by using something like a typestate pattern (common in rust).
I strongly recommend "A Philosophy of Software Design". It basically boils down to measuring the quality of an abstraction by the ratio of the complexity it contains vs the complexity of the interface. Or at least, that's the rule of thumb I came away with, and it's incredible how far that heuristic takes you. I'm constantly thinking about my software design in these terms now, and it's hugely helpful.

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.

Implicitly, IIRC, the optimal ratio is 5-20:1. Your interface must cover 5-20 cases for it have value. Any fewer, the additional abstraction is unneeded complexity. Any more, and your abstraction is likely too broad to be useful/understandable. The example he gives specifically was considering the number of subclasses in a hierarchy.

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.

This is a good rule of thumb, but what would be a good response to have interfaces because, "what if a new scenario comes up in the future"?
"We'll extract interfaces as and when we need them - and when we know what the requirements are we'll be more able to design interfaces that fit them. Extracting them now is premature, unless we really don't have any other feature work to be doing?"
The scenario NEVER comes up in the future as it was originally expected. You'll end up having to remove and refactor a lot of code. Abstractions are useful only used sparingly and when they don't account for handling something that doesn't even exist yet.
If you own the code base, refactor. It's true that, if you're offering a stable interface to users whose code you can't edit, you need to plan carefully for backward compatibility.
When doing the initial design start in the middle of the complexity to abstraction budget. If you have 100 “units of complexity” (lines of code, conditions, states, classes, use cases, whatever) try to find 10 subdivisions of 10 units each. Rarely, you’ll have a one-off. Sometimes, you’ll end up with more than 20 in a group. Mostly, you should have 5-20 groups of 5-20 units.

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.

Even one use case may be enough e.g., if one class accepts another then a protocol (using Python parlance) SupportsSomething could be used to decouple two classes, to carve out the exact boundary. The protocol may be used for creating a test double (a fake) too.
Maybe some examples would clarify your intent, because all the candidate interpretations I can think of are absurd.

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.

Think of it more like a “complexity distribution.”

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.

And with respect to function behavior, I’d view it through the lens of cyclomatic complexity.

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.

That's not what cyclomatic complexity is, and if you think 5–20 test cases is enough for sin(), open(), or Lisp EVAL, you need your head examined.
You’re right, I suggested two different dimensions of complexity there as a lens into how much complexity a function contains. But I think the principle holds for either dimension.

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?

Yes, open() is a good API. I can't believe you're asking that question! It's close to the Platonic ideal of a good API; not that it couldn't have been designed better, but almost no interface in the software world comes close to providing as much functionality with as little interface complexity, or serving so many different callers or so many different callees. Maybe TCP/IP, HTTP, JSON, and SQL compete along some of these axes, but not much else.

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.

That function covers 2⁶⁴ inputs, not cases. It handles only one case: converting an angular value to (half of) a cartesian coordinate.
Sounds like you haven't ever tried to implement it. But if the "case" you're thinking of is the "case" narnarpapadaddy was referring to, that takes us to their clause, "Any fewer [cases], the additional abstraction is unneeded complexity." This is obviously absurd when we're talking about the sin() function. Therefore, that can't possibly have been their intended meaning.
The alternative and more charitable interpretation, of course, is that a single function like sin() is not what said GP meant when using the word "interface". But hey, don't let me interrupt your tilting at straw men, you're doing a great job.
Appreciate the charitable interpretation. Both “complexity“ and “abstraction” take many different forms in software, and exceptions to the rule-of-thumb abound so it’s easy to come up with counter examples. Regardless, thinking in terms of complexity ratios has been a useful perspective for me. :)

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.

Uncle Bob's insistence that functions should be 2-4 lines long is baffling to me. I don't understand how he can be taken seriously. Is there a single application in the entire world with substantial functionality that conforms to this rule?
There are. A lot of Java code bases look like this.

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.

It's always fun stepping through 412 stack frames that are all 2-line long methods to figure out where the thing you're interested in actually happened.
I once fully spelunked such a Java call stack to convert some code to golang. It was amazing, there were like 5 layers of indirection over some code that actually did what I want, but I had to fully trace it keeping arguments from the full call stack in mind to figure this out, because several of the layers of indirection had the potential of doing substantially more work with much more complex dependency graphs. I ended up with a single go file with two functions (one that reproduced the actually interesting Java code and one that called it the way it would have been called across all the layers of indirection. It was less than 100 lines and _much_ easier to understand.
Add in Go and C++ code bases to that as well.
I've seen this in what I call 'lasagna code' - multiple thin layers that seem to do nothing (or almost nothing) but each one is an implementation of some abstruse interface that exists in the mind of the original developer.

Eventually, your code has to do something. Get that thing in one place where you can look at it in its whole.

Too often I see functions that are shells that reshuffle the arguments and pass them to another function, which also reshuffles the arguments and forwards them to another, and on and on. One was 11 layers deep.
And a lot of people doesn't understand how dangerous shuffling parameters is, especially in languages that do not have named parameters...
Powershell is an awful language but it has made me fall in love with named parameters. Name all the things.
If you have a 2-4 line function and you spot this, you can trivially remove it.
I have seen such code too - with just S,K and I combinators. It wasn't readable.
This is something that's easier to read than it is to understand. A lot of languages force you to do quite a lot in a function and becoming blind to bloat is way to easy to do. (C++/Go/Java/etc yep).

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.

Like with a lot of his approach, its great for teaching people better coding skills in an educational setting, but doesn't make as much sense in the real world.
Yes, I've worked on a couple of codebases like that. It's glorious, you break everything down little by little and every step makes sense and can be tested individually. Best jobs I've had.
But are those steps actually doing anything that can be tested? My experience with these sorts of codebases was always that most of the functions aren't doing much other than calling other functions, and therefore testing those functions ends up either with testing exactly the same behaviour in several places, or mocking so heavily as to make the test pointless.

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.

> testing exactly the same behaviour in several places

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.

These large compound statements look nice if they are perfect, but when you make giant expressions without intermediate variables, it is much more difficult to test.

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.

Yes. This works but only if the functions are pure and using pure function composition.

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` is seven lines long though.
Then split it. All pure functions are easily decomposed into the most primitive units so even the most dogmatic ass hole can't talk shit.

   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.

Well, the most interesting thing about purely functional composition would be the ability to un(de)compose them, inlining the bodies until the resulting function is large enough to be worth the effort of reading it.
Exactly you have the lowest level primitives. You can arbitrarily compose them however you want forming arbitrary layers of abstraction from a tree of compositions.

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.

Based on his blog, Martin has been getting into Clojure in recent years. I was kind of hoping that the experience with a functional lisp would shift some of opinions that he previously stood by in Clean Code, but based on this discussion, it doesn't seem like it.
John Carmack would disagree with Uncle Bob and John Carmack actually programs.

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.

The problem with collapsing is that you need to know a priori which sub-scopes are independent and hence collapsible and which aren’t. Meaning, you have to analyze the unfamiliar code first in order to know which sub-scopes you might want to collapse. And, given an already-collapsed sub-scope, due to it being collapsed you can’t see if it reads or mutates some local variable. 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).

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.

you need to know a priori which sub-scopes are independent and hence collapsible and which aren’t.

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.

It is a bit weird.

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.

> median line count of methods

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

Not part of smalltalk.
fyi the Cincom Smalltalk IDE New Class dialog shows these fields and checkboxes—

     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

You should be baffled, because I never presented that as a hard and fast rule.
I’ve come full-circle back to my junior engineer attitude with respect to coding “best practices”: Avoid anything resembling dogma.
I've enjoyed both books but Uncle Bob is something you grow out of. He was a bit of a cult figure at the time. Trying to actually follow the guidelines in Clean Code taught me a lot about "over-decomposition" and, ultimately, how not to write code. It reminds me it's possible to take aesthetics so far the results become ugly. Fussing over a proliferation of small functions that do only one thing is a kind of madness. Each individual function eventually does zero things. You are left sifting through the ashes of your program wondering "Where did I go wrong?"

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.

> If you want to write good code, read good code.

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.

You need to know what is good code. Opinions may vary a lot between programmers, even senior ones. The Clean Code cult would tell you to find good code there but that is the most poisonous programming book I have read.
there are lots of very robust programs in various languages to learn from. It would be hard to know in isolation but by contrast it is easier to learn what good code looks like. Some code will flow and be easy to read. Other code will be obtuse. Start with simpler projects that don't involve a lot of low-level calls. Work up to more complex implementations. There was never a better time to read code than now with an LLM as a tutor. If you use one of the AI-integrated editors or a code packer you can provide a lot of context to the LLM. Ask the LLM for important modules and work your way through them. Ask it for alternative implementations of a function or translated into a different language. Set up an program in a running environment and walk through using a debugger. Look at the way the code is organized in files and modules. You will inevitably encounter cruft and "bad code". Sometimes there are good reasons for that too. If you prefer books, the Architectures of Open Source Applications (AOSA) books are interesting, but there really isn't a way to avoid pulling down a repo and reading the code. Soon, you'll develop your own taste for what makes sense to you and be able to think independently about the choices the developer made.

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.

Forget about the code itself and focus on the results.

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.

Not really, long-living projects don't adapt their complete code base with gained experience, much like the Linux Kernel probably will never be rewritten in Rust, C++ projects never transformed to C++14+, etc.
The interesting thing to look for here is the parts of the codebase that don't need to adapt with gained experience. That's the key. If people aren't changing it, they haven't needed to, and that's a useful signal.

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.

It can also mean "We have not changed this because we don't dare to do that, or it is too much work and we just have to live with the bad decisions made 25 years ago". And that is the last code you want to copy.
It is, but those cases tend to be obvious.
It’s interesting that we just assume a newer language produces “better code”
No, because then you end up reading old C-code that are IFDEF mazes and think that is good code. No, to see good code you usually have to look at what experienced people write when they get to greenfield something new.
So you think code that is ugly is bad code? Or is it that it uses janky/outdated features? What makes it good if not its lifetime value?

Surely you don’t just presume the quality of code based on the person who wrote the code, right?

What makes it good is the tradeoff between how well it solves the problem compared to how easy it is to maintain. And people learn how to write better code as they get more experienced, but old projects are seldom rewritten using the learnings - it is often just easier to start over from scratch.
You have to read a lot of different code. Everyone thinks their code is good when they write it. Often old ugly code has a beautiful design still hidden behind many many requirements changes that didn't fit with the original design. Other code looks nice and beautiful but it will stand the test of requirement changes even worse than the other.
> Forget about the code itself and focus on the results.

This reminds me of what Dijkstra said (paraphrasing): the computation is the important thing, not the code.

Agreed.

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.

Every if statement is a chance of a bug because the code has two or more paths to follow

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

I often abbreviate that to "Psychosomatic Complexity" because more complex code is likely to give the programmer a headache.
Just a point here, "good code" is sometimes subjective, and depends on understanding the context of what the code is doing. What you think is good code might be overly verbose to another person, or overly terse, or have poorly named variables, or not have sufficiently conservative guard clauses, or throw insufficiently-granular exceptions. What you think is confusing code might lack context for where it is in the stack and what problems it needs to solve at that layer of the stack.

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.

https://qntm.org/clean

^ 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/

>Each individual function eventually does zero things.

Lambda calculus, basically :)

> You are left sifting through the ashes of your program wondering "Where did I go wrong?"

Brilliantly phrased metaphor, thank you.

it has been years since I read the book, but I'm surprised that there's so much hatred for it here. From memory it seemed like fairly harmless things like give things good names, try to make the code readable, dont comment what the code does but why, use consistent formatting, avoid duplication.

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?

Please read it again, you might see it differently now.
One method of commenting that has paid off for me the most was inserting links to:

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.

I also fixed the disassembler to also add a clickable link to the instruction spec page for each instruction.
I did that back in the day (before we could click on anything) about what the compiler was thinking as it generated the code. That was fun.
For anyone like me who at first skipped over this article because it seems from the title that someone just compared two approaches:

No, it’s an actual debate between the actual John and Bob. Them debating each other. It’s an amazing read.

I find the lack of discussion of type systems really surprising in these sorts of discussions and books. Effective use of type systems is a killer factor for me for creating clean, safe, readable and maintainable software designs.

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.

That was exactly the approach taken by Prof. Ousterhout in setting up the class which lead to this book --- rather than just having students turn in working code for a grade, the code is reviewed with the student and the student then works to make it better --- in turn, the 2nd edition of the book was informed by the experience of teaching the class and the author actually changed his position based on the experience gained.
(comment deleted)
Why is that convincing though? Students aren't experienced coders, aren't working in large teams, and student assignments aren't like long-term large commercial projects.

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

It's better credentials and experiential basis than most other programming books.

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.

> It's better credentials and experiential basis than most other programming books.

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.

Thanks.

At a quick glance, that book seems to align well with APoSD --- what aspects do you believe run counter to it?

The books aren't that similar I found. The Google one is more about practical tips on the dev process of scaling and maintaining software in teams, rather than focused on code.
Okay.

What book that covers a similar range of topics as APoSD would you suggest?

Because the pendulum of typing hadn't swung back to static being in vogue when the Philosophy of Software Design came out. At the time you had mostly the Scala & Haskell people standing in a corner screaming until they (well, we as I was one of them) were blue in the face about reducing "certain types of bugs", and making impossible states impossible.

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.

(comment deleted)
Type systems and type-based coding patterns are very hip right now, but they weren't 6 years ago. That is partly because the type systems in the main languages in use 6 years ago were hack jobs (to put it politely).

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.

"That's a valid concern. However, it is tempered by the fact that the functions are presented in the order they are called. Thus we can expect that the reader has already seen the main loop and understands that candidate increases by two each iteration."

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

Bob's comments on... commenting.. are so bizarre that I can't help but think that he just refuses to concede the point rather than admit he might have been wrong about it. Like, the paranoia around incorrect/stale comments is fairly absurd, I've been coding for 20 years across many code bases, and I can't even recall a time when I've been significantly mislead by a comment which caused a significant waste of time. However, the amount of time I've wasted on unclear code that has zero comments is absolutely staggering. However, what really sealed the weirdness to me was his argument that this was somehow a good comment:

                                                                    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.
Lol, this reminded me of those engravings we put on spacecraft. Like, if we had to communicate the algorithm to an alien civilization then sure, this might be the best way to do it!

https://en.wikipedia.org/wiki/Pioneer_plaque

The juxtaposition of how the original comment starts, then the appearance of the verbatim “good comment”, then the spacecraft engravings made me laugh tears.

Especially the buildup from how bizarre the understanding of UB of comments is to actually seeing one “in the wild”.

> I'm not unfairly clipping context away from it

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. I guess what I meant is there wasn't any additional meat to the _comment_, you had to read the code to know what the comment even meant
That's the point of comments right? Comments are not supposed to be stand alone, they exist to provide additional information that the code cannot express fully, but no more than necessary that it becomes a noise. It's all about balance.

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

Once you notice that the primes are in the top rows, it becomes a pretty good comment.
Just learned that 9, 15, 21, ... are primes. Excellent comment ^^.
well, ackchyually, 'technically' the primes _are_ in there in that top row

just not only primes ^^

> so bizarre ... what the hell is that supposed to tell someone?!

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'm pretty much in agreement with you on this, however I'm always aware of the possibility of comments being bugs. If code gets moved around, there is the very real possibility that the comment is now attached to the wrong method or line of code.

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.

The how should be, but the why might not be. I think comments should explain choices, especially ones that I am likely to question when returning to the code later.
Actually, I presented that figure as an example of just how difficult that problem was to understand, and how (on a bike ride) I was finally able to visualize what was going on. The ascii image was presented a bit tongue in cheek.

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.

They are both mostly right but the devil is in the details and to try to not get too dogmatic about things. For example function length is one of those things that you can obsess about and debate endlessly.

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.

I am biased ( a former coworker was an Uncle Bob fan, and was bent on doing everything by the book, with layers of abstraction, patterns, hexagonal architecture, lots of unit tests, no cutting corners, even as we did not know what exactly we want to build and needed an MVP ASAP) but I'll just say this: Ousterhout wrote TCL - widely considered one of the best C codebases - besides being a professor at Standford and having other software achievements under their belt, while Robert Martin is more like a software technology evangelist. The former good at actual deliverables the latter good at selling.

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.

Let's not forget that Uncle Bob, by the time of writing "Clean Code" had 4 decades coding experience.
My middle school English teacher had 4 decades of experience writing. What she wrote was lesson plans. That doesn't make her Stephen King.
Do not make the mistake of the craftsman who claims to have 20 years of experience, but in truth only has 1 year of experience repeated 20 times.
Are there large code bases that he has written that we know anything about?
Software is results driven, there's no value in simply warming a seat for X YOE, talking about code instead of actually executing.
Kenneth Copeland has been a pastor for 50 years and his theology and pastoral practice is still terrible. Years of experience is not a useful metric when you could instead look at results.
Biased against the approach of your former coworker and thus the "Clean Code" way? I assume it did not work out well, because you needed to move fast to build an MVP before trying to do it right?
yes, biased against knowing 'the best way do write software' and applying it regardless of what the current requirements and constraints are. And arguing for their position by sending people links to Uncle Bob videos for 'enlightenment'.
Following Clean Code is not the right way to develop software in any stage of the project. It is a few opinions of someone who has not actually written any code of substance. In addition to Clean Code being a bad approach, it can also be a very slow process.
The example they use is irrelevant. A solved problem can be written how ever one likes.

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.

All great, but generaly useless in most big corp project with offshoring, where we are already happy that we actually delivered something that works in first place.
This was a fun read. I read APoSD for the first time a couple of months ago and found myself nodding enthusiastically as I read. I have a few quibbles, of course, but overall it matches my experience in how to write software that is correct, maintainable, extensible, and understandable.

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.

[1]: https://qntm.org/clean

buried at the end: there's a planned second edition of Clean Code! Given Bob's intransigence in this conversation, I wonder what he'll change.
Cleanliness AND Design is Highly correlated with IO, side effects and state.

Most programmers know about this in 2025 but they didn't back then. Looks like the authors don't even mention it.

I find it funny how much people obsess over Clean Code. In my opinion Robert Martin's Clean Architecture is a much more valuable and realistic idea than all this madness about 3 line functions, no comments, do one thing, etc. I would take the ugliest code that followed Clean Architecture over any "Clean Code" that didn't bother sensibly separating business logic and I/O.

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.