The No-Frauds Club » Veeq7 » Case against OOP

DON'T POOP YOURSELF!

A case against OOP




Primer


I, as a programmer, started in object oriented programming, in a language called Java. It was the natural way of approaching problems for me, but it never felt quite right. During last few years I was revising it. This is a summary of my conclusions and what I learned, made both for my own self reflection and to hopefully aid others in their programming journey.

This case is based on my few years of exploring alternatives and the research I have been doing along the way. If you are not sold on the points I am making, please at least try the style I am suggesting, I am pretty sure you will enjoy programming more and produce better software objectively (in terms of performance, flexbility, correctness, etc.). Along the way I tried many languages, but finally I arrived at C, thus most of what I will say will be related to C++. General advice should still be applicable to any programming language.



What's wrong


Looking back I now recognize that the thing I loved about OOP when I first learned it, was the ability of defining my own data types. That is possible without OOP, that predates OOP. You can abandon all the unecessary crap around it, use performant solutions, and actually understand the problems you are facing.

Here are the aspects of OOP that are believed to be a modern panacea, but are indeed wrong, counter productive, and less performant. Abandon the holy grail, as it is only a mirage!

Keep in mind that nothing that OOP has done is unique to OOP, there are much better ways of achieving what OOP does, but both more performantly and in a way that is way more maintainable and powerful. Those techniques existed before OOP was born and are superior to it in every way. I will be noting those solutions at the end of each chapter.



The Orientedness and Inflexibility


Orienting your codebase on objects will result in a design that is basically upside down. You are starting with structure for something that doesn't exist yet. You are enforcing a course of action, for what you yet don't know. Imposing a solid and complex structure to a problem that is not yet solved will only make the codebase inflexible and will make your progress slower.

As Casey Muratori says, the problem with OOP is not in the Objects but in the Orientedness. The urge of fracturing your code in to small pieces of encapsulation. The urge of approaching your problem with actors, not actions. That is the number one problem that creates inflexibility, code fragility, and huge performance issues.

Instead:
Try starting with what your code wants to accomplish, think in terms of actions, not actors. Create data types necessary to what you are doing, but don't overthink them. Keep them simple!



Layers of Abstraction


OOP loves abstraction, but contrary to what OOP apologists say, abstraction doesn't make stuff simpler, it makes them holistically more complex. The appeal is mostly that in the lower scale, as a user of such abstraction, you don't need to think too much about the whole system, you are just adding few additonal things on top of the black box abstraction. Unfortunately this isn't quite a healthy way of doing stuff for the long run. Abstractions will bite you back. The performance problems will creep up. Your holistic understanding will be fragile. Bugs will never end!

Instead:
Invest some time into learning how things you are using actually work. Keep in mind that you are using a physical machine, don't think about it as a magic box that does computation for no cost. Try removing code instead of adding more. If you can acomplish the same thing with less code, you will have less code to maintain.



Encapsulation and Object Hierarchies


By design, object oriented programming enforces object hierarchies, where objects interact with other objects using methods, causing mutation of private states.

The original idea here was to prevent the mutation of global state, using a tree of responsibility. Unfortunately that did not work out, state is just fractured around the codebase entire, and objects still reach out for objects at the top of hierarchy all the time. This is not a solution, it only hides a way the real problem, while introducing more complexity.

Additionally the tying functions to data types introduces a problem of fake data types (sometimes as stupid as "Abstract Object Controller Factory"), which introduces a burden of managing instances to actors that should have just been ordinary functions.

Mutation of global state is scary, but unavoidable. If your program is any complex (in other words it does something useful) you will have some sort of global state anyways. Removing globally scoped variables or hiding the state across objects won't resolve the issue. Quality of your code is the only thing that will, that's why it's important to keep the code simple and minimal.

The tying of functions with data types also has an unfortunate side effect, where you will often have to spend time thinking which data type should be a primary owner of the function. For example should message send itself? Should it have a sender? Should it have a receiver? A transmitter? The answer is no! Keep it simple!

Instead:
Avoid methods, use ordinary functions instead. Organize your big data chunks into structures, where you keep all members public. Pass that state as function parameters. You can use globals sometimes (especially for OS resources like window handles), but be careful since those can be difficult to debug.

Bonus:
If your language of choosing supports function overloading, you can keep your function names really simple, fe.
update(entity); instead of entity->update(); or
add(v1, 1.0); instead of v2_add_float(v1, 1.0);



Inheritance, Virtual Functions, and Cache Performance


It's clear to even the OOP people that inheritance is indeed fragile. One change in a base class can easily cascade to a myriad of issues in the entire code base. Multi level inheritance is not really advised by anyone anymore.

Virtual functions allow for specialization, which is great, but they aren't implemented in the best of ways. They add a VTable to your class which increases it's size. VTables are also immuttable and hidden from the progammer, way less flexible than function pointers. Calling virtual functions results in pointer chasing, which might result in a death of thousand paper cuts.

Unfortunate part of inheritance implementation in C++ is that it adds all the new members at the end of the class, instead of just having a pointer to the new data. This makes the class size vary per subclass. If you would want an array of such objects, what you would have to do would be to allocate those objects at heap and store the pointers to them in the array, this is really bad in terms of cache coherancy, since you will always have a cache miss when iterating over those (since they are in different places in the heap). Alternatively you could have each subtype in a different array, but that kinda defeats the point, you would no longer be able to easily iterate over every element.

Instead:
Use enums together with unions to handle your special cases, unlike inheritance this approach allows for fixed sized structures and more than one specialization. You can also try function pointers, they are way more flexible than vtables, since they can be altered at any time.

Bonus:
Group your types in a way that reduces amounts of if statements you need to do, so for example instead of having isActive boolean, have a separete array for active and inactive entities. Try to group them by subtype, so for example one array for Units and another for Projectiles. You can still keep one that has all subtypes. In this way you can easily reduce the amount of branch mispredictions cache misses.



Modern Technologies


Modern technologies don't matter as much as people think. Don't fall into the trap of the shinniest newest framework. Avoid Standard Template Library. Avoid RAII. Minimize the usage of templates. Avoid prebuilt game engines. Those things usually have hidden costs that you might not see at first, be it performance, build times, or abstraction. It's all just trivia, not actual knowledge!

Instead:
Write your own solutions, focus on the real knowledge. Don't worry about trivia as it will always change. If you have sufficient knowledge, trivia will be just another inconvience, something that you will naturally be able to deal with.



Sources