Rendered at 09:21:02 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
Aurornis 18 hours ago [-]
I consider Clean Code to be in the category of books/styles that is helpful for early developers who need some structure, but harmful to late-stage developers who adopt it as dogma.
On a long enough career path, eventually you will run into one Clean Code zealot who carries an air of superiority and nit picks every PR over things like a function having more than an arbitrary number of lines in it instead of reviewing the actual code. This is the point where most people come to hate Clean Code.
wasmperson 16 hours ago [-]
> I consider Clean Code to be in the category of books/styles that is helpful for early developers who need some structure
Clean Code is unhelpful to beginners too, though: misuse of industry-standard terms, shunning of comments in favor of tiny functions with long names, shunning function arguments in favor of mutating state, polymorphism obsession, etc. So much of the concrete advice the book gives is just plain bad.
The reason people get more pissed off at Clean Code than they would at any other book that gives bad advice is the preachy and authoritative tone it uses. It frames people who don't do "Clean Code" as unprofessional and lazy, and this framing is very convincing to some people, as evidenced by some of the replies in this thread.
recursivecaveat 3 hours ago [-]
The tone really is genius marketing though. Like who wants to be a "dirty" coder? Reminds me of that quote about the etymology of dynamic programming:
> it's impossible to use the word dynamic in a pejorative sense. Try thinking of some combination that will possibly give it a pejorative meaning. It's impossible. Thus, I thought dynamic programming was a good name. It was something not even a Congressman could object to. So I used it as an umbrella for my activities.
0x20cowboy 47 minutes ago [-]
Which would be fine if these people weren't the ones making / influencing the hiring decisions, paying them half a million dollars (even though they have done nothing) and people retweeted their “opinionated” takes on subjects they barely understand.
BeetleB 13 hours ago [-]
> but harmful to late-stage developers who adopt it as dogma.
Even Robert Martin, very often in his videos and blogs, espouses "engineering judgment" and is quite fine abandoning advice in his book when the situation calls for it.
If performance is important and clean code is impacting it, he won't object to your breaking the rules.
It's mostly with his TDD evangelism that he goes (a little) crazy.
josephg 11 hours ago [-]
I just think - in the book at least - he has really bad taste. The examples are full of “spooky action at a distance”. He writes the exact sort of class functions that make a lot of OO terrible to work with, where a function that should be pure has some weird hidden side effects. Code like that is really difficult to reason about and impossible to reuse safely. Somewhat ironic, given what he preaches in the rest of the book.
I really like that he brings attention to the idea that code can be beautiful. But judging by his examples, he just seems to have no idea how to actually write beautiful software himself.
I wish he went off and learned some functional programming. Haskell, Erlang, Clojure, F#. Something like that. Really go deep. If you want to know what really beautiful software looks like, that’s the place. Those communities know beauty.
Scubabear68 10 hours ago [-]
The ugly reality is consultants like Martin have little production coding experience. Martin has posts going back to the early 90s showing he had little understanding of how software teams work and deliver value.
From what I have seen, his ideas were formed in a vacuum divorced from real coding. You can see it in the small amounts of open source he has released. The kind of guy who will always prefer 50 classes to 5. Who clutches his pearls at an if statement.
I remember he was invited to give a talk at Bloomberg and the majority of us were simply disgusted that an obvious hack like him was lecturing a group of software engineers who had really been there and done that.
bheadmaster 3 hours ago [-]
Those who can't do, teach.
nixon_why69 2 hours ago [-]
I hate that phrase. There are billions of people who can't code and also aren't teaching it. And plenty of people who are good at something and enjoy giving back and sharing in the form of teaching.
saidnooneever 5 hours ago [-]
your opinion is no less valid than his. you see the problem if you use the word clean or beautiful. its opinions. so people will debate it till no end. it matters nothing at all.
josephg 1 hours ago [-]
I don't think beauty in code is entirely subjective. Beautiful code is like beautiful mathematics. Simple, clean, and correct by construction. All at once. There's plenty of ways to implement a graphics engine, web browser, database or OS kernel. Some of them are simpler, faster and more correct. And some approaches will inexorably lead you to a bug ridden mess.
You see this clearly if you ever teach. I would give all my students the same spec. Some students submitted small, simple programs which passed all the tests with flying colours. Some students would submit huge programs which barely worked.
As Alan Kay said once, the right point of view (on a problem) is worth 50 IQ points. At the margins there's subjectivity and tradeoffs. But lots of programs are more or - more often - much less beautiful.
pseudony 10 hours ago [-]
You mean Clojure, and he did. He is very enthusiastic about it, vastly preferring it to Java.
Fyi.
josephg 9 hours ago [-]
Fixed! And I'm glad to hear it. If his blog is anything to go by, it looks like he was messing around in clojure in about 2019, 12 years after he wrote Clean Code.
Maybe we'll see an updated version some day, with better examples. It would be valuable if only so people would stop trying to defend his old, bad advice.
Jtsummers 9 hours ago [-]
It was updated and the second edition was published last year. I have not read it so have no idea what changed and what did not.
BeetleB 11 hours ago [-]
Yeah - I mostly watched his videos (the first N of them), and liked it. But later I saw examples from his book and they were terrible.
giancarlostoro 16 hours ago [-]
The function size is one rule from Clean Code I disagree with, it's silly. I love helper methods, but use them to a reasonable standard. I'd argue, if you cannot see it all on a 1080p monitor, that it might be getting a bit too long. I read PEP-8 religiously before I learned about "Clean Code" and it helped me to have sane standards in general. Methods that are roughly under 100 lines of code are okay, better is to fit it all in your monitor, 1080p being probably the most common resolution that leaves you with roughly 50 to 60 lines of code. If you have to scroll, you might want to consider helper functions to simplify and shorten logic.
Functions always being under 10 lines just means you've got functions everywhere, which can be mentally exhausting to follow logic, if you aim for like 40 lines tops you can write better "stories" with your code that are easier to follow and more expressive.
wren6991 13 hours ago [-]
I'm fine with a 1k-line function if you just have that many things to do in a row without taking a breath. Breaking it up into smaller functions feels neater when you write it, but when I read it I'm essentially just macro-expanding it in my brain into the original 1k linear version, and that has some cognitive overhead (especially when they end up misordered in the file, or split across different files).
I also have to think about whether there are any other areas of the code that might be calling your helpers, and whether they might break if I change the helper. Seeing everything inlined makes it absolutely clear from local reading that modifying the code only affects the local functionality.
I'm not saying go insane and copy/paste the same thing multiple times, just that 1k-line functions are sometimes the least of all evils. I liked what John Carmack had to say on this topic: http://number-none.com/blow/john_carmack_on_inlined_code.htm...
Another point from that post that I try to take to heart: if it can be a pure function, it should be a pure function (even in C). Bob Martin's style is the opposite.
nomel 9 hours ago [-]
I've heard this called "lasagna code".
I've definitely drifted towards longer code, if nothing in that function is used elsewhere. Some tasks really are just lists of things to do (especially in something like image processing), and I think that sometimes it doesn't make sense to put lists of lists in your list of things to do.
xnzakg 2 hours ago [-]
But in many cases, you could split it up into smaller, `inline` functions.
Gibbon1 10 minutes ago [-]
I keep coming back to the idea he's advocating using long function names as a substitute for comments.
Instead of // do the thing followed by 5 lines of code to do the thing. You're supposed to put those five lines in a function named DoTheThing(). That doesn't seem better to me.
mattmanser 25 minutes ago [-]
50-60 lines? You must have tiny text size, on mine, with VS Code, I can see 40 lines. As you get past 30 and your eyesight starts changing, you need to up it. By 45 you need it quite a bit bigger than you used to have it. In the mid-2000s there used to be a lot of articles reminding the mainly young developers that 11-12px font size was unreadable to anyone past 40.
On top of that, the default place for a console in VS Code is at the bottom, now I'm down to 25-30 lines if I have that open.
And then there's heavier IDEs like IntelliJ, Visual studio, etc.
They might have two or three tab lines at the top, a row used for class navigation (that I've just realized I've never, ever used), an extra row or two of quick action bars, a row of extra tabs at the bottom for navigating between consoles/call stacks/locals/error lists, 2 info bar rows at the bottom, and sometimes two or even three scroll bars stacked on top of each other.
All of a sudden you're down from 30 lines visible to 17, which is my actual visible lines of code when I have debugging running.
98codes 14 hours ago [-]
Adopting anything as blind dogma is Expert Beginner territory. See also: DB table normalization.
ericmcer 6 hours ago [-]
Sometimes it feels like the more someone has memorized all the little acronyms and jargon and principals around programming the worse they are as an engineer.
I worked with a guy who would constantly drop niche jargon and quotes from famous engineers and then kind of smugly look at you. He could not write a function without proudly saying what principals it was following. He was a horrible programmer, ended up getting laid off.
cavoirom 17 hours ago [-]
I learned Clean Code at the beginning of my career, but I don't actually get it. Recently I know about "testable code" from Justin Searls. I found the "testable code" concept is more useful because we can monitor the effectiveness of the concept and I can see the actual benefits in my projects.
MrBuddyCasino 17 hours ago [-]
Well said. It is not without merit, but tends to attract the tedious killjoys and midwits.
The bureaucrats who above all value process over outcome.
wing-_-nuts 16 hours ago [-]
My personal benchmark for 'maybe this function is too long' is when it doesn't fit on the page.
jeltz 16 hours ago [-]
I don't think any such benchmark should exist. As long as the function only does one thing there is no upper limit. Artificially splitting a large function only reduces readsbility. What would you name the parts? do_stuff1(), do_stuff2(), do_stuff3()?
I have seen very clean codebases with a handful very long functions, but they were no issue she nice they only did one thing.
I personally write quite short functions but I have never understood why people take issue with large functions. Those are one of the easiest things to fix in a bad codebase. It is much harder to clean up after someone who used too small functions.
Someone 2 hours ago [-]
> I don’t think any such benchmark should exist.
wing-_-nuts says (emphasis added) “My personal benchmark for 'MAYBE this function is too long' is when it doesn't fit on the page.”.
I think that’s a fine heuristic. Long functions can be fine, but longer functions tend to be less testable, so you should try to avoid them.
On the other hand, it can take lots of thinking to properly decompose functionality, and that decomposition can easily change when requirements change, so spending that time may not be worth it.
tcfhgj 16 hours ago [-]
> What would you name the parts? do_stuff1(), do_stuff2(), do_stuff3()?
depends on what the function does, most likely the best decomposition into functions isn't simply splitting the function in to n sequential parts
> I have seen very clean codebases with a handful very long functions, but they were no issue she nice they only did one thing.
one thing usually consists of multiple other things
imho length should correlate negatively with cyclomatic complexity - it's ok if you write 300 locs if all you do is fill a map with trivial entries
azertify 16 hours ago [-]
There aren't usually many domains where a process can't be described as a series of steps. Deciding what those are called, and structuring data in such a way that it each of those steps works sensibly can be challenging, but that is the process of making code comprehensible.
I can remember as a novice that I would write an entire program in a single, many-thousand line function, unable to see where the boundaries between functions should be. With experience and expertise in the domain, it becomes easier to see where those should be.
wpm 16 hours ago [-]
And that's why my monitor is 2560x2880.
Brian_K_White 12 hours ago [-]
A smaller chunk that no one else calls should just be included. The break up is actually hurting undertsanding and maintainability.
Except the only real rule is that rules are wrong.
All there are is different and actually contradictory pressures for different and actually contradictory priorities that are all true and valid at the same time even though many contrdict. The correct thing in each given moment is whatever makes the shortest rubber band lines between all priorities.
Sometimes that will be a very large single function even if some other times that will be a bunch of 10 liners.
dirtbag__dad 9 hours ago [-]
I think you’ve missed the point of clean code if this is your gripe with it.
Every time you over scope a function signature, because you want to handle that other case, you add mental tax to the next person. This accumulates, burns time, and now confuses agents, which is time and tokens ($).
No one wants to work with a dogmatic individual but I’d rather a nit picker than a human or agent slop machine.
bluefirebrand 5 hours ago [-]
Okay, but Clean Code* would advocate you extract that function to its own class with a new abstraction and it would ultimately wind up way more complicated than the extra params in a function signature
asdaqopqkq 8 hours ago [-]
i'd argue it is even more harmful for beginns, i remember we had to code a chess program and seeing students getting stuck on OOP stuff rather than actually solve the problems.
HeavyStorm 17 hours ago [-]
[flagged]
locknitpicker 17 hours ago [-]
[flagged]
ryanbrunner 17 hours ago [-]
Fortunately we are humans, and professionally trained humans at that, and we can judge readability and comprehensibility of methods through better measures than whether it crosses a boundary of number of lines.
There is absolutely a place for PR reviews, and I don't think the person you were replying to was against that, just that PR reviews would be better by actually judging things like readability directly rather than relying on measures that estimate those qualities.
I can think of many times arbitrary rules like linting or Clean Code-esque standards resulted in a "solution" of making my code less readable.
rbanffy 17 hours ago [-]
> Fortunately we are humans, and professionally trained humans at that, and we can judge readability and comprehensibility of methods through better measures than whether it crosses a boundary of number of lines.
It's very hard to make a function you need to scroll back and forth to understand readable. Break it into smaller ideas that are more easily reasoned about. We love to think we are too clever, but we are not and we always need to keep an eye on cognitive load - having epifanies when you finally understand how something works is a great feeling, but relying on epifanies coming to you when you are trying to figure out how something works because it's not working now, is a terrible practice.
ryanbrunner 14 hours ago [-]
I think the rule that "functions should be small enough that they should be easily reasoned about" is a reasonable rule, and it makes sense to follow it 95% of the time.
"Functions should be 5 lines or less" is a measure that approximates that rule, but isn't exactly the same thing - I hope you agree we could both come up with 4 line functions that are impossibly complex or 6 line functions that are easily reasoned about.
I think with Clean Code (and a lot of these kinds of things - Design Patterns is a great old example of this), people can get too dogmatic about applying these sort of approximated rules, when it would make a lot more sense for someone else (i.e. not the code writer) to use their best judgement and just directly answer the question "is this function easy to reason about?" rather than using the approximate measure.
rbanffy 12 hours ago [-]
> "Functions should be 5 lines or less"
This can’t really be a serious guideline unless you are writing APL, in which 5 lines can already be daunting to grasp. This guidance depends on the language. For Python, once you get over 50 lines it starts to look like you don’t actually know what you are doing anymore.
ryanbrunner 11 hours ago [-]
Sorry, you're right - 20 lines or less is the recommendation in the actual text of Clean Code. I have seen people try to push that down to 5 in Ruby on Rails development (IIRC, the most popular linter at one time tried to enforce 5 lines)
In any case, the point stands - there are 19 line functions that are too sense, and 21 line functions that are sensible.
rbanffy 1 hours ago [-]
A hard boundary should only emit a warning. The same with nested loops and ifs.
josephg 11 hours ago [-]
I think it’s very easy to over apply that rule, and break a large function into a lot of small functions that you need to scroll up and down to reason about.
Here’s a 150 line long function I wrote which I think is quite beautiful:
This function traverses a DAG given 2 points in the dag, A and B. It breaks the dag into 4 regions - the nodes which are (transitively) only in the parent subgraph of A, B, in both or - implicitly - in neither. It runs in O(n log n) time.
How would you improve this function? It could use a better doc comment. But do you honestly think it would be better if it were broken into a lot of small functions, each called once? How would you do it?
rbanffy 17 hours ago [-]
> The problem with a long function is not if it has N or N+1 lines of code, it's that length code with many branching conditions is prone to be untestable and introduce non trivial bugs. Once you start to refactor, not only is it easier to parse but harder to break. This fact is known for decades now.
A function with too many lines is, most likely, doing more than one thing. Functions should do one thing, be easy to test (with few or no external dependencies whenever possible), be deterministic (unless required not to be), and so on. Excessive mocking is another code smell I look for - it often betrays poorly designed functions that can't be easily tested.
fellowniusmonk 17 hours ago [-]
I once saw a professional software engineer try to refactor the spaghetti code in a complex bioinfomactics project written in python.
It was a complete failure. That branch was abandoned and development continued off the spaghetti.
There is a reason bioinformatics has its own set of viz charts that only they use.
That's my anecdote anyway, it led me to the conclusion that sometimes things are continuous spaghetti and other than some small organizational changes, attempts to exhaustively discretize the code are a fools errand.
The biggest benefits most projects like that are likely to see are performance and debugging improvements accomplished by factoring out recursion.
Mapping to terrain is always the real effort in my opinion.
rbanffy 17 hours ago [-]
> It was a complete failure. That branch was abandoned and development continued off the spaghetti.
It was considered too hard, most likely because the present state of the code already degenerated beyond recovery. It might be difficult, but it's never impossible.
> Mapping to terrain is always the real effort in my opinion.
Yes. The domain might be complex, and it might be possible that there are no simple ways to work within that domain. Irreducible complexity is, after all, a thing.
lionkor 17 hours ago [-]
There are way better metrics of function complexity, like how many branching points, how many loops, or even just how many levels of indentation.
TDD, OOP, Clean Code, etc are an attempt to solve very real problems. They are then applied as dogma to places where these problems are not evident. That's the issue. Of course these rules have their place, but always with a caveat and never applied over all possible places where they might fit. Very often, a better solution exists, as well.
17 hours ago [-]
kelseyfrog 17 hours ago [-]
Clean code nitpickers mistake the map for the territory. The rules are the map, maintainability is the territory. The map is a model of the territory, but the territory always contains more detail, both zones of maintainability not covered by the rules and zones of unmaintainability covered by the rules. The rules are heuristics, and like all heuristics, they have false positives and false negatives. Being a mature developer means knowing the limits of tools including processes, style, and standards. When they nit to the rules and not to the goal, it doesn't contribute, it distracts.
the__alchemist 17 hours ago [-]
It sounds like we have opposite programming styles!
tcfhgj 17 hours ago [-]
[flagged]
tranceylc 17 hours ago [-]
Even the author of clean code would fall under someone who can’t program
tcfhgj 17 hours ago [-]
what?
I am talking about knowing the idea of clean code, which doesn't include dogmatically limiting #locs in functions to an arbitrary number.
So if someone hates clean code for someone doing that, it's just dumb.
tarcon 18 hours ago [-]
You mean people come to hate code reviews.
If you don't use those rules, you'll argue about something else in the code reviews. Likely something even more ambigous that wasn't explicitly written down for everyone as a baseline.
taybin 18 hours ago [-]
Yes, a toy problem only needs a simple implementation. This is a straw man. And I don't even like Robert Martin's Clean Code, but the author is not addressing where this style actually provides benefits. When you're updating 23 if-statements because you had to add support for some new business workflow, you'll wish you had a conceptual entity that encapsulated the operations on the type of workflows so you just had to implement them in one place.
alerighi 17 hours ago [-]
Indeed. The problem is trying to apply the principles of "Clean Code" or more generally of OOP everywhere. There are surely cases where having an interface as an abstraction and multiple implementations makes sense. There are case where it doesn't, and if you take as a dogma "everything shall be the implementation of an interface" you get more complex code and performance penalty for nothing.
There is nothing wrong with having procedural code with a switch case, as there is nothing wrong in having global variables, in having even goto, depends on how you use it.
philippta 13 hours ago [-]
> There are surely cases where having an interface as an abstraction and multiple implementations makes sense.
I think most people aren't aware of the alternative, which is: A function that can call different implementations based on some other variable.
E.g. instead of having RealDB and MockDB type have a createUser() (method), you have a createUser() (function) that switches part of it's logic based on what DB is selected.
That's the prodecural way of achieving the same thing without needing a concept for virtual functions.
Casey explains this in the long discussion with Uncle Bob.
TheCoelacanth 11 hours ago [-]
Yes, but there's not a chance that there's a material difference in performance between those options because of virtual functions.
Unless you're doing something really stupid, nothing other than the DB access is going to be worth optimizing. If those two options are accessing the DB in exactly the same way, then they will probably be within 1% of each other in performance.
alerighi 2 hours ago [-]
Depends, for example if the variable that selects between the two implementations is a compile time constant (#define or constexpr variable) the compiler can really remove the conditional and all the code of the choice that is not always selected leading to higher performance and smaller footprint.
aidos 13 hours ago [-]
Are you suggesting something like this?
def createUesr(db):
if db is type1:
behaviour1
if db is type2:
behaviour2
The principle of clean code includes KISS, therefore complex code for nothing isn't clean code
weegee101 13 hours ago [-]
Sure, but then Clean Code goes and directly pushes for polymorphism in an area (branching) where indirection and polymorphism is known to be costly for both complexity and performance.
An aspect of this that I wish Muratori had touched on when he wrote this in 2023 is how each of these tenants he has issues with in Clean Code are just trading complexity. All four of the structural rules that Muratori demonstrated issues with generally don't reduce complexity. At best, each trades one type of complexity for another.
There are some great ideas in Clean Code, but outside of DRY, the structural recommendations tend to be more harmful than good.
calvinmorrison 13 hours ago [-]
> There are surely cases where having an interface as an abstraction and multiple implementations makes sense.
A tried and true way solve problems is by adding more layers of indirection, starting with an interface makes it trivial to swap things out. I just did a rewrite of some old sound tool that was hard coded to OSS and Alsa. Now i wanted Pulse and Pipewire, this ended up requiring basically a rewrite because there was a lack of a good interface and assumptions everywhere. Instead now I have some good interfaces and adding whatever the next Linux audio stack comes in - it likely won't be a problem.
josephg 11 hours ago [-]
I think about this like hinges in a door. For a door to move, it needs some hinges. Otherwise the door can’t move. But dont get carried away thinking more hinges is always better. We don’t cut door panels in half and reattach the pieces together with more hinges. That would make the door complex and weak.
Like a door, your software should have hinges (interfaces) in the places it needs to be able to change. And it shouldn’t have hinges in places where it won’t change. Rigidity allows for simpler code and better performance. Flexibility allows for changing requirements and modularity.
The mark of an experienced software engineer is having the judgement to know ahead of time where your code should be flexible and where it should be rigid. A good rule of thumb is to only add an interface when you have 2 or more implementations you want to code up. Until then, just call methods directly. If you don’t have 2 different case studies, you’re going to design the API badly because you don’t know the real requirements.
calvinmorrison 10 hours ago [-]
maybe I am using interfaces the word differently. I mean interfaces as in a language construct, a library, or some programming mechanism to separate things out. Even if I only have -one backend- of something, it's still often a good idea to separate those concerns from the rest of your program, stack, etc, just to make things reason about. to have a mental boundary about where things are happening, or to debug, etc.
inigyou 11 hours ago [-]
What if ALSA is that interface? AFAIK, it can load arbitrary .so's to implement snd_pcm, depending on configuration.
hyperbolablabla 13 hours ago [-]
This is the author's gripe, though. You're sacrificing end user experience for developer ergonomics.
TheCoelacanth 11 hours ago [-]
No, it's a false dichotomy.
When working on large applications, by far the single most important factor in performance is having simple and understandable code.
Understandable but slow code can be fixed. Incomprehensible code can't, so it either stays slow or gets worked around with caching/async processing/etc.
If you want fast software, you should write the simplest thing that isn't obviously stupidly slow, then measure and see what parts you need to change. Occasionally you need to make pieces less readable to make them faster, but it's going to be 5% of the application, not the whole thing.
josephg 10 hours ago [-]
This is only true up to some point of skill & complexity. Hotspot optimisation only takes you so far. Eventually you can end up with a program that is fast everywhere but which is still somehow slow at the macro level. Like LLVM.
Truly fast software is made by thinking about data flow from the start. If you use the right data structures, the code takes care of itself.
But this is far beyond Clean Code. The examples in that book are neither readable nor performant. He uses bad data structures and hidden mutation everywhere. In the large, that approach leads to a buggy, fragile mess.
izacus 4 hours ago [-]
This. I've spent a good chunk of my career on performance work and this idea that you can hotspot optimize stuff after the fact is utterly wrong and the reason why so much of your software sucks battery and runs like crap.
Clean code like approaches have a very real cost for users (even in languages with good optimizers) and is usually unfixable after the fact.
Jtsummers 12 hours ago [-]
> You're sacrificing end user experience for developer ergonomics.
No. You may be sacrificing end user experience, but it's not guaranteed. You have to examine the system under development to determine which style is appropriate.
If you actually have to process huge numbers of these objects, then yes. But if you don't, if whatever the actual real-world object is trickles in at 10 per second, do you need to worry about performance and cache misses here? You're already going to suffer from cache misses because the processing rate is so low.
So you get to make an engineering choice based on circumstances. If you need high-throughput, use a design that satisfies that requirement but maybe forfeits flexibility and maintainability. If you don't, then you can lean towards a design that forgoes a bit of performance in favor of flexibility and maintainability.
Use your judgement, don't follow any rule blindly whether it comes from Muratori or Martin.
HeavyStorm 17 hours ago [-]
Thank you! I always see this stupid conversation about performance and nobody seems to get this.
rbanffy 17 hours ago [-]
It's much easier to optimise an easy to understand program than it is to debug a highly optimised one.
jackling 17 hours ago [-]
> stupid conversation about performance
The article is titled "'Clean' Code, Horrible Performance", that's the argument being made. Why is it a stupid conversation? If you think the trade-offs are necessary, then fine, argue that. But that doesn't change the objective measures that the author did to demonstrate the thesis of article.
leecommamichael 14 hours ago [-]
> you'll wish you had a conceptual entity that encapsulated the operations on the type of workflows so you just had to implement them in one place.
Why have you drawn the conclusion that the author is against this? A function with a switch-statement can do this.
bijowo1676 14 hours ago [-]
switch is example of explicit control flow, which Clean Code argues strictly against.
the better approach would be to use implicit control flow using class hierarchies, interfaces, and such and rely on class behavior, polymorphism and runtime dispatch, instead of explicit switch() which tends to multiply itself across the codebase
fodkodrasz 13 hours ago [-]
You accidentally used the phrase better approach, instead of Clean Code.
wasmperson 16 hours ago [-]
> When you're updating 23 if-statements because you had to add support for some new business workflow, you'll wish you had a conceptual entity that encapsulated the operations on the type of workflows so you just had to implement them in one place.
Polymorphism won't get rid of the 23 if statements, it will just replace them with 23 method implementations. Then when you try to serialize that "conceptual entity" to a file or network socket you'll yearn for the if statements once more.
The main benefit of polymorphism is that it allows you to modify one part of a program without recompiling the other parts. In the absence of pre-compiled modules, polymorphism is isomorphic to branching/switch statements:
It's a problem chosen by the author of Clean Code. How is it a strawman? The author of the article is directly refuting the style of the problem/solution that the original author chose, and arguably demonstrated a better approach. That is not a strawman.
wduquette 16 hours ago [-]
Using `switch` is not a better approach if the design allows for outsiders to add their own shapes at a later time. Using `switch` probably is a better approach if the range is shapes is fixed and new shapes can't be added, especially if the language's `switch` statement requires that all valid cases be included.
jackling 16 hours ago [-]
Sure, but I don't see what this has to do with what I said? I was arguing against the parents assertion that the author's example was a strawman.
For your point, what part of the design shows claims that shapes are to be added/removed by outsiders? You should design for what you know and can reasonably predict. Nothing in the article seems to claim that this problem is situated on outsiders adding their own shapes?
Let's ground the example. Suppose I was writing some 2D collision checking library where these operations we're useful. Now I did Triangles, Rectangles and Circles. If I predict that arbitrary shapes should be added, how should I go about it?
The vtable way could work, but as the author showed, you're likely going to get hit with a fairly significant performance impact. Now if you can reason about your use case and see that its not in a hot loop, then the vtable way should be good to go. But if it was called a lot, then you want that to be performant and find a different method.
Some thinking can lead you to the fact that you don't need a new class at all, you just need a general Polygon object, and use the switch method. Or going by the article, you can precompute the information you need that is constant, area, # of points and add those to a dynamically allocated array (or large enough statically allocated one), and have the best of both worlds.
My point is, you can't really say which one is better until you actually know what your use case and the constraints on your system/users. We need to know how the code is used. People complain about this being a simple example, but its an example that was in the "Clean Code" book. What's important is to realize that the Clean Code version might not be worse in terms of hard to measure things, like maintainability or eligibility, but it is empirically worse for performance, and that trade off matters for many use cases.
Jtsummers 16 hours ago [-]
It's a strawman because Muratori took an obvious toy example meant to illustrate a concept (using classes and methods to dispatch on operations instead of using a series of if/else's as in Martin's prior example) and focused on what it did poorly (performance), but it was not meant as an example of high-performance code. It was an illustration of a concept that fit into a page. Attacking an illustrative example for not being realistic is kind of dumb.
I had to track down a copy of the book because I didn't have one on hand (thanks internet!) but that example is from chapter 6. The first listing is actually close to Muratori's code (except using classes instead of a tagged struct for dispatch but still using a procedural approach rather than dispatching off of methods), the second listing is the OO one that Muratori starts with. The point being illustrated is summed up in the book in these two quotes:
> Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. OO code, on the other hand, makes it easy to add new classes without changing existing functions.
> Procedural code makes it hard to add new data structures because all the functions must change. OO code makes it hard to add new functions because all the classes must change.
And amusingly, given that this whole thing is meant as a criticism of Martin and Clean Code he has this right after those two statements:
> Mature programmers know that the idea that everything is an object is a myth. Sometimes you really do want simple data structures with procedures operating on them.
So at least in the book, he has right here, after the "bad" code Muratori is criticizing, addressed the fact that you need to choose your representation based on your circumstances.
jackling 16 hours ago [-]
An article proving its thesis that clean code can cause bad performance isn't a strawman. He wasn't intentionally using a weaker argument of Bob Martin just to find flaws. He was taking an example from the book to show where it failed.
He also could have showed the if-statement version, and it wouldn't have some of the performance impacts, but there's a big chunk of the article that's independent of that. There would still be performance benefits, since the article isn't purely switch statements vs vtables. It went through a series of clean-code tenets that were shown to cause performance problems. That's the authors point, performance deteriorates when following those principles. Even in real world examples this will happen, are you claiming otherwise?
I feel like everyone is just talking over the article, unless you disagree with the actual thesis, that the clean code tenets listed cause bad performance, then you don't really disagree with the author here right? You can argue in spite of the performance decrease, the clean code method is better for real systems, which is fine and I have no issues with that, but that's a separate claim you should prove, and state clearly to who ever is working on the code you're writing.
> but it was not meant as an example of high-performance code
That's part of the point, the clean-code version can't be high-performance. The tenets of it contradict how the hardware works, and causes slows down (not necessarily all the time, but it does typically.)
Jtsummers 16 hours ago [-]
> He also could have showed the if-statement version, and it wouldn't have some of the performance impacts, but there's a big chunk of the article that's independent of that.
You just explained why the piece comes across (when taken as a criticism of Clean Code) as a strawman. Muratori explicitly ignored the example in the book with the better performance and Martin's statement that the second way (using method dispatch) wasn't always the right way.
That is exactly what a strawman argument does. It ignores parts of the original statement to argue against something not claimed. Muratori exaggerates the idea that Clean Code says you must use the second (slower) approach even though the book itself says that you should use your judgement and pick the correct style based on what you need to do. While not explicitly addressed in the book, this means that if you need performance, then the book is not objecting to the first (or Muratori's) style.
jackling 13 hours ago [-]
I just don't see how this is a strawman. It's not a logical fallacy to take an argument that X is good, and say it has Y flaw that wasn't considered. If you want to talk about how steak and eggs are good for building muscle, it wouldn't be a strawman for me to argue that its bad for your heart, and there are better methods.
If the author was purposely mischaracterizing what clean code was advocating for, arguing against the weakest version of what Martin was saying was clean code, I can see that being an issue. But he took a section of the code that Martin claimed was clean code, and arguing against the provided example being good code despite it fitting Martin's idea of clean.
The book says to pick the best version, and maybe it was improper for the author to omit the other version, but even the other version has issues that the article addresses. You can use the more performant switch case version and see how omitting other principles of clean code cause gains from even that version.
Again, the claim in the article was not purely vTables vs Switch statements, there several other claims that have nothing to do with that, for example the reliance on not using internal details of a class, or DRY which appear in both the OO and procedural versions of Martin's code IIRC.
The book actually makes a stronger claim than the author's IMO. The books claim is that there are principles that make clean code, and a person should follow in order to make their code clean. The implication being that not following these rules makes your code unclean (but Martin doesn't explicitly say this iirc, so this may be too strong of a statement). Martin doesn't really provide useful metrics to back up this claim either, so its hard to tell what parts of it to take as sage advice, and what really doesn't work. The author of the article at least provides empirical data to back up the thesis, which is that this "Clean Code" has terrible performance. It doesn't matter if Martin doesn't argue that it is performant, the fact (as proven by the data shown) that the code has worse performance than other methods is enough to prove the author's claim, and is not a strawman.
pragmatic 9 hours ago [-]
What?
jayd16 18 hours ago [-]
Ok now add a Path shape that has to calculate the area of a polygon with arbitrary complexity.
Consider how the workload is now dominated by the core task of actually calculating the area, reducing the impact of struct usage.
Consider the diffs required to make this change.
It's not like Clean Code should be taken as gospel but this micro-benchmark is not a realistic example of what CC is trying to solve.
lunar_mycroft 18 hours ago [-]
In that case, you'd branch into a separate function/block that runs the calculation. Sure, it's slower than a simple array index to find a coefficient, but you're only incurring that cost when you actually need it and it's still much faster than using polymorphism everywhere instead.
coldbrewed 17 hours ago [-]
The problem in both of these cases is to how prioritize the complexity of the domain vs. the cognitive overhead of the implementation vs. the computational complexity. If the domain is complex and best represented by modeling the domain, model the domain. If the domain is simple and the the complexity is low, make it simple. If the computational complexity is high and the domain is complex, then all solutions will be bad so minimize the suck in the best way that you know how.
Occam's razor applies to all domains. Don't use confusing implementations until there are no good options left.
josephg 10 hours ago [-]
I think if you push your craft, most things become this sort of tradeoff between approaches. More performance at the cost of more code complexity and a harder to use API. Deep testing improves correctness but locks you in to your existing design choices and makes refactoring harder.
But most code is still nowhere near the Pareto frontier. Lots of code can be improved on one or multiple axes without sacrificing anything. For example, making functions pure when you can often results in easier to read code, better readability and better performance (with other changes). This is my main gripe with “clean code”. His examples are full of hidden side effects and latent performance problems. He over relies on classes, inner mutation, virtual functions and tiny functions spread out everywhere. It’s a pity, but he doesn’t seem to know how to actually practice what he preaches.
Discussion between Casey (author of this article) and Uncle Bob (author of _Clean Code_, whose programming patterns Casey is critiquing), posted on HN on 2023-03-11 (https://news.ycombinator.com/item?id=35105528), 223 points, 213 comments
It really depends on what you are building, coding is always about trade-offs, and sometimes (not always) you have to choose between maintainability/readability and performance.
If you are writing code for embedded devices where every cpu cycle counts, I would indeed trade a bit of the maintenance for some cpu cycle.
If I am writing a huge web app that has to be maintained years by a large team of devs, I would prefer a more simple/maintanable code over a fast one (+ in such scenarios the real bottlenecks are often your I/O, not the raw CPU perf).
This is for the same reason you usually write code that needs to be fast in low-level programming lng like C and huge web app in Node.js or Java.
jeffnash 15 hours ago [-]
It seems like the main takeaway is that many textbook OO paradigms aren't the most optimized representations of the code. In this case, the cost is dynamic dispatch and pointer-chasing. This is a function of the Shape abstraction, but not the abstraction itself.
But the argument is you're trading some of that performance optimization for maintainability. None of this is exactly news. And while I'm here ranting: I never understood why shapes are the canonical OOP example. Shapes are a closed set of types (yes I'm sure GPT-324 invented a new one) with an open set of operations. There's always going to be one more thing you need to do with those shapes, but you'll never be adding new shapes down the road unless you are still in Kindergarten. OOP is useful for the exact opposite case, where there is a relatively fixed set of operations and you routinely introduce a new subtype that needs to perform all or most of those operations.
I've noticed that most courses that introduce the concept of OOP do so in a way that (perhaps unintentionally) emphasizes the false notion that everything should have an 'x-is-a-y' taxonomy before actually asking the question if that is appropriate. Putting the Cart extends Vehicle before the Horse extends Animal.
captainbland 2 hours ago [-]
I think single dynamic dispatch is one thing but ends up reasonably well optimised by modern compilers, especially the JVM. I think most code written since the 2010s prefers the composition over inheritance pattern for the most part so tends to use interfaces rather than concrete base classes.
That said, double dispatch as in the visitor pattern is often too hard to analyse for optimisation and I think humans frequently get a bit lost with it as well. Fortunately pattern matching is doing away with it. I think it's one of these gang of four patterns that has a lot of people scratching their heads and wondering if the open/closed principal is that worth sticking to if this is the outcome.
leecommamichael 14 hours ago [-]
> In this case, the cost is dynamic dispatch and pointer-chasing.
To sharpen your statement, the cost is missing the CPU caches, which is often caused by failing to pool allocations and reading indirectly.
> But the argument is you're trading some of that performance optimization for maintainability.
Right, but exactly how much? I would argue "very OOP" design styles neuter your ability to optimize the system, and sometimes necessitate that you are kept at arms-length from the system, only capable of "customizing" it via more abstract API layers. I do believe certain OOP practices can make maintaining software easier, but I also believe we have not figured out how to retain control over the computer in the face of these abstractions.
As an example, Clean Coders advocate for "separation of responsibilities" and often speak in terms like "ownership" or what a function/class "knows about" or "should have to know about." When different classes are given different data-fields in the pursuit of making it clearer (what should exist in that scope,) you are creating a constraint which is virally spread through the codebase which runs counter to what the CPU wants. The CPU wants an array, but you can't have an array because the FileManagerFile can't "know about" the FileManagerFileCache, and the FileManagerFileCache can't known about the FileCache, so now each FileManager "owns" its own cache, which is an entirely separate heap allocation.
jeffnash 7 hours ago [-]
Well said. And when you put it that way (we have not figured out how to retain control over the computer in the face of these abstractions), I almost laughed out loud because of how true it is. Some of the worst, most confusing codebases I have touched are full of abstractions that require me to have 5 tabs open at once in my IDE to understand what is going on. The type of codebase that ends up being a sales pitch for multi-monitor setups.
flossly 19 hours ago [-]
I'd say Clean Code is teaching many bad-practices. Too many to be recommended.
pragmatic 9 hours ago [-]
Yes. So all you need to do is look at the code samples.
Is the most non-sensical thing I've seen. So of course the junior dev parade thinks it's the gospel.
Had a terrible manger who would swear by this book but couldn't code his way out of a paper bag.
general1465 18 hours ago [-]
> Functions should be small + Functions should do one thing
This is often a trap for performance. Sure, it looks nice on a screen but calling a function to return a variable is usually epic waste of performance unless compiler will save you by inlining the function into your code or architecture you are using has a magic instruction for that (call vs fcall - which compiler has to recognize and use) which is just fancy "goto there, mov r1 <- *var, goto back"
bluGill 17 hours ago [-]
If your compilers is any good it will inline, and do a better job than you of figuring out what should be inlined. For that matter function calls are generally fast so long as the objects you copy as part of the function call are not slow to copy (which they can be). There are exceptions to the above, but in general small functions are not a problem.
What is a problem is large functions. I have seen functions that were over 60,000 lines long (and few comments or other excess space takers). I will take a 5 lines max rule for functions (this is nearly straw man levels of short!) over that. Functions that are 50 lines long start to get annoying to read but are not a problem. Even 100 lines functions I can handle. However the extreme of long functions is much worse than the extreme of short.
rbanffy 17 hours ago [-]
> I have seen functions that were over 60,000 lines long
That can't possibly be from a serious person.
Jtsummers 17 hours ago [-]
I've seen 10k+ SLOC functions written in C, and 20k SLOC functions written in Fortran, so it wouldn't surprise me if people created ones as big as bluGill describes.
The C was almost always written by EEs who learned that function calls were expensive and so they minimized their use of them (this was their stated rationale, not me guessing). What amused me was that every time I tackled one of those things I'd reduce the line count by 70-90%, and usually at least double performance, by using a bunch of small functions to encapsulate the repeated logic. Compilers inline well, and have for quite some time.
rbanffy 17 hours ago [-]
> I've seen 10k+ SLOC functions written in C, and 20k SLOC functions written in Fortran,
That, spoken by Rutger Hauer.
bluGill 16 hours ago [-]
Sadly it is serious and in production code. (I left there 15 years ago, I suspect it isn't in production anymore)
Worse, it was a giant switch, and the target system didn't have enough memory for all the code so there were different builds and the user would select which to load.
There was code like
case foo:
doSomething();
#ifdef build_two
doSomethingElse();
break;
case bar:
SomeThing();
#endif
MoreThings();
break;
Try to follow that mess.
rbanffy 12 hours ago [-]
I’m not sure the author knew what was happening. It seems like it worked like this and they left it at that.
bluGill 12 hours ago [-]
While this was the worst example he constantly wrote code like that. He could write code like that with few bugs faster than any other programmer I've ever worked with could write code. Management changed between loving and hating him, they knew what his code cost, but they also knew they were getting results fast when that mattered.
rbanffy 1 hours ago [-]
I knew a guy who naturally wrote code that baffled me all the time. Sometimes I wondered how the compiler managed to figure it out.
Fun thing: he was like that all the time - it felt like his language cortex was just wired differently.
tialaramex 17 hours ago [-]
Manual inlining, like manual loop unrolling wants an explanation, why did you do this, why not let the compiler do it? If I see it with no explanation I am going to assume you don't know what you're doing.
rbanffy 17 hours ago [-]
> unless compiler will save you by inlining the function into your code or architecture
That's precisely what a compiler should do. Your code should be easy to read and understand. Let the compiler inline calls and unroll loops (until the I1/L2/L3 cache starts becoming a problem, that is)
pixlmint 18 hours ago [-]
eh, when I read it as a newbie it was really helpful. still had to make my own experiences and judgments, but overall I think reading it made me a better programmer
flossly 18 hours ago [-]
at bast it makes to better at "Clean(TM) OOP code".
programming in general is waaaaaay bigger than what the book covers.
Jtsummers 18 hours ago [-]
> programming in general is waaaaaay bigger than what the book covers.
It's way bigger than any book covers. Clean Code has some useful things, but if anyone actually reads chapter 1 they'd see that Martin even addresses the idea that you should not just read Clean Code and use it alone, or even entirely. It's a collection of one person's judgements (some good, some bad), just like all the other books like it.
pixlmint 15 hours ago [-]
you realize reading books isn't a zero-sum game right? I can still read more books, it didn't end with Clean Code
flossly 8 hours ago [-]
and hence i scope it's message to "clean OOP code"; simply to show that it's flawed message does not even pertain to code in general.
pragmatic 9 hours ago [-]
Why did Uncle Boob ever have any credence or credibility?
What did Bob ever ship that gives him any gravitas or authority in this area?
scelerat 17 hours ago [-]
How much of the performance differences come down to language or compiler choice in these examples?
Would I see the same kinds of performance gains or losses avoiding or using certain patterns in Go or Rust or Java? Are they the same examples as in C++?
What about dynamic languages like ruby or python or javascript?
nylonstrung 13 hours ago [-]
I believe in Rust there would be almost no performance hit due to the compiler using monomorphizing everything via the "zero-cost abstraction" we love to brag about
relug 4 hours ago [-]
its a bigger problem of class based abstractions...cpu thinks in terms of arrays and lanes and indexes thats literally what a pointer is...when you try and abstract that away it in the wrong way that compiler cant understand, it creates overhead. but i think this is overall for all patterns in programming classrs are just so low level people take it for dogma and are appalled that something so standard is anti pattern
bellgrove 12 hours ago [-]
I think the author comes from a very specific perspective; RAD tools, as I understand it, generally has only one, or a very few, software engineers per product. The way I would write code on a personal project is very different than the way I’d write code in an environment with changing team members, interns, guest commits, etc.
Also, In real-time simulations (ie games) often then way you write code can be the bottleneck. In web services the bottlenecks are more often network calls, database model, etc.
narnarpapadaddy 16 hours ago [-]
I think performance generally trades along a different axis: open-world vs closed-world assumptions. There are many cases where closed-world assumptions may confer performance benefits, such as tree-shaking, whole program optimization, and using switch statements rather than a class hierarchy. Whereas designing for extensibility necessarily precludes some of those choices (though it doesn’t necessarily require OOP, for example registering a handler in a table). In other words, it’s easier to optimize a problem that is fixed and well-understood, versus one flexible and unknown. Take that ideas to the extreme and end up at ASIC bitcoin miners.
glitchc 17 hours ago [-]
I'm not sure I follow the thrust of the article. The author starts off with talking about clean code, but then compares OO with procedural code. It's not the same thing, and of course we've always known that OO abstractions carry a performance penalty. Even the founders of OO (Alan Kay et al.) acknowledged the memory and compute impact, but thought it was a worthwhile tradeoff for clean abstractions in complex code-bases.
Back then computers were far less performant than they are today, so the first languages (e.g. SmallTalk) had to be compiled into a bytecode VM that ran on a Xerox PARC. Other efforts included hardcoding some of the constructs into the ISA.
jackling 16 hours ago [-]
He states at the start of the article the tenets of clean code he's arguing against, not just the general OOP of it. Shows how ignoring a certain tenet leads to increase performance, that's the thrust of the article. He routinely in the article goes back to the tenets he's arguing against.
ferroman 12 hours ago [-]
Clean Code wasn't trying to solve performance issues. It tries to solve issue with expensive code maintenance.
teddyh 13 hours ago [-]
Please note that this criticism is from 2023, but the “Clean Code” book has a second edition from 2025, extensively revised to account for the many misconceptions which new programmers might have gotten from the old edition, such as interpreting rules too strictly, etc.
pragmatic 9 hours ago [-]
Are the examples any better?
What a muddled bunch of gibberish.
meerita 17 hours ago [-]
"Code Complete" by Steve McConnell is a good option for those who want to improve their development practices.
wduquette 16 hours ago [-]
Make it work, then make it "clean" (that is, readable and maintainable); then make it fast, and only if measurement indicates that it matters.
brianpan 12 hours ago [-]
Yes! Clean code (which is more readable and maintainable) should be a stop on the way to higher performance code. Otherwise, you are optimizing too early.
bluGill 17 hours ago [-]
I stopped reading as soon as I saw the shape class. This example (along with the proverbial animal) has done a lot of harm to OOP and programming. You need base classes (which are not always the right answer, but when they are) to be based on the abstract concept you need to model not something real that is easy to understand when someone isn't an expert in your domain.
1:00:00 - Open/closed principle and 1:13:52 - Liskov substitution principle.
Both are given in terms of Shape, but it's to paint the picture that things are more complicated than you thought, even with something that should have been as simple as shapes. (As opposed to "shapes are easy, just model the world like that and it will be easy too")
usr_222 16 hours ago [-]
The only reason why your code is slow or bad - because you created it in such a way, not due Clean Code.
I cannot stop being surprised by how ridiculously short-sighted developers are - and how you continue to believe in golden hammers and silver bullets.
You want to build a car, so you take the “Clean Code” hammer and try to build one with it. Then you say, “Hmm, I built a car using the Clean Code hammer, but it cannot even reach 100 km/h. Therefore, Clean Code is bullshit.”
This is ridiculous.
The same applies to blind followers of Clean Code and SOLID who build systems without any high-level understanding of the system they are trying to create. The result is almost always an unreadable, unmaintainable pile of shit.
In fact, they are all in the same boat.
All of these principles are just that: principles. They are not specifications to be implemented. Moreover, they are LOW-LEVEL principles. So, they cannot be “bad,” “good,” “slow,” or “fast”. Your code is bad or slow - not the programming principles.
Until you understand what you are trying to build and how it should work, you cannot decide whether Clean Code, SOLID, GoF patterns, or any other principles are appropriate.
Once you have a solid architectural backbone that satisfies the required system characteristics, you can apply the principles that help you implement that design in the simplest and most effective way.
And each principle has its own trade-off with other principles!
--- too much DRY -> dead coupling (all these “cores” and “libraries” that team leads cobble together at night and proudly turning a distributed system into monolith)
--- too loose coupling -> excessive fragmentation -> low cohesion and broken incapsulation
--- excessive SRP -> low cohesion
and so on and so on.
So it is not Clean Code bad - you just not understand what Clean Code and other principles are.
MonstraG 23 hours ago [-]
(2023)
inigyou 21 hours ago [-]
Still true today.
unscaled 18 hours ago [-]
I don't think it was true even in 2023.
This sounds like tackling the problems of C++ in the early 2000s.
1. Casey Muratori also that DRY shouldn't doesn't have to result in non-performant code.
2. Smaller functions, functions that do one-thing: Modern compiler can inline those. There are some edge cases where inlining may make less efficient use of states and loops but I don't think that's a main problem nowadays. I also wouldn't say the extreme version of this idea (very small functions) is still popular. The strongest proponent of this was Uncle Bob, and the last time I've heard him speak about code, he said he now lets the LLM write everything and he only reviews the module hierarchy and maybe the modules' public interfaces.
3. Polymorphism instead of ifs and switches was a big fad in the late 1990s until the late 2000s and had some holdouts in the 2010s. It was only ever popular in the Enterprise Java and C++ world (and maybe in Enterprise Smalltalk, never hard). Overuse of runtime polymorphism widely considered bad form in newer static languages like Go and Rust and in most dynamic languages there was always a tacit understanding of "use mostly conditions, add polymorphism if you need extensibility".
In functional languages (or languages heavily influenced by functional programming like Rust, Swift and Kotlin[1]), the classic approach for the type of scenario in this example is to use a sum type, and run a safe exhaustive match/switch on all the variants.
4. Hiding internals: The sum type example is telling of modern best-practices. Sum type fields are generally made public. Some languages (e.g. Rust and most pure functional languages) do not support private fields in sum types at all! Other languages (e.g. Kotlin)
but immutable, so it's easy to maintain invariants without hiding information. Sometimes we do want to hide the type details and wrap it with public-facing type (this is a common pattern with internal error enums in Rust for example). Even in this case, there is no impact since we do not use runtime polymorphism or indirection (that would be Box<T> in Rust).
Due to compiler optimizations, hiding internals has marginal performance cost (if any) unless you require runtime polymorphism to achieve it. But why should you?
I feel like the performance costs lamented in this article mostly have to do with runtime polymorphism in static languages. And I fully agree here: runtime polymorphism is something that should be avoided when you don't need it[2]. But that's the thing: if you're looking at modern static language codebases, runtime polymorphism is not as hyped as it used to be in the past. Some languages still require heavy use of runtime polymorphism (Go is a good example of this), but other languages more often rely on static polymorphism (Rust) or compile time duck-typing (Zig and you could argue C++ template meta-programming used to do that, albeit quite awkwardly).
Even with all the issues you get with polymorphism, I don't think it's the main cause of slow application performance. It be very much the culprit in tight loops inside games, but if you look at the performance issues plaguing everyday apps, I think the two major culprits are endless layers of abstraction (the most quintessential example is basically every sluggish Electron app out there) and blocking the user on slow actions (like network loads).
---
[1] Even Java had sealed record types for a while now, and I'm sure will see Enterprise frameworks encouraging them in 20 years, when the rest of the world has moved on to spacefaring super-intelligent LLMs. But Enterprise frameworks also don't encourage you to write DRY code or keep your functions short.
[2] But do keep in mind that in Java it could be almost zero-cost in many cases. The JIT will monomorphize or bimorphize your classes if you always use the same class at the same callsite. The pointer indirection is not an extra cost, since every non-primitive that doesn't undergo Scalar Replacement[3] lives on the heap, and has a pointer.
This reads like contrarianism to me, like you have to oppose the article because you just do (maybe you dislike Casey). There's plenty of code written the way Casey disagrees with.
Johanx64 17 hours ago [-]
I wish we were at the level where some doofus has red too many "Gang of Four" "Design Patterns OOP" bullshit books and gone to town. Because that would be way better than what we have now.
Whenever I run a thing and it's unbearabily super duper slow, when you look at the process lists the thing will have spawned bunch of chromium instances - on top of probably making bunch of internet connections. Delegating some of the work that can easily done on my PC to "cloud" instead.
What we have now is way worse - it's electron and webshit technologies on desktop. Like you couldn't make software of worse quality even if you tried. The performance way worse than PCs of 1990s. It's almost like using software that's running from a floppy disk.
And now this trash is probably getting generated with LLMs.
WesolyKubeczek 19 hours ago [-]
Nobody argues with that. But it's helpful to know right from the title that it's the original Casey's work and not something newer.
cratermoon 13 hours ago [-]
This is Muratori showing he's a solo programmer who has only ever worked on relative small, simple software that runs on a single machine.
throw16180339 7 hours ago [-]
He worked on the Granny animation system at Rad Game Tools. It's shipped in over 5200 games[1] and targets all the major game dev platforms.
Actually, that video predates the one on clean code.
jgwil2 15 hours ago [-]
I stand corrected. Still I'd recommend it as a followup for anyone intrigued by this post as it shows a real world, non-trivial example of removing abstractions in order to improve performance.
devmor 17 hours ago [-]
Performance vs. Maintainability is the infinite debate, and it’s a mind numbing one because in the vast majority of professional roles you will have the opportunity to prefer neither.
jeltz 16 hours ago [-]
And following Clean Code gives you neither. The book is written by someone with a very limited experience and the advice is either basic and obvious or harmful. People should just stop reading that book.
ErroneousBosh 14 hours ago [-]
This is just bloody stupid.
If you care about performance, you don't use OOP, you don't use if/else, you don't use switch{case}, what you do is you write the hot parts in assembler.
If you aren't writing it in assembler, you're writing slow code.
But that code is still not optimised until you've implemented it in an ASIC.
zabzonk 12 hours ago [-]
> If you aren't writing it in assembler, you're writing slow code.
Depends in part in how good you are at writing assembler.
ErroneousBosh 2 hours ago [-]
True, with modern processors there is a hell of a lot of "it has to be this way round for the pipeline to flow" that the compiler does for you.
But you're still throwing away so much time on things like bounds-checking memory accesses that never need it.
On a long enough career path, eventually you will run into one Clean Code zealot who carries an air of superiority and nit picks every PR over things like a function having more than an arbitrary number of lines in it instead of reviewing the actual code. This is the point where most people come to hate Clean Code.
Clean Code is unhelpful to beginners too, though: misuse of industry-standard terms, shunning of comments in favor of tiny functions with long names, shunning function arguments in favor of mutating state, polymorphism obsession, etc. So much of the concrete advice the book gives is just plain bad.
The reason people get more pissed off at Clean Code than they would at any other book that gives bad advice is the preachy and authoritative tone it uses. It frames people who don't do "Clean Code" as unprofessional and lazy, and this framing is very convincing to some people, as evidenced by some of the replies in this thread.
> it's impossible to use the word dynamic in a pejorative sense. Try thinking of some combination that will possibly give it a pejorative meaning. It's impossible. Thus, I thought dynamic programming was a good name. It was something not even a Congressman could object to. So I used it as an umbrella for my activities.
Even Robert Martin, very often in his videos and blogs, espouses "engineering judgment" and is quite fine abandoning advice in his book when the situation calls for it.
If performance is important and clean code is impacting it, he won't object to your breaking the rules.
It's mostly with his TDD evangelism that he goes (a little) crazy.
I really like that he brings attention to the idea that code can be beautiful. But judging by his examples, he just seems to have no idea how to actually write beautiful software himself.
I wish he went off and learned some functional programming. Haskell, Erlang, Clojure, F#. Something like that. Really go deep. If you want to know what really beautiful software looks like, that’s the place. Those communities know beauty.
From what I have seen, his ideas were formed in a vacuum divorced from real coding. You can see it in the small amounts of open source he has released. The kind of guy who will always prefer 50 classes to 5. Who clutches his pearls at an if statement.
I remember he was invited to give a talk at Bloomberg and the majority of us were simply disgusted that an obvious hack like him was lecturing a group of software engineers who had really been there and done that.
You see this clearly if you ever teach. I would give all my students the same spec. Some students submitted small, simple programs which passed all the tests with flying colours. Some students would submit huge programs which barely worked.
As Alan Kay said once, the right point of view (on a problem) is worth 50 IQ points. At the margins there's subjectivity and tradeoffs. But lots of programs are more or - more often - much less beautiful.
Fyi.
Maybe we'll see an updated version some day, with better examples. It would be valuable if only so people would stop trying to defend his old, bad advice.
Functions always being under 10 lines just means you've got functions everywhere, which can be mentally exhausting to follow logic, if you aim for like 40 lines tops you can write better "stories" with your code that are easier to follow and more expressive.
I also have to think about whether there are any other areas of the code that might be calling your helpers, and whether they might break if I change the helper. Seeing everything inlined makes it absolutely clear from local reading that modifying the code only affects the local functionality.
I'm not saying go insane and copy/paste the same thing multiple times, just that 1k-line functions are sometimes the least of all evils. I liked what John Carmack had to say on this topic: http://number-none.com/blow/john_carmack_on_inlined_code.htm...
Another point from that post that I try to take to heart: if it can be a pure function, it should be a pure function (even in C). Bob Martin's style is the opposite.
I've definitely drifted towards longer code, if nothing in that function is used elsewhere. Some tasks really are just lists of things to do (especially in something like image processing), and I think that sometimes it doesn't make sense to put lists of lists in your list of things to do.
Instead of // do the thing followed by 5 lines of code to do the thing. You're supposed to put those five lines in a function named DoTheThing(). That doesn't seem better to me.
On top of that, the default place for a console in VS Code is at the bottom, now I'm down to 25-30 lines if I have that open.
And then there's heavier IDEs like IntelliJ, Visual studio, etc.
They might have two or three tab lines at the top, a row used for class navigation (that I've just realized I've never, ever used), an extra row or two of quick action bars, a row of extra tabs at the bottom for navigating between consoles/call stacks/locals/error lists, 2 info bar rows at the bottom, and sometimes two or even three scroll bars stacked on top of each other.
All of a sudden you're down from 30 lines visible to 17, which is my actual visible lines of code when I have debugging running.
I worked with a guy who would constantly drop niche jargon and quotes from famous engineers and then kind of smugly look at you. He could not write a function without proudly saying what principals it was following. He was a horrible programmer, ended up getting laid off.
The bureaucrats who above all value process over outcome.
I have seen very clean codebases with a handful very long functions, but they were no issue she nice they only did one thing.
I personally write quite short functions but I have never understood why people take issue with large functions. Those are one of the easiest things to fix in a bad codebase. It is much harder to clean up after someone who used too small functions.
wing-_-nuts says (emphasis added) “My personal benchmark for 'MAYBE this function is too long' is when it doesn't fit on the page.”.
I think that’s a fine heuristic. Long functions can be fine, but longer functions tend to be less testable, so you should try to avoid them.
On the other hand, it can take lots of thinking to properly decompose functionality, and that decomposition can easily change when requirements change, so spending that time may not be worth it.
depends on what the function does, most likely the best decomposition into functions isn't simply splitting the function in to n sequential parts
> I have seen very clean codebases with a handful very long functions, but they were no issue she nice they only did one thing.
one thing usually consists of multiple other things
imho length should correlate negatively with cyclomatic complexity - it's ok if you write 300 locs if all you do is fill a map with trivial entries
I can remember as a novice that I would write an entire program in a single, many-thousand line function, unable to see where the boundaries between functions should be. With experience and expertise in the domain, it becomes easier to see where those should be.
Except the only real rule is that rules are wrong.
All there are is different and actually contradictory pressures for different and actually contradictory priorities that are all true and valid at the same time even though many contrdict. The correct thing in each given moment is whatever makes the shortest rubber band lines between all priorities.
Sometimes that will be a very large single function even if some other times that will be a bunch of 10 liners.
Every time you over scope a function signature, because you want to handle that other case, you add mental tax to the next person. This accumulates, burns time, and now confuses agents, which is time and tokens ($).
No one wants to work with a dogmatic individual but I’d rather a nit picker than a human or agent slop machine.
There is absolutely a place for PR reviews, and I don't think the person you were replying to was against that, just that PR reviews would be better by actually judging things like readability directly rather than relying on measures that estimate those qualities.
I can think of many times arbitrary rules like linting or Clean Code-esque standards resulted in a "solution" of making my code less readable.
It's very hard to make a function you need to scroll back and forth to understand readable. Break it into smaller ideas that are more easily reasoned about. We love to think we are too clever, but we are not and we always need to keep an eye on cognitive load - having epifanies when you finally understand how something works is a great feeling, but relying on epifanies coming to you when you are trying to figure out how something works because it's not working now, is a terrible practice.
"Functions should be 5 lines or less" is a measure that approximates that rule, but isn't exactly the same thing - I hope you agree we could both come up with 4 line functions that are impossibly complex or 6 line functions that are easily reasoned about.
I think with Clean Code (and a lot of these kinds of things - Design Patterns is a great old example of this), people can get too dogmatic about applying these sort of approximated rules, when it would make a lot more sense for someone else (i.e. not the code writer) to use their best judgement and just directly answer the question "is this function easy to reason about?" rather than using the approximate measure.
This can’t really be a serious guideline unless you are writing APL, in which 5 lines can already be daunting to grasp. This guidance depends on the language. For Python, once you get over 50 lines it starts to look like you don’t actually know what you are doing anymore.
In any case, the point stands - there are 19 line functions that are too sense, and 21 line functions that are sensible.
Here’s a 150 line long function I wrote which I think is quite beautiful:
https://github.com/josephg/diamond-types/blob/e143890a596aaf...
This function traverses a DAG given 2 points in the dag, A and B. It breaks the dag into 4 regions - the nodes which are (transitively) only in the parent subgraph of A, B, in both or - implicitly - in neither. It runs in O(n log n) time.
How would you improve this function? It could use a better doc comment. But do you honestly think it would be better if it were broken into a lot of small functions, each called once? How would you do it?
A function with too many lines is, most likely, doing more than one thing. Functions should do one thing, be easy to test (with few or no external dependencies whenever possible), be deterministic (unless required not to be), and so on. Excessive mocking is another code smell I look for - it often betrays poorly designed functions that can't be easily tested.
It was a complete failure. That branch was abandoned and development continued off the spaghetti.
There is a reason bioinformatics has its own set of viz charts that only they use.
That's my anecdote anyway, it led me to the conclusion that sometimes things are continuous spaghetti and other than some small organizational changes, attempts to exhaustively discretize the code are a fools errand.
The biggest benefits most projects like that are likely to see are performance and debugging improvements accomplished by factoring out recursion.
Mapping to terrain is always the real effort in my opinion.
It was considered too hard, most likely because the present state of the code already degenerated beyond recovery. It might be difficult, but it's never impossible.
> Mapping to terrain is always the real effort in my opinion.
Yes. The domain might be complex, and it might be possible that there are no simple ways to work within that domain. Irreducible complexity is, after all, a thing.
TDD, OOP, Clean Code, etc are an attempt to solve very real problems. They are then applied as dogma to places where these problems are not evident. That's the issue. Of course these rules have their place, but always with a caveat and never applied over all possible places where they might fit. Very often, a better solution exists, as well.
I am talking about knowing the idea of clean code, which doesn't include dogmatically limiting #locs in functions to an arbitrary number.
So if someone hates clean code for someone doing that, it's just dumb.
If you don't use those rules, you'll argue about something else in the code reviews. Likely something even more ambigous that wasn't explicitly written down for everyone as a baseline.
There is nothing wrong with having procedural code with a switch case, as there is nothing wrong in having global variables, in having even goto, depends on how you use it.
I think most people aren't aware of the alternative, which is: A function that can call different implementations based on some other variable.
E.g. instead of having RealDB and MockDB type have a createUser() (method), you have a createUser() (function) that switches part of it's logic based on what DB is selected.
That's the prodecural way of achieving the same thing without needing a concept for virtual functions.
Casey explains this in the long discussion with Uncle Bob.
Unless you're doing something really stupid, nothing other than the DB access is going to be worth optimizing. If those two options are accessing the DB in exactly the same way, then they will probably be within 1% of each other in performance.
An aspect of this that I wish Muratori had touched on when he wrote this in 2023 is how each of these tenants he has issues with in Clean Code are just trading complexity. All four of the structural rules that Muratori demonstrated issues with generally don't reduce complexity. At best, each trades one type of complexity for another.
There are some great ideas in Clean Code, but outside of DRY, the structural recommendations tend to be more harmful than good.
A tried and true way solve problems is by adding more layers of indirection, starting with an interface makes it trivial to swap things out. I just did a rewrite of some old sound tool that was hard coded to OSS and Alsa. Now i wanted Pulse and Pipewire, this ended up requiring basically a rewrite because there was a lack of a good interface and assumptions everywhere. Instead now I have some good interfaces and adding whatever the next Linux audio stack comes in - it likely won't be a problem.
Like a door, your software should have hinges (interfaces) in the places it needs to be able to change. And it shouldn’t have hinges in places where it won’t change. Rigidity allows for simpler code and better performance. Flexibility allows for changing requirements and modularity.
The mark of an experienced software engineer is having the judgement to know ahead of time where your code should be flexible and where it should be rigid. A good rule of thumb is to only add an interface when you have 2 or more implementations you want to code up. Until then, just call methods directly. If you don’t have 2 different case studies, you’re going to design the API badly because you don’t know the real requirements.
When working on large applications, by far the single most important factor in performance is having simple and understandable code.
Understandable but slow code can be fixed. Incomprehensible code can't, so it either stays slow or gets worked around with caching/async processing/etc.
If you want fast software, you should write the simplest thing that isn't obviously stupidly slow, then measure and see what parts you need to change. Occasionally you need to make pieces less readable to make them faster, but it's going to be 5% of the application, not the whole thing.
Truly fast software is made by thinking about data flow from the start. If you use the right data structures, the code takes care of itself.
But this is far beyond Clean Code. The examples in that book are neither readable nor performant. He uses bad data structures and hidden mutation everywhere. In the large, that approach leads to a buggy, fragile mess.
Clean code like approaches have a very real cost for users (even in languages with good optimizers) and is usually unfixable after the fact.
No. You may be sacrificing end user experience, but it's not guaranteed. You have to examine the system under development to determine which style is appropriate.
If you actually have to process huge numbers of these objects, then yes. But if you don't, if whatever the actual real-world object is trickles in at 10 per second, do you need to worry about performance and cache misses here? You're already going to suffer from cache misses because the processing rate is so low.
So you get to make an engineering choice based on circumstances. If you need high-throughput, use a design that satisfies that requirement but maybe forfeits flexibility and maintainability. If you don't, then you can lean towards a design that forgoes a bit of performance in favor of flexibility and maintainability.
Use your judgement, don't follow any rule blindly whether it comes from Muratori or Martin.
The article is titled "'Clean' Code, Horrible Performance", that's the argument being made. Why is it a stupid conversation? If you think the trade-offs are necessary, then fine, argue that. But that doesn't change the objective measures that the author did to demonstrate the thesis of article.
Why have you drawn the conclusion that the author is against this? A function with a switch-statement can do this.
the better approach would be to use implicit control flow using class hierarchies, interfaces, and such and rely on class behavior, polymorphism and runtime dispatch, instead of explicit switch() which tends to multiply itself across the codebase
Polymorphism won't get rid of the 23 if statements, it will just replace them with 23 method implementations. Then when you try to serialize that "conceptual entity" to a file or network socket you'll yearn for the if statements once more.
The main benefit of polymorphism is that it allows you to modify one part of a program without recompiling the other parts. In the absence of pre-compiled modules, polymorphism is isomorphic to branching/switch statements:
https://en.wikipedia.org/wiki/Expression_problem
For your point, what part of the design shows claims that shapes are to be added/removed by outsiders? You should design for what you know and can reasonably predict. Nothing in the article seems to claim that this problem is situated on outsiders adding their own shapes?
Let's ground the example. Suppose I was writing some 2D collision checking library where these operations we're useful. Now I did Triangles, Rectangles and Circles. If I predict that arbitrary shapes should be added, how should I go about it?
The vtable way could work, but as the author showed, you're likely going to get hit with a fairly significant performance impact. Now if you can reason about your use case and see that its not in a hot loop, then the vtable way should be good to go. But if it was called a lot, then you want that to be performant and find a different method.
Some thinking can lead you to the fact that you don't need a new class at all, you just need a general Polygon object, and use the switch method. Or going by the article, you can precompute the information you need that is constant, area, # of points and add those to a dynamically allocated array (or large enough statically allocated one), and have the best of both worlds.
My point is, you can't really say which one is better until you actually know what your use case and the constraints on your system/users. We need to know how the code is used. People complain about this being a simple example, but its an example that was in the "Clean Code" book. What's important is to realize that the Clean Code version might not be worse in terms of hard to measure things, like maintainability or eligibility, but it is empirically worse for performance, and that trade off matters for many use cases.
I had to track down a copy of the book because I didn't have one on hand (thanks internet!) but that example is from chapter 6. The first listing is actually close to Muratori's code (except using classes instead of a tagged struct for dispatch but still using a procedural approach rather than dispatching off of methods), the second listing is the OO one that Muratori starts with. The point being illustrated is summed up in the book in these two quotes:
> Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. OO code, on the other hand, makes it easy to add new classes without changing existing functions.
> Procedural code makes it hard to add new data structures because all the functions must change. OO code makes it hard to add new functions because all the classes must change.
And amusingly, given that this whole thing is meant as a criticism of Martin and Clean Code he has this right after those two statements:
> Mature programmers know that the idea that everything is an object is a myth. Sometimes you really do want simple data structures with procedures operating on them.
So at least in the book, he has right here, after the "bad" code Muratori is criticizing, addressed the fact that you need to choose your representation based on your circumstances.
He also could have showed the if-statement version, and it wouldn't have some of the performance impacts, but there's a big chunk of the article that's independent of that. There would still be performance benefits, since the article isn't purely switch statements vs vtables. It went through a series of clean-code tenets that were shown to cause performance problems. That's the authors point, performance deteriorates when following those principles. Even in real world examples this will happen, are you claiming otherwise?
I feel like everyone is just talking over the article, unless you disagree with the actual thesis, that the clean code tenets listed cause bad performance, then you don't really disagree with the author here right? You can argue in spite of the performance decrease, the clean code method is better for real systems, which is fine and I have no issues with that, but that's a separate claim you should prove, and state clearly to who ever is working on the code you're writing.
> but it was not meant as an example of high-performance code
That's part of the point, the clean-code version can't be high-performance. The tenets of it contradict how the hardware works, and causes slows down (not necessarily all the time, but it does typically.)
You just explained why the piece comes across (when taken as a criticism of Clean Code) as a strawman. Muratori explicitly ignored the example in the book with the better performance and Martin's statement that the second way (using method dispatch) wasn't always the right way.
That is exactly what a strawman argument does. It ignores parts of the original statement to argue against something not claimed. Muratori exaggerates the idea that Clean Code says you must use the second (slower) approach even though the book itself says that you should use your judgement and pick the correct style based on what you need to do. While not explicitly addressed in the book, this means that if you need performance, then the book is not objecting to the first (or Muratori's) style.
If the author was purposely mischaracterizing what clean code was advocating for, arguing against the weakest version of what Martin was saying was clean code, I can see that being an issue. But he took a section of the code that Martin claimed was clean code, and arguing against the provided example being good code despite it fitting Martin's idea of clean.
The book says to pick the best version, and maybe it was improper for the author to omit the other version, but even the other version has issues that the article addresses. You can use the more performant switch case version and see how omitting other principles of clean code cause gains from even that version.
Again, the claim in the article was not purely vTables vs Switch statements, there several other claims that have nothing to do with that, for example the reliance on not using internal details of a class, or DRY which appear in both the OO and procedural versions of Martin's code IIRC.
The book actually makes a stronger claim than the author's IMO. The books claim is that there are principles that make clean code, and a person should follow in order to make their code clean. The implication being that not following these rules makes your code unclean (but Martin doesn't explicitly say this iirc, so this may be too strong of a statement). Martin doesn't really provide useful metrics to back up this claim either, so its hard to tell what parts of it to take as sage advice, and what really doesn't work. The author of the article at least provides empirical data to back up the thesis, which is that this "Clean Code" has terrible performance. It doesn't matter if Martin doesn't argue that it is performant, the fact (as proven by the data shown) that the code has worse performance than other methods is enough to prove the author's claim, and is not a strawman.
Consider how the workload is now dominated by the core task of actually calculating the area, reducing the impact of struct usage.
Consider the diffs required to make this change.
It's not like Clean Code should be taken as gospel but this micro-benchmark is not a realistic example of what CC is trying to solve.
Occam's razor applies to all domains. Don't use confusing implementations until there are no good options left.
But most code is still nowhere near the Pareto frontier. Lots of code can be improved on one or multiple axes without sacrificing anything. For example, making functions pure when you can often results in easier to read code, better readability and better performance (with other changes). This is my main gripe with “clean code”. His examples are full of hidden side effects and latent performance problems. He over relies on classes, inner mutation, virtual functions and tiny functions spread out everywhere. It’s a pity, but he doesn’t seem to know how to actually practice what he preaches.
HN post for original article on 2023-02-28 (https://news.ycombinator.com/item?id=34966137), 739 points, 914 comments
Discussion between Casey (author of this article) and Uncle Bob (author of _Clean Code_, whose programming patterns Casey is critiquing), posted on HN on 2023-03-11 (https://news.ycombinator.com/item?id=35105528), 223 points, 213 comments
"Horrible Code, Clean Performance", a "homage" to Casey's original article, posted on HN on 2023-04-19 (https://news.ycombinator.com/item?id=35596069), 121 points, 114 comments
If you are writing code for embedded devices where every cpu cycle counts, I would indeed trade a bit of the maintenance for some cpu cycle.
If I am writing a huge web app that has to be maintained years by a large team of devs, I would prefer a more simple/maintanable code over a fast one (+ in such scenarios the real bottlenecks are often your I/O, not the raw CPU perf).
This is for the same reason you usually write code that needs to be fast in low-level programming lng like C and huge web app in Node.js or Java.
But the argument is you're trading some of that performance optimization for maintainability. None of this is exactly news. And while I'm here ranting: I never understood why shapes are the canonical OOP example. Shapes are a closed set of types (yes I'm sure GPT-324 invented a new one) with an open set of operations. There's always going to be one more thing you need to do with those shapes, but you'll never be adding new shapes down the road unless you are still in Kindergarten. OOP is useful for the exact opposite case, where there is a relatively fixed set of operations and you routinely introduce a new subtype that needs to perform all or most of those operations.
I've noticed that most courses that introduce the concept of OOP do so in a way that (perhaps unintentionally) emphasizes the false notion that everything should have an 'x-is-a-y' taxonomy before actually asking the question if that is appropriate. Putting the Cart extends Vehicle before the Horse extends Animal.
That said, double dispatch as in the visitor pattern is often too hard to analyse for optimisation and I think humans frequently get a bit lost with it as well. Fortunately pattern matching is doing away with it. I think it's one of these gang of four patterns that has a lot of people scratching their heads and wondering if the open/closed principal is that worth sticking to if this is the outcome.
To sharpen your statement, the cost is missing the CPU caches, which is often caused by failing to pool allocations and reading indirectly.
> But the argument is you're trading some of that performance optimization for maintainability.
Right, but exactly how much? I would argue "very OOP" design styles neuter your ability to optimize the system, and sometimes necessitate that you are kept at arms-length from the system, only capable of "customizing" it via more abstract API layers. I do believe certain OOP practices can make maintaining software easier, but I also believe we have not figured out how to retain control over the computer in the face of these abstractions.
As an example, Clean Coders advocate for "separation of responsibilities" and often speak in terms like "ownership" or what a function/class "knows about" or "should have to know about." When different classes are given different data-fields in the pursuit of making it clearer (what should exist in that scope,) you are creating a constraint which is virally spread through the codebase which runs counter to what the CPU wants. The CPU wants an array, but you can't have an array because the FileManagerFile can't "know about" the FileManagerFileCache, and the FileManagerFileCache can't known about the FileCache, so now each FileManager "owns" its own cache, which is an entirely separate heap allocation.
Is the most non-sensical thing I've seen. So of course the junior dev parade thinks it's the gospel.
Had a terrible manger who would swear by this book but couldn't code his way out of a paper bag.
This is often a trap for performance. Sure, it looks nice on a screen but calling a function to return a variable is usually epic waste of performance unless compiler will save you by inlining the function into your code or architecture you are using has a magic instruction for that (call vs fcall - which compiler has to recognize and use) which is just fancy "goto there, mov r1 <- *var, goto back"
What is a problem is large functions. I have seen functions that were over 60,000 lines long (and few comments or other excess space takers). I will take a 5 lines max rule for functions (this is nearly straw man levels of short!) over that. Functions that are 50 lines long start to get annoying to read but are not a problem. Even 100 lines functions I can handle. However the extreme of long functions is much worse than the extreme of short.
That can't possibly be from a serious person.
The C was almost always written by EEs who learned that function calls were expensive and so they minimized their use of them (this was their stated rationale, not me guessing). What amused me was that every time I tackled one of those things I'd reduce the line count by 70-90%, and usually at least double performance, by using a bunch of small functions to encapsulate the repeated logic. Compilers inline well, and have for quite some time.
That, spoken by Rutger Hauer.
Worse, it was a giant switch, and the target system didn't have enough memory for all the code so there were different builds and the user would select which to load.
There was code like
Try to follow that mess.Fun thing: he was like that all the time - it felt like his language cortex was just wired differently.
That's precisely what a compiler should do. Your code should be easy to read and understand. Let the compiler inline calls and unroll loops (until the I1/L2/L3 cache starts becoming a problem, that is)
programming in general is waaaaaay bigger than what the book covers.
It's way bigger than any book covers. Clean Code has some useful things, but if anyone actually reads chapter 1 they'd see that Martin even addresses the idea that you should not just read Clean Code and use it alone, or even entirely. It's a collection of one person's judgements (some good, some bad), just like all the other books like it.
What did Bob ever ship that gives him any gravitas or authority in this area?
Would I see the same kinds of performance gains or losses avoiding or using certain patterns in Go or Rust or Java? Are they the same examples as in C++?
What about dynamic languages like ruby or python or javascript?
Also, In real-time simulations (ie games) often then way you write code can be the bottleneck. In web services the bottlenecks are more often network calls, database model, etc.
Back then computers were far less performant than they are today, so the first languages (e.g. SmallTalk) had to be compiled into a bytecode VM that ran on a Xerox PARC. Other efforts included hardcoding some of the constructs into the ISA.
What a muddled bunch of gibberish.
https://www.youtube.com/watch?v=zHiWqnTWsn4
1:00:00 - Open/closed principle and 1:13:52 - Liskov substitution principle.
Both are given in terms of Shape, but it's to paint the picture that things are more complicated than you thought, even with something that should have been as simple as shapes. (As opposed to "shapes are easy, just model the world like that and it will be easy too")
I cannot stop being surprised by how ridiculously short-sighted developers are - and how you continue to believe in golden hammers and silver bullets. You want to build a car, so you take the “Clean Code” hammer and try to build one with it. Then you say, “Hmm, I built a car using the Clean Code hammer, but it cannot even reach 100 km/h. Therefore, Clean Code is bullshit.”
This is ridiculous.
The same applies to blind followers of Clean Code and SOLID who build systems without any high-level understanding of the system they are trying to create. The result is almost always an unreadable, unmaintainable pile of shit. In fact, they are all in the same boat.
All of these principles are just that: principles. They are not specifications to be implemented. Moreover, they are LOW-LEVEL principles. So, they cannot be “bad,” “good,” “slow,” or “fast”. Your code is bad or slow - not the programming principles.
Until you understand what you are trying to build and how it should work, you cannot decide whether Clean Code, SOLID, GoF patterns, or any other principles are appropriate. Once you have a solid architectural backbone that satisfies the required system characteristics, you can apply the principles that help you implement that design in the simplest and most effective way.
And each principle has its own trade-off with other principles! --- too much DRY -> dead coupling (all these “cores” and “libraries” that team leads cobble together at night and proudly turning a distributed system into monolith) --- too loose coupling -> excessive fragmentation -> low cohesion and broken incapsulation --- excessive SRP -> low cohesion and so on and so on.
So it is not Clean Code bad - you just not understand what Clean Code and other principles are.
This sounds like tackling the problems of C++ in the early 2000s.
1. Casey Muratori also that DRY shouldn't doesn't have to result in non-performant code.
2. Smaller functions, functions that do one-thing: Modern compiler can inline those. There are some edge cases where inlining may make less efficient use of states and loops but I don't think that's a main problem nowadays. I also wouldn't say the extreme version of this idea (very small functions) is still popular. The strongest proponent of this was Uncle Bob, and the last time I've heard him speak about code, he said he now lets the LLM write everything and he only reviews the module hierarchy and maybe the modules' public interfaces.
3. Polymorphism instead of ifs and switches was a big fad in the late 1990s until the late 2000s and had some holdouts in the 2010s. It was only ever popular in the Enterprise Java and C++ world (and maybe in Enterprise Smalltalk, never hard). Overuse of runtime polymorphism widely considered bad form in newer static languages like Go and Rust and in most dynamic languages there was always a tacit understanding of "use mostly conditions, add polymorphism if you need extensibility".
In functional languages (or languages heavily influenced by functional programming like Rust, Swift and Kotlin[1]), the classic approach for the type of scenario in this example is to use a sum type, and run a safe exhaustive match/switch on all the variants.
4. Hiding internals: The sum type example is telling of modern best-practices. Sum type fields are generally made public. Some languages (e.g. Rust and most pure functional languages) do not support private fields in sum types at all! Other languages (e.g. Kotlin) but immutable, so it's easy to maintain invariants without hiding information. Sometimes we do want to hide the type details and wrap it with public-facing type (this is a common pattern with internal error enums in Rust for example). Even in this case, there is no impact since we do not use runtime polymorphism or indirection (that would be Box<T> in Rust).
Due to compiler optimizations, hiding internals has marginal performance cost (if any) unless you require runtime polymorphism to achieve it. But why should you?
I feel like the performance costs lamented in this article mostly have to do with runtime polymorphism in static languages. And I fully agree here: runtime polymorphism is something that should be avoided when you don't need it[2]. But that's the thing: if you're looking at modern static language codebases, runtime polymorphism is not as hyped as it used to be in the past. Some languages still require heavy use of runtime polymorphism (Go is a good example of this), but other languages more often rely on static polymorphism (Rust) or compile time duck-typing (Zig and you could argue C++ template meta-programming used to do that, albeit quite awkwardly).
Even with all the issues you get with polymorphism, I don't think it's the main cause of slow application performance. It be very much the culprit in tight loops inside games, but if you look at the performance issues plaguing everyday apps, I think the two major culprits are endless layers of abstraction (the most quintessential example is basically every sluggish Electron app out there) and blocking the user on slow actions (like network loads).
---
[1] Even Java had sealed record types for a while now, and I'm sure will see Enterprise frameworks encouraging them in 20 years, when the rest of the world has moved on to spacefaring super-intelligent LLMs. But Enterprise frameworks also don't encourage you to write DRY code or keep your functions short.
[2] But do keep in mind that in Java it could be almost zero-cost in many cases. The JIT will monomorphize or bimorphize your classes if you always use the same class at the same callsite. The pointer indirection is not an extra cost, since every non-primitive that doesn't undergo Scalar Replacement[3] lives on the heap, and has a pointer.
[3] https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacemen...
Whenever I run a thing and it's unbearabily super duper slow, when you look at the process lists the thing will have spawned bunch of chromium instances - on top of probably making bunch of internet connections. Delegating some of the work that can easily done on my PC to "cloud" instead.
What we have now is way worse - it's electron and webshit technologies on desktop. Like you couldn't make software of worse quality even if you tried. The performance way worse than PCs of 1990s. It's almost like using software that's running from a floppy disk.
And now this trash is probably getting generated with LLMs.
[1] https://www.radgametools.com/granny/customers.html
If you care about performance, you don't use OOP, you don't use if/else, you don't use switch{case}, what you do is you write the hot parts in assembler.
If you aren't writing it in assembler, you're writing slow code.
But that code is still not optimised until you've implemented it in an ASIC.
Depends in part in how good you are at writing assembler.
But you're still throwing away so much time on things like bounds-checking memory accesses that never need it.