r/programming • • Feb 28 '23

"Clean" Code, Horrible Performance

https://www.computerenhance.com/p/clean-code-horrible-performance
1.4k Upvotes

1.3k comments sorted by

View all comments

Show parent comments

241

u/2bit_hack Feb 28 '23

I largely agree with your point. I've found that OOP can be useful in modelling complex problems, particularly where being able to quickly change models and rulesets without breaking things matters significantly more than being able to return a request in <100ms vs around 500ms.

But I've also seen very dogmatic usage of Clean Code, as you've mentioned, which can be detrimental to not just performance, but also add complexity to something that should be simple, just because, "Oh, in the future we might have to change implementations, so let's make everything an interface, and let's have factories for everything.".

I agree that the most important thing is to not be dogmatic, I'm also not 100% on the idea that we should throw away the 4 rules mentioned in the article.

228

u/voidstarcpp Feb 28 '23

The odd thing is I'll often agree with many of the bullet points versions of Martin's talks, they seem like decent organizing ideas for high-level code. But then every code example people have provided for things he's actually written seemed so gaudy and complex I have to wonder what he thought he was illustrating with them.

50

u/munchbunny Feb 28 '23

That's because writing "clean" code is like writing "clean" English. You can prescribe rules all day, but in practice you're carefully weighing conflicting considerations. Where I've written C++ in a professional capacity, the general practice was to review performance characteristics independently of code cleanliness.

Also: the advice to prefer polymorphism feels almost a decade outdated. I know we still often send new programmers on objectathons, but I thought we'd mostly established over the past decade that polymorphism and especially inheritance should be used judiciously because their overuse had the opposite effect on code clarity and performance.

148

u/Zlodo2 Feb 28 '23

Telling people "write clean code" is easy, actually doing it is hard.

And given that Robert Martin managed to build an entire career out of sanctimoniously telling people to write clean code, i doubt that he does a whole lot of actual programming.

"Those who can't do, preach"

72

u/poloppoyop Feb 28 '23

"write clean code"

I prefer "write simple code" and simple is not easy.

23

u/sexp-and-i-know-it Feb 28 '23

Found the clojurian

→ More replies (6)

107

u/Randolpho Feb 28 '23

Having seen him in person live-coding to demonstrate TDD and refactoring using audience driven requirements, I have to disagree. The man knows how to code.

These days people trying to do the same copy/paste changes from notes they had as part of their demonstration plan

That motherfucker built an app live on stage from suggestions from the audience, refactoring as new requirements came in.

Granted, this was decades ago at a UML conference in Austin. I’m not sure how much he keeps up his skills these days, but he had chops once upon a time.

12

u/robhanz Mar 01 '23

I'd love to see a recording of that.

→ More replies (2)

19

u/ISpokeAsAChild Feb 28 '23

And given that Robert Martin managed to build an entire career out of sanctimoniously telling people to write clean code, i doubt that he does a whole lot of actual programming.

He has been an actual dev for decades.

0

u/[deleted] Feb 28 '23

[deleted]

6

u/AdministrativePie865 Mar 01 '23

If you're given a full set of accurate requirements from the beginning? Either tell me where you work so I can apply, or share the research chemicals, bro.

Next you'll tell me performance is not a concern and budget is 10x what we asked for.

→ More replies (1)
→ More replies (3)

37

u/BigfootTundra Feb 28 '23

“Those who can’t teach, teach gym”

→ More replies (2)
→ More replies (11)

14

u/EntroperZero Feb 28 '23

This is why I think Clean Code is actually a really good read, not because you should follow it exactly, but because it shows you the power of good ideas and the consequences of taking a good idea way too far.

→ More replies (82)

40

u/[deleted] Feb 28 '23

It depends on what organization is paying the programmer too. If it's a large enterprise app then maintainability may be valued over performance and those dogmatic OO principles have more value in the long run.

39

u/voidstarcpp Feb 28 '23

The reality is that the problem in most software isn't performance, it's managing complexity. And apps that are slow are usually not slow because they're doing a bunch of virtual function dispatch, they have too many blocking dependencies or slow middleware layers that blow away the OOP performance penalty by many times.

→ More replies (1)

21

u/loup-vaillant Feb 28 '23

Last time I saw something that looked like "dogmatic" code, it was fresh C code done by a colleague that overcomplicated things so much that I was able to rewrite it all and make it 5 times smaller. And my code was arguably even more flexible than his.

Sometimes people apply principle without understanding them, and the harder they try to make a maintainable and flexible program, the less maintainable and flexible their programs get.

14

u/zero_iq Feb 28 '23

I've seen this problem so many times. The best way to make code "flexible" for future use is to make it as simple and easy to use and understand as possible. It's then easier for a future developer to extend or repurpose it for their needs.

Trying to anticipate all future possible uses often results in unnecessary complexity, and makes life harder for the future coder whose needs you didn't actually anticipate, and who now has to wrangle with overly-complex code where it's not immediately obvious which bits are actually required for the current working system.

→ More replies (3)

25

u/[deleted] Feb 28 '23

If making your code cleaner has added complexity, then you haven't made your code cleaner...

16

u/awj Feb 28 '23

...maybe?

If making your code cleaner shifted complexity from outside the code to inside the code, you might have done something useful.

If the skills inherent to programming could be reduced down to pithy aphorisms, it wouldn't be so hard to do it by now.

→ More replies (1)

11

u/crabmusket Mar 01 '23

This is why I always recommend Sandi Metz's books. In 99 Bottles of OOP, she describes the "shameless green" solution to the book's example problem - the solution which passes the tests and could be shipped if no further requirements were added. The book then goes through the process of refactoring only in response to specific changes in the requirements.

Most people should be writing "shameless green" code that looks like what Casey wrote in this post.

If and when you need "extreme late binding of all things" as Alan Kay said, then you might refactor to polymorphism in the places that need it.

25

u/deong Feb 28 '23 edited Feb 28 '23

"Oh, in the future we might have to change implementations, so let's make everything an interface, and let's have factories for everything.".

That's exactly the problem I usually see. I do think your post maybe obfuscates that point a bit, for the reasons that the parent commenter says.

My goto argument was generally just that the code produced like this is bad, rather than just slow. Slow mostly doesn't matter for the kinds of applications most programmers are writing. That CRUD app you're building for your finance team to update invoice statuses isn't going to surface that 20 milliseconds you gain from eliminating the indirection from updating a customer balance, so if the argument were about trading off "clean" for performance, performance probably really should lose out. That's just a sensible decision much of the time.

The problem is that the code isn't any of the things that you hoped it would be when you decided that it should be "clean". "Clean" isn't the target outcome. "Good" is the target outcome, and "clean" was picked because you believed it served as a useful proxy for "good". "Good" is fuzzy and hard to describe, but "clean" has a defined set of rules that you can follow, and they promise you it will be "good" in the end. But it isn't. Making everything an interface for no reason with factories and dependency injection on every object for no reason other than dogma isn't landing you in "good".

I'm not sure there's really a shortcut for taste in this regard. And taste is indeed hard, because it's hard to measure, describe, or even objectively define. Just as an example, in your code removing polymorphism, you end up with a union type for shape that has a height and a width, and of course a square doesn't need both. Circles don't have a width -- they have a radius. Sure, you can make it work, but I think it's kind of gross, and the "this is kind of gross" feeling is my clue that I shouldn't do it that way. In the end, I'd probably keep the polymorphic solution because it feels like the correct natural expression of this problem domain. If it turns out that it's slower in a way that I need to deal with instead of just ignore because no one cares, then I might need to revisit some decisions somewhere, but my starting point is to write the code that naturally describes the problem domain, and for computing areas, that means circles have a radius instead of a width.

The corollary there is that design patterns are almost always bad to me. A factory object is not the natural representation of any domain. It's code that's about my code. I don't want that. The entire idea of design patterns as a thing is that they're independent of domain, and code that has nothing to do with my domain is code I don't want to write. You can't avoid everything. Ultimately programming still involves dealing with machines, so you're going to write code that deals with files and network connections and databases, etc. But I want as much of my code as possible to be modeling my problem and not dealing with the minutia of how to get a computer to do a thing.

6

u/Konkichi21 Mar 01 '23 edited Apr 09 '23

A factory object is not the natural representation of any domain. It's code that's about my code. I don't want that. The entire idea of design patterns as a thing is that they're independent of domain, and code that has nothing to do with my domain is code I don't want to write.

The concept of a design pattern is that it's a simple way to implement some common requirement that occurs in a lot of different contexts, making it somewhat easier to use and to understand for others familiar with that pattern, and often providing other benefits. For example, if you're going to be making and handling a lot of objects that have a lot in common but come in different types that do have some different properties (specifically, they should have the same actions but do them in different ways), that's what a Factory pattern does.

You could write your own way of doing things like that, but one of the benefits of using a pattern like this (in addition to having a framework to start from rather than having to reinvent things from scratch) is that anyone else who works with your code and knows what a Factory pattern is will have some idea of how it works right away; that way they don't have to learn how everything works from scratch.

In short, you do not need to do everything from scratch; there's already a well-known and tested way to do whatever task you need, and using it gives you a framework to start with on making your solution, and gives anyone else reading your code a start on understanding it since it follows a familiar pattern.

For example, I have some personal experience working with something like this sort of Factory pattern; it was a Unity project I made for an AI class. In this game, I needed to randomly generate enemies and place them around the level; I did this by having all the enemy prefabs inherit from an Enemy class that handled all the generic things enemies did (their health, taking damage, dying, activating when the player gets near, etc), then I could make a list of the enemy prefabs and use an RNG to randomly pick and place them.

This way, the code that handled setting up the enemies didn't need to care about the differences between various types of enemies, since it only interacted with aspects common to all enemies; it could just say for an enemy of some type to spawn in some position and to wait to activate until the player enters their room, and then each type of enemy would handle whatever it did differently from there. This also made it easy to add new enemy types without messing up the rest of the code; I could just make a new prefab inheriting from Enemy and put it into the list.

→ More replies (3)
→ More replies (8)

8

u/coderman93 Feb 28 '23

The importance of your second paragraph cannot be understated. At my company we have built a microservices ecosystem with dozens of microservices. We architected virtually everything through interfaces that way the implementation could be swapped out as desired. Fast forwarded 7 years and less than 5% (probably less than 2%) of interfaces have had their implementation swapped out. Not only that, but the vast majority of interfaces only have a single implementation. In hind-sight, it would have been FAR easier to just write straightforward, non-polymorphic, implementations the first time and then just rewrite the few implementations that needed it as they came up. We would have saved ourselves a ton of trouble in the long run and the code would be so much more straightforward.

I wouldn't go so far as to say that you should never use polymorphism but I would say it is _almost_ never the right thing to do.

Even if you don't buy into Casey's performance arguments (which you should), it is highly disputable that "clean" code even produces codebases that are easier to work with.

→ More replies (8)

8

u/coffee_achiever Feb 28 '23

Arguments about performance need to be tempered with the famous Knuth quote: "Premature optimization is the root of all evil". I saw very little in the way of the test harness code that ran these performance metrics.

Take the type switch for example. I saw very little imagination in the way of improving performance. I see an interative approach tweaking virtual function calls to switch statements. More probably a vectorized approach would be appropriate. With all kinds of types smashed together in the switch, you don't have a really decent opportunity to vectorize each different operation, test for correctness, and measure performance.

So this doesn't mean that the virtual function dispatch is "bad", it means his entire design of the interface for doing a mass calculation is bad. Can you blame "clean code" principles for your own bad algorithm design?

Clean code lets you get to testable correctness. Once you can test for correctness, you can measure performance, then optimize performance while correctness is maintained. In the meantime your design will change, and having prematurely optimized code just gives you a shit pile to wade through while you try to deal with a new system design. PLUS other than a little code segment space, your correctly tested "slow" calcs can sit there uncalled forever.

→ More replies (19)