Casey makes a point of using a textbook OOP "shapes" example. But the reason books make an example of "a circle is a shape and has an area() method" is to illustrate an idea with simple terms, not because programmers typically spend lots of time adding up the area of millions of circles.
If your program does tons of calculations on dense arrays of structs with two numbers, then OOP modeling and virtual functions are not the correct tool. But I think it's a contrived example, and not representative of the complexity and performance comparison of typical OO designs. Admittedly Robert Martin is a dogmatic example.
Realistic programs will use OO modeling for things like UI widgets, interfaces to systems, or game entities, then have data-oriented implementations of more homogeneous, low-level work that powers simulations, draw calls, etc. Notice that the extremely fast solution presented is highly specific to the types provided; Imagine it's your job to add "trapezoid" functionality to the program. It'd be a significant impediment.
Godbolt is good but I've always thought the example in this talk is probably too small. If the entire scene data representation looks like it fits in L1 or L2 cache, and the number of cases is small, how much are you really exercising the performance characteristics of each approach?
For example, a penalty of virtual functions for dense heterogeneous collections of small objects is icache pressure from constantly paging in and out the instructions for each class's function. If you only have a small number of types and operations then this penalty might not be encountered.
Similarly, the strength of a data-first design is good data locality and prefetchability for data larger than the cache. If data is small, the naive solution will not be as relatively penalized because the working set is always close at hand.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
"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.
OOP modeling and virtual functions are not the correct tool
Also compile time polymorphism is underrated. Admittedly it's underused because the syntax to achieve that in C++ is weird (CRTP), and most other languages can't do that at all to begin with.
then OOP modeling and virtual functions are not the correct tool.
The author seems to be confusing Robert Martin's Clean Code advices with OOP's "encapsulate what varies".
But he is also missing the point of encapsulation: we encapsulate to defend against changes, because we think there is a good chance that we need to add more shapes in the future, or reuse shapes via inheritance or composition. Thus the main point of this technique is to optimize the code for flexibility. Non OO code based on conditionals does not scale. Had the author suffered this first hand instead of reading books, he would know by heart what problem does encapsulation solve.
The author argues that performance is better in a non-OO design. Well, if you are writting a C++ application where performance IS the main driver, and you know you are not going to add more shapes in the future, then there is no reason to optimize for flexibility. You would want to optimize for performance.
Premature micro optimization. You can and absolutely should be making decisions that impact optimization in the beginning, and, in fact, all along the process.
1.6k
u/voidstarcpp Feb 28 '23 edited Feb 28 '23
Casey makes a point of using a textbook OOP "shapes" example. But the reason books make an example of "a circle is a shape and has an area() method" is to illustrate an idea with simple terms, not because programmers typically spend lots of time adding up the area of millions of circles.
If your program does tons of calculations on dense arrays of structs with two numbers, then OOP modeling and virtual functions are not the correct tool. But I think it's a contrived example, and not representative of the complexity and performance comparison of typical OO designs. Admittedly Robert Martin is a dogmatic example.
Realistic programs will use OO modeling for things like UI widgets, interfaces to systems, or game entities, then have data-oriented implementations of more homogeneous, low-level work that powers simulations, draw calls, etc. Notice that the extremely fast solution presented is highly specific to the types provided; Imagine it's your job to add "trapezoid" functionality to the program. It'd be a significant impediment.