By Aleksander Demko, March, 2024.
Object-oriented programming (OOP) is a programming paradigm that, although invented and practiced much earlier, really took off in the 1990s. However, recently (say 2015 onwards) it seems that everyone was promoting and predicting its demise in favor of functional programming. Yet, after a decade, it hasn’t happened yet at all. OOP still mostly rules the programming world. I’d like to present my thoughts on why I think it’s not going anywhere.
Before OOP software development was kind of chaotic. We had floating functions that operated on semi-related variables in structs. We tried to group the functions in modules (if the language supported them) or used common prefixes (if not) to help organize them, but it was still messy.. It was not obvious which functions worked on what items and in what order. Discovery was really hard.
For example, the open() C function returns an integer. What do you do with that integer? What other operations are available on it? Can I do math with it? Can it be copied around? Do I have to do anything to clean it up? The man page for open, although very detailed about all the technical options, doesn’t really cover any of these questions. Now a “file” is one of the most core and fundamental “objects” out there, but these questions apply to almost any “object” you have in software, so this problem is very universal.
OOP on the other hand handles all these questions in a consistent manner. The object itself is opaque and you don’t have to worry about what’s inside of it. The other operations are listed in the class definition grouped explicitly in the documentation or by pressing “.” and letting your edit show you the list of methods. You can copy it if there is a copy constructor. Cleanup is handled by the destructor. This logic applies to all objects consistently.
During the 1990s OOP was taking off like wildfire. It was everywhere and it spread by itself - it didn’t seem to have any single pusher or proponent. Although Smalltalk was invented two decades prior and C++ one decade prior, it was the 90s where adoption really took off. C++ was hot and many other languages (such as Turbo Pascal & Java) joined the hype and added OOP features or were born with them. The programming media (ie magazines) raved about, providing introductions and overviews of its benefits. The Design Patterns book were released and provided a much-useful taxonomy of OOP software patterns.
In the 2000s the OOP hype settled and just settled everywhere. There was no more debate or hype. All languages had it and life went on. We grouped our state with their functions. We made class hairchies, some large, some small. We made pluggable systems that were easy to extend. We followed the patterns in the big.
In the mid-2010s the functional programming movement proponents started to appear. Maybe they rediscovered Lisp or perhaps Haskell’s stable release emboldened them. The examples showing off functional code were typically elegant, but very small. They threw a lot of stones at OOP claiming it was confusing and a huge mistake and that we should all write pure functions. They hand waved away the major shortcomings of pure functional programming, which include: If you truly want to have immutable data, then that means you’ll be doing a lot of unnecessarily data copying. Copying a large list just to change one element is nonsense. This will stress your garbage collector, waste memory and cause CPU cache misses. Computers think in state, so you can’t avoid it and pretend it doesn’t. A database connection is stateful, as are files. Memory is volatile and you only have so much of it. You will still need to reconcile them in your code. Humans also think in state. When we want to open the oven door, we do overInstance.open(). We don’t return a new copy of the oven who’s door is open. Recursion as your go-to for iteration is crazy. It is much harder to reason about recursion than a for-loop. Recursion also negates the ability to do more efficient searches and queries. Abusing the stack like this is also terrible for CPU and memory, making one depend on efficient tail call recursion in their compiler or runtime just to get back decent performance. Functional programs are supposed to be “infinite scalability” but this capability never to be utilized or when attempted, has poor performance. EIther the work unit is too tiny or the overhead of scheduling and data copying too great. If you truly want multiprocessing, you still need to think about shared state and efficient locking.
The take over of pure functional programs has simply never arrived. Haskell didn’t take over the world. Except for the old workhorses like C & Fortran, all of the most popular general purpose languages had most of the features of OOP. Even new languages like Go or Rust, to claim not be OOPy, had to have 3 of the 4 OOP features.
In all my years I still have yet to see a non-OOP code base. Everyone seems to organize around objects. Even in non-OOP languages like C, nice code bases tend to group their states and methods together, essentially making objects. Old programmers haven’t changed and younger programmers (who supposedly would not have been “tainted by OOP”) still seem to be mostly coding in an OOP style. Except for some token list or stream processing uses, functional programming is mostly a small add-on pattern to existing OOP languages. Functional programming did not have the wildfire take off that OOP had in the 90s - not even close.
So where do we go from here? Like the answer to all things, it’s about moderation.
Use OOP but don’t over do it. Don’t do abstractions too early - save polymorphism for when you’ve proven you need it, such when you actually have a second implementation. Don’t make your class hierarchies deep - try to keep them flat. Consider using containment (has-A) occasional over inheritance (iis-A) once in a while. Be careful with inheritance and especially be careful with multiple inheritance (if your language supports it). Do you use generic types to let the type system help. Do read the design patterns book. Do understand that there are different classes, such as pure data, mostly static, interface, abstract base classes and implementation/descendant classes. Keep your ownership and construction/destruction patterns obvious. Avoid global variables, but do formalize them when you do need them. See my post on that. Be careful of any absolute patterns such as those exposed in Clean Code or in the SOLID principles.
Do use functional programming here and there. Recursive iteration is dangerous (from a complexity and performance point of view) and large immutable types are just wasteful. Embrace and understand state. All things being equal though, do prefer functions that don’t mutate their inputs. Trying to limit your use of global variables. Do use streams/list iteration when it makes the code easier to read.
As with all things, there are no cut and dry absolute answers. Everything is about moderation. Don’t blame OOP for when it’s misused or overused - blame the code(er). Use what works in each situation. Perhaps a functional style here or a polymorphic OOP hierarchy there. Reason about your code and consider the alternatives. Write code that you can maintain and be proud of.
For the purposes of discussions, I’d like to include my own definitions of OOP and functional programming here. Although definitions seem to vary on the web now, these are the ones I was given when I started to learn how to program.
I consider a programming language if it provides features to help the programmer with the following OOP principles:
Abstraction - Ability to model concepts as objects & classes. Encapsulation - Provides the ability to hide internal state behind groups of functions (aka methods). This grouping is called an object. Polymorphism (aka dynamic dispatch) extensibility. Allows for the object-implementations to be swapped out and changed at runtime without having to change the consuming code. Inheritance - Allows for classes to reuse code from others classes directly in their implementation and/or interface.
Note that it is possible to develop OOP software in a non-OOP language if you follow these styles. For example, the GObject library provides you with some features that help build OOP software in C, a non-OOP language.
Functional programming is defined as applying (and especially) composing functions that map data to another data. Data and functions are not merged in this paradigm.
In a truly functional language, the language follows the mathematical definition of functions in that they: Do not access any data outside of their input list. That is, they do not access any globals. Do not modify their inputs in any way. They instead create new outputs.
With these properties it means that you can get functions that are perfectly deterministic. This means that their outputs can be easily testable and cacheable. They’re also immutable and theoretically easy to parallelize.
However, I’m confident to say that almost no real world programs follow these strict guidelines 100%. In fact, despite a decade (or more) or functional features being added to mainstream languages, I’d say that most programs are at best, 10% functional. They’re mostly a combination of the OOP, procedure & imperative paradigms.