Object-Oriented Programming is The Biggest Mistake of Computer Science
C++ and Java probably are some of the worst mistakes of computer science. Both have been heavily criticized by Alan Kay, the creator of OOP himself, and many other prominent computer scientists. Yet C++ and Java paved the way for the most notorious programming paradigm — the modern OOP.
Its popularity is very unfortunate, it has caused tremendous damage to the modern economy, causing indirect losses of trillions upon trillions of dollars. Thousands of human lives have been lost as a result of OOP. There’s no industry that went untouched by the latent OO crisis, unfolding right before our eyes for the past three decades.
Why is OOP so dangerous? Let’s find out.
Imagine taking your family out for a ride on a beautiful Sunday afternoon. It is nice and sunny outside. All of you enter the car, and you take the exact same highway that you’ve already driven on a million times.
Yet this time something is different — the car keeps accelerating uncontrollably, even when you release the gas pedal. Brakes aren’t working either, it seems they’ve lost their power. In a desperate attempt to save the situation, you pull the emergency brake. This leaves a 150-feet long skid mark on the road before your car runs into an embankment on the side of the road.
Sounds like a nightmare? Yet this is exactly what has happened to Jean Bookout in September, 2007 while she was driving her Toyota Camry. This wasn’t the only such incident. It was one of the many incidents related to the so-called “unintended acceleration”, which has plagued Toyota cars for well over a decade, causing close to 100 deaths. The car manufacturer was quick to point fingers at things like “sticky pedals”, driver error, and even floor mats. Yet some experts have long suspected that faulty software might have been at play.
To help with the problem, software experts from NASA have been enlisted, to find nothing. Only a few years later, during the investigation of the Bookout incident, the real culprit was found by another team of software experts. They’ve spent nearly 18 months digging through Toyota code. They’ve described the Toyota codebase as “spaghetti code” — a programmer lingo for tangled mess of code.
The software experts have demonstrated more than 10 million ways for the Toyota software to cause unintended acceleration. Eventually, Toyota was forced to recall more than 9 million cars, and paid over $3 billion in settlement fees and fines.
Is spaghetti code a problem?
100 human lives taken by some software fault is way too many. What makes this really scary is that the problem with Toyota code isn’t unique.
Two Boeing 737 Max airplanes have crashed, causing 346 deaths, and more than 60 billion dollars in damage. All because of a software bug, with 100% certainty caused by spaghetti code.
Spaghetti code plagues way too many codebases worldwide. On-board airplane computers, medical equipment, code running on nuclear power plants.
Program code isn’t as much written for the machines, as it is written for fellow humans. As Martin Fowler has said, “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
If the code doesn’t run, then it’s broken. Yet if people can’t understand the code, then it will be broken. Soon.
Let’s take a quick detour, and talk about the human brain. The human brain is the most powerful machine in the world. However, it comes with its own limitations. Our working memory is limited, the human brain can only think about 5 things at a time. This means, that program code should be written in a way that doesn’t overwhelm the human brain.
Spaghetti code makes it impossible for the human brain to understand the codebase. This has far-reaching consequences — it is impossible to see if some change will break something else. Exhaustive tests for flaws become impossible. No one can even be sure if such a system is working correctly. And if it does work, why does it even work?
What causes spaghetti code?
Why does code become spaghetti code over time? Because of entropy — everything in the universe eventually becomes disorganized, chaotic. Just like cables will eventually become tangled, our code eventually will become a tangled mess. Unless sufficient constraints are put in place.
Why do we have speed limits on the roads? Yes, some people will always hate them, but they prevent us from crashing to death. Why do we have markings on the road? To prevent people from going the wrong way, to prevent accidents.
A similar approach would totally make sense when programming. Such constraints should not be left to the human programmer to put in place. They should be enforced automatically, by tooling, or ideally by the programming paradigm itself.
Why is OOP the root of all evil?
How can we enforce sufficient constraints to prevent the code from turning into spaghetti? Two options — manually, or automatically. Manual approach is error-prone, humans will always make errors. Therefore, it is logical for such constraints to be automatically enforced.
Unfortunately, OOP is not the solution we’ve all been looking for. It provides no constraints to help with the problem of code entanglement. One can become proficient in various OOP best practices, like Dependency Injection, test-driven Development, Domain-Driven Design, and others (which do help). However, none of that is enforced by the programming paradigm itself (and no such tooling exists that would enforce the best practices).
None of the built-in OOP features help with preventing spaghetti code — encapsulation simply hides and scatters state across the program, which only makes things worse. Inheritance adds even more confusion. OOP polymorphism once again makes things even more confusing — there are no benefits in not knowing what exact execution path the program is going to take at runtime. Especially when multiple levels of inheritance are involved.
OOP further exacerbates the spaghetti code problem
The lack of proper constraints (to prevent code from becoming a tangled mess) isn’t the only drawback of OOP.
In most object-oriented languages, everything by default is shared by reference. Effectively turning a program into one huge blob of global state. This goes in direct conflict with the original idea of OOP. Alan Kay, the creator of OOP had a background in biology. He had an idea for a language (Simula), that would allow writing computer programs in a way that resembles biological cells. He wanted to have independent programs (cells) communicate by sending messages to each other. The state of the independent programs would never be shared with the outside world (encapsulation).
Alan Kay never intended for the “cells” to reach directly into the internals of other cells to make changes. Yet this is exactly what happens in modern OOP, since in modern OOP, by default, everything is shared by reference. This also means that regressions become inevitable. Changing one part of the program will often break things somewhere else (which is much less common with other programming paradigms, like Functional Programming).
We can clearly see that modern OOP is fundamentally flawed. It is the “monster” that will torture you every day at work. And it will also haunt you at night.
Let’s talk about predictability
Spaghetti code is a big problem. And Object-Oriented code is especially prone to spaghettification.
Spaghetti code makes software unmaintainable. Yet it is only a part of the problem. We also want software to be reliable. But that is not enough, software (or any other system for that matter) is expected to be predictable.
A user of any system should have the same predictable experience, no matter what. Pressing the car accelerator pedal should always result in the car speeding up. Pressing the brakes should always result in the car slowing down. In computer science lingo, we expect the car to be deterministic.
It is highly undesirable for the car to exhibit random behaviors, like the accelerator failing to accelerate, or the brakes failing to brake (the Toyota problem). Even if such issues occur only once in a trillion times.
Yet the majority of software engineers have the mindset of “the software should be good enough for our customers to keep using it”. Can’t we really do any better than that? Sure, we can, and we should do better than that! The best place to start is to address the nondeterminism of our programs.
— Wikipedia article on Nondeterministic Algorithms
If the above Wikipedia quote on nondeterminism doesn’t sound good to you, it is because nondeterminism isn’t any good. Let’s take a look at a code sample that simply calls a function:
We don’t know what the function does, but it seems that the function always returns the same output, given the same input. Now let’s take a look at another example that calls another function,
This time, the function has returned different values for the same input. What is the difference between the two? The former function always produces the same output, given the same input, just like functions in mathematics. In other words, the function is deterministic. The latter function may produce the expected value, but this is not guaranteed. Or in other words, the function is nondeterministic.
What makes a function deterministic or nondeterministic?
- A function that does not depend on external state is 100% deterministic.
- A function that only calls other deterministic functions is deterministic.
In the above example,
computea is deterministic, and will always give the same output, given the same input. Because its output depends only on its argument
On the other hand,
computeb is nondeterministic because it calls another nondeterministic function
Math.random(). How do we know that
Math.random() is nondeterministic? Internally, it depends on system time (external state) to calculate the random value. It also takes no arguments — a dead giveaway of a function that depends on external state.
What does determinism have to do with predictability? Deterministic code is predictable code. Nondeterministic code is unpredictable code.
From Determinism to Nondeterminism
Let’s take a look at an addition function:
We can always be sure, that given the input of
(2, 2) , the result will always be equal to
4 . How can we be so sure? In most programming languages, the
addition operation is implemented on the hardware, in other words, the CPU is responsible for the result of the computation to always remain the same. Unless we’re dealing with the comparison of floating-point numbers, (but that is a different story, unrelated to the problem of nondeterminism). For now, let’s focus on integers. The hardware is extremely reliable, and it is safe to assume that the result of addition will always be correct.
Now, let’s box the value of
So far so good, the function is deterministic!
Let’s now make a small change to the body of the function:
What happened? Suddenly the result of the function is no longer predictable! It worked fine the first time, but on every subsequent run, its result started getting more and more unpredictable. In other words, the function is no longer deterministic.
Why did it suddenly become non-deterministic? The function has caused a side effect by modifying a value outside of its scope.
A deterministic program guarantees that
2+2==4 . In other words, given an input of
(2, 2) , the function
add , should always result in the output of
4 . No matter how many times you call the function, no matter whether or not you call the function in parallel, and no matter what the world outside of the function looks like.
Nondeterministic programs are the exact opposite. In most of the cases, the call to
add(2, 2) will return
4 . But once in a while, the function might return 3, 5, or even 1004. Nondeterminism is highly undesirable in programs, I hope you can now understand why.
What are the consequences of nondeterministic code? Software defects, or as they are more commonly referred to as “bugs”. Bugs make the developers waste precious time debugging, and significantly degrade the customer experience if they made their way into production.
To make our programs more reliable, we should first and foremost address the issues of nondeterminism.
This brings us to the problem of side effects.
What is a side effect? If you’re taking medication for headache, but that medication is making you nauseous, then the nausea is a side-effect. Simply put, something undesirable.
Imagine, that you’ve purchased a calculator. You bring it home, start using it, and then suddenly realize that this is not a simple calculator. You got yourself a calculator with a twist! You enter
10 * 11 , it prints
110 as the output, but it also yells ONE HUNDRED AND TEN back at you. This is a side effect. Next, you enter
41+1 , it prints
42 , and comments “42, the meaning of life”. Side effect as well! You’re puzzled, and start talking to your significant other that you’d like to order pizza. The calculator overhears the conversation, says “ok” out loud, and places a pizza order. Side effect as well!
Let’s get back to our addition function:
Yes, the function performs the expected operation, it adds
b . However, it also introduces a side-effect. The call to
a.value += b.value has caused the object
a to change. The function argument
a was referring to the object
two , and therefore
two.value is no longer equal to
2 . Its value became
4 after the first call,
6 after the second call, and so on.
Having discussed both determinism and side-effects, we’re ready to talk about purity. A pure function is a function that is both deterministic, and has no side effects.
Once again, deterministic means predictable — the function will always return the same result, given the same input. And no side effects means that the function will do nothing other than returning a value. Such function is pure.
What are the benefits of pure functions? As I’ve already said, they’re predictable. This makes them very easy to test (no need for mocks and stubs). Reasoning about pure functions is easy — unlike in OOP, there’s no need to keep in mind the entire application state. You only need to worry about the current function you’re working on.
Pure functions can be composed easily (since they don’t change anything outside of their scope). Pure functions are great for concurrency, since no state is shared between functions. Refactoring pure functions is pure joy — just copy and paste, no need for complex IDE tooling.
Simply put, pure functions bring the joy back into programming.
How pure is Object-Oriented Programming?
For the sake of an example, let’s talk about two features of OOP — getters and setters.
The result of a getter depends on external state — the object state. Calling a getter multiple times may result in different output, depending on the state of the system. This makes getters inherently non-deterministic.
Now, setters. Setters are meant to change state of an object, which makes them inherently side-effectful.
This means that all methods (apart maybe from static methods) in OOP are either non-deterministic, or cause side-effects, neither of each is good. Hence, Object-Oriented Programming is anything but pure, it is the complete opposite of pure.
There’s a silver bullet.
Yet few of us dare to try it.
Being ignorant is not so much a shame, as being unwilling to learn.
— Benjamin Franklin
In the gloomy world of software failures, there is a ray of hope, something that will solve most, if not all of our problems. A true silver bullet. But only if you’re willing to learn and apply — most people aren’t.
What is the definition of a silver bullet? Something that can be used to solve all of our problems. Is mathematics a silver bullet? If anything, it comes very close to being a silver bullet.
We owe it to the thousands of extremely intelligent men and women who worked hard for millennia to give us mathematics. Euclid, Pythagoras, Archimedes, Isaac Newton, Leonhard Euler, Alonzo Church, and many many others.
How far do you think our world would go if something nondeterministic (i.e. unpredictable) would be the backbone of modern science? Likely not very far, we’d stay in the middle ages. This has actually happened in the world of medicine — in the past there were no rigorous trials to confirm the efficacy of a particular treatment or medication. People relied on the opinion of doctors to treat their health problems (which unfortunately still happens in countries like Russia). In the past, ineffective techniques like bloodletting have been popular. Something as unsafe as arsenic was widely used.
Unfortunately, the software industry of today is way too similar to the medicine of the past. It is not based on solid foundation. Instead, the modern software industry is mostly based on a weak wobbly foundation, on called Object-Oriented Programming. Had human lives directly depended on software, OOP would long be gone and forgotten, just like bloodletting and other unsafe practices.
A solid foundation
Is there an alternative? Can we have something as reliable as mathematics in the world of programming? Yes! Many mathematical concepts translate directly to programming, and lay the foundation of something called Functional Programming.
Functional Programming is the mathematics of programming — an extremely solid and robust foundation, that can be used to build solid and robust programs. What makes it so robust? It is based upon mathematics, Lambda Calculus in particular.
To draw a comparison, what is the modern OOP based upon? Yes, the proper Alan Kay OOP was based on biological cells. However, the modern Java/C# OOP is based on a set of ridiculous ideas such as classes, inheritance and encapsulation, it has none of the original ideas that the genius of Alan Kay has invented. And the rest is simply a set of bandaids to address the shortcomings of its inferior ideas.
What about functional programming? It’s core building block is a function, in most cases a pure function. Pure functions are deterministic, which makes them predictable. This means that programs composed of pure functions will be predictable. Will they always be bug-free? No, but if there’s a bug in the program, it will be deterministic as well — the same bug will always occur for the same inputs, which makes it easier to fix.
How did I get here?
In the past, the
goto statement was widely used in programming languages, before the advent of procedures/functions. The
goto statement simply allowed the program to jump to any part of the code during execution. This made it really hard for the developers to answer the question “how did I get to this point of execution?”. And yes, this has caused a large number of bugs.
A very similar problem is happening nowadays. Only this time the difficult question is “how did I get to this state” instead of “how did I get to this point of execution”.
OOP (and imperative programming in general) makes answering the question of “how did I get to this state?” hard. In OOP, everything is passed by reference. This technically means, that any object can be mutated by any other object (OOP places no constraints to prevent that). And encapsulation doesn’t help at all — calling a method to mutate some object field is no better than mutating it directly. This means, that the programs quickly turn into a mess of dependencies, effectively making the whole program a big blob of global state.
What is the solution to make us stop asking the question “how did I get to this state”? As you may have already guessed, functional programming.
A lot of people in the past have resisted the recommendation to stop using
goto, just like many people of today resist the idea of functional programming, and immutable state.
But wait, what about spaghetti code?
In OOP, it is considered best practice to “prefer composition over inheritance”. Such best practices should theoretically help with spaghetti code. Unfortunately, this only is a “best practice”. The object-oriented programming paradigm itself places no constraints to enforce such best practices. It’s up to the junior developers on your team to follow such best practices, and for them to be enforced in code reviews (which won’t always happen).
What about functional programming? In functional programming, functional composition (and decomposition) is the only way to build programs. This means that the programming paradigm itself enforces composition. Exactly what we’ve been looking for!
Functions call other functions, bigger functions are always composed from smaller functions. And that’s it. Unlike in OOP, composition in functional programming is natural. Furthermore, this makes processes like refactoring extremely easy — simply cut code, and paste it into a new function. No complex object dependencies to manage, no complex tooling (e.g. Resharper) needed.
One can clearly see that OOP is an inferior choice for code organization. A clear win for functional programming.
But OOP and FP are complementary!
Sorry to disappoint you. They’re not complementary.
Object-oriented programming is the complete opposite of functional programming. Saying that OOP and FP are complementary probably is the same as saying that bloodletting and antibiotics are complementary… Are they?
OOP violates many of the fundamental FP principles:
- FP encourages purity, whereas OOP encourages impurity.
- FP code fundamentally is deterministic, and therefore is predictable. OOP code is inherently nondeterministic, and therefore is unpredictable.
- Composition is natural in FP, it is not natural in OOP.
- OOP typically results in buggy software, and spaghetti code. FP results in reliable, predictable, and maintainable software.
- Debugging is rarely needed with FP, more often than not simple unit tests will do. OOP programmers, on the other hand, live in the debugger.
- OOP programmers spend most of their time fixing bugs. FP programmers spend most of their time delivering results.
Ultimately, functional programming is the mathematics of the software world. If mathematics has given a very solid foundation to modern sciences, it can also give a solid foundation to our software, in the form of functional programming.
Take action, before it’s too late
OOP was a very big and a terribly expensive mistake. Let’s all finally admit it.
Knowing that the car I ride in runs software written with OOP makes me scared. Knowing that the airplane that takes me and my family on a vacation uses Object-Oriented code doesn’t make me feel any safer.
The time has come for all of us to finally take action. We should all start making small steps, to recognize the dangers of Object-Oriented Programming, and start making the effort to learn Functional Programming. This is not a quick process, it will take at least a decade before the majority of us will make the shift. I believe that in the near future, those who keep using OOP will be viewed as “dinosaurs”, similar to the COBOL programmers of today, obsolete. C++ and Java will die. C# will die. TypeScript will also soon become a thing of the past.
I want you to take action today— if you haven’t already, start learning functional programming. Become really good at it, and spread the word. F#, ReasonML, and Elixir are all great options to get started.
The big software revolution has started. Will you join, or will you be left behind?