Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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.


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.


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


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.


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.


It doesn’t matter. You’re saving nanoseconds when a db access costs milliseconds. Better to focus on making the code easy to understand and change and focus your effort on optimizing the database.


you are hyperfocusing on a single example. there are contexts where it matters and others where it doesnt sure, but i dont think either approach is inherently easier to understand or maintain


The only context where the overhead of a virtual function call matters is in tight inner loops.

That's by far the easiest place to apply the "write something simple and readable and then measure and see if you need to change it" approach, so it just plainly doesn't work as argument against that approach.


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


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.


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


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.


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.


What if ALSA is that interface? AFAIK, it can load arbitrary .so's to implement snd_pcm, depending on configuration.


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.


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.


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.


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.


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


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


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.


Thank you! I always see this stupid conversation about performance and nobody seems to get this.


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


It's much easier to optimise an easy to understand program than it is to debug a highly optimised one.


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


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


You accidentally used the phrase better approach, instead of Clean Code.


This is the author's gripe, though. You're sacrificing end user experience for developer ergonomics.


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.


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.


Choosing the right data structure isn't in opposition to making simple, readable code; it's part of it. Nothing over-complicates code more than a bad choice of data structure. If you pick the data structure that simplifies your code the most, the vast majority of the time that is also the right choice for performance.

I disagree with much of the advice in Clean Code, but it has nothing to do with performance. Clean Code is bad because it produces overly-complex unreadable code. The reason that it produces poor performance isn't because the code is too readable; to the contrary, if the code was more readable it would be more obvious that it's using the wrong data structure.

I'm not saying that you shouldn't think about performance from the beginning. I'm saying that you shouldn't sacrifice simplicity and readability for the sake of performance until you are sure it's necessary because those things are rarely in opposition to each other on the macro scale.

Nothing is worse for performance than doing work you don't need to do and unreadable code tends to do a lot of that if it has been actively maintained for more than a year or two.


If you go with simple, straightforward data structures you often get adequate performance at the macro level. In my experience, getting really good performance often involves being clever with data structures. It depends how far you want to push performance.

I’ve done a lot of work optimising text CRDTs. There, the simple data structure is (essentially) a list which contains metadata for each character. But you’re constantly scanning and inserting into the list. You can improve it in two ways: first, make each list item store the metadata for a connected span of characters. Second, use a b-tree for fast insertion. Make the b-tree store aggregate metadata in internal nodes. That gets you orders of magnitude better performance - O(n) per keystroke to O(log n). RLE gets ~10x lower ram utilisation. It’s only the obvious data structure when you’ve thought about the problem a lot. And you have to write your own btree - there are no libraries for this. At least, none I have found.

> I'm saying that you shouldn't sacrifice simplicity and readability for the sake of performance until you are sure it's necessary

It really depends on the domain. If you’re making a note taking app, you probably get good enough performance by doing the obvious thing. If you’re making a browser, database, llm inference engine or 3d game engine, it pays to think about perf from the start. But my impression is that most people on this site aren’t doing that sort of thing.


That seems like a pretty niche problem. There are already numerous high quality browsers, databases, llm inference engines and 3d game engines and only a tiny portion of devs are working on that type of thing.

The vast majority of applications are better off using one of the many high-performance, battle-tested implementations of b-trees that already exist, which, for users of those implementations, is one of the simplest and most commonly used data structures; we just call them databases and filesystems instead of b-trees.

Every rule has exceptions but you should know the rules before you decide to break them. For anyone other than an experienced expert, writing your own b-tree implementation in a production system is an extremely foolish decision (if it's for fun or learning, do whatever you want).


There are already numerous iOS apps and numerous websites. And yet, people keep making more!

I think I broadly agree with your overall point. I’ve just spent a lot of my career working on niche problems like this. And there are a lot of people working on systems software. Windows, Linux, macOS, chrome, postgres, etc don’t write themselves. But unless you move in those circles, you can spend your whole life never interacting with any of those engineers.

> we just call them databases and filesystems instead of b-trees.

The b-trees I’m talking about are in memory. Btrees often outperform other kinds of in memory tree structures (avl, rb, binary, etc) because you get fewer dram memory stalls.


I'm not disagreeing that there are cases where it makes sense to implement a b-tree; I don't think there are any cases where it makes sense to implement a b-tree (in production) as a person who needs beginner-level advice about code organization.


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.


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


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

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


the toy problem is for demonstration purposes. this is a lived experienced in everyday programming of performance sensitive fields.

those apps tend to be vastly more architecturally complex in almost every way compared to your average corporate or web app too.


What?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: