So the original problem is that programs turned into an incomprehensible mess. Too many disparate systems can make arbitrary changes to the state of the problem at any time.
The solution we’ve been using (or trying to use) for the last couple of decades is to stick the various bits of our program into appropriately named objects, and then fit those objects into a hierarchy.
If I have a SpaceMarine object then I should have a limited number of things that I can do to it. Perhaps Heal(), Damage(), or Spawn(), and so the inner workings of a SpaceMarine will be hidden from the outside world. This is called encapsulation. It keeps the complexity of the SpaceMarine from leaking out into the rest of the code, and it keeps the complexity of the rest of the code from polluting the SpaceMarine.
Now let’s discuss some of the objections to OOP. Remember that this particular conversation was started by Brian Will in his video:
But I don’t want to simply repeat what Will says, so I’m going to offer my own criticism of OOP. If you want to hear his arguments, watch the video.
Problem One: Complete encapsulation is an unattainable ideal.
In my indie game Good Robot, enemy Robots can act upon each other. A robot might create other robots. It will both shove and be shoved by other robots that are crowding it. Robots can also create bullets. Robots can damage the player through direct contact.
BulletsIn this context, a “bullet” is any damaging projectile: Missiles, lasers, bombs, shotgun pellets, etc. can destroy robots. Bullets can destroy each other when lasers shoot down missiles. Bullets can change their trajectory (homing missiles) based on the presence of robots. Bullets die when they strike doors, but they might also cause the door to open. Bullets can destroy machines that produce robots.
Doors can act on robots and the player, preventing them from passing through. They block bullets. They open in response to player proximity.
The player can create bullets. The player can be killed by bullets. The player alters the behavior of robots. The player alters the course of bullets. (Homing missiles again.) The player alters the behavior of doors. (It opens if they’re close enough.)
Keep in mind this is a simplified explanation of an already simple game. Once you’re talking about a game where players can control vehicles that have mounted weapons that can shoot projectiles that can destroy different vehicles that can cause explosions that will detonate barrels that can collapse physics objects that can crush players, you’re up against some serious challenges in enforcing simplicity on a system that’s inherently complex.
Where is the encapsulation here? You can’t just shove bullets into a box and forget about them, because when bullets are updated they need to change the state of almost everything else in the gameworld. That’s the whole point of bullets. When you update bullets, a bunch of non-bullet things need to change, all across the program. You can fight this truth all you want, but in the end you haven’t really solved the original problem with procedural programming, which is that state interactions are complex and hard to understand. You’ve still got all of these interconnected systems changing one another at different times. You can’t hide their state away. Having objects A B and C all able to make changes to each other at various times is the goal of the program and the reason the problem was so hard to solve in the first place.
A cheeky programmer might say something like, “Hey, robots and bullets are both just “things that move around and run into each other”. So let’s make them two different flavors of the same shared idea. Call them uh… “flyers”. Then it’s just Flyers acting on other Flyers, and that technically doesn’t break encapsulation!
But this is a semantic trick that doesn’t actually do anything to help us solve the problem. The program is exactly as complex as it was before. More importantly, bullets and robots are very different things and need to be handled in very different ways. Bullets are tiny, number in the hundreds, and live only for a few seconds. Robots number in the dozens and live for many seconds or even a few minutes. Both of these things need to be kept in separate lists, and those lists should be tightly packed in memory. They need to be drawn at different times in the render loop and they serve totally different purposes in the game.
By mashing these two different ideas together, all you’ve done is made the nomenclature confusing and created a bunch of performance concerns.
2. Object hierarchies just replace one complexity with another.
So next our enterprising coder will try a new approach. The point of OOP is to prevent different parts of the program from altering each other’s state in unpredictable ways. So what if we make a new object? We can call it “Scene” or something. It’s in charge of all the bullets, robots, doors, the player and anything else that might be doing rude things like changing the state of the game. Since the scene object owns all bullets and all robots, they are now part of its overall state. It’s not robots and bullets changing each other, it’s the Scene changing itself. And that adheres to OOP principles! Hooray!
Of course, this solution is even worse than the last one. We’ve taken the code that controls the behavior of bullets and robots and pulled it out of their respective systems and into this huge container looming over them. Now if I want to see how bullets work I might need to look in Bullets.cpp, but maybe what I’m looking for is in Scene.cpp.
The scene class will continue to bloat until it reaches gigantic proportions, because regardless of what you call it, conceptually this class is the class for “anything that might change the state of the game”. Sooner or later everything will end up inside of this thing and you’ll end up with all game logic controlled by a single class. You’ll have all the complexity problems of PP, plus you’ve got terribly organized code.
Alternatively, the coder might create more and more classes for wrangling all of these types. A class to own bullets and robots. Another for doors and machines. Another for the static scenery. And then another class to manage all of those. All of those inter-object relationships can be mediated by these high-level abstract classes that only exist to allow these objects to talk to each other, rather than letting them simply talk to each other directly. As if making them talk through this third party will somehow simplify the inherent complexity of the problem. It’s an attempt to fix the problem through bureaucracy.
These are just a few example. There are endless ways to impose OOP principles onto this problem. A programmer sees a problem where it’s really hard to keep track of state changes, and take it as a personal challenge to create some “pure” (yet impractical) OOP framework around it. The problem is that we eventually lose sight of the goal, which is to create clear, efficient code.
There’s nothing wrong with OOP, you’re just bad at it!
When you point out that OOP results in a bunch of abstract classes, or that functionality ends up moved to non-intuitive places simply to satisfy OOP orthodoxy, the usual retort is that this won’t happen if you would just do it “right”. And here we come back to the problem of talking about coding as if it was a single profession and not a bunch of vastly different jobs that all share the same basic tools. An aerospace mechanic and an HVAC mechanic might both work on “machines” and they might both use wrenches, but neither one of them is qualified to tell the other how to do their job.
Insisting that OOP works fine in all cases is like insisting that all machines can be fixed with a wrench because you’ve never encountered a problem that couldn’t be fixed with a wrench. It’s basically claiming that no problem could possibly exist that might be ill-suited to a strict OOP design. Instead the OOP apologist will insist that your problem – which they’ve never seen and know nothing about – could be solved by simply following their advice with the proper level of tenacity and blind faith.
My problem isn’t really with OOP itself, but rather with the idea that its principles should be adhered to at all times without asking if it’s the right tool for the job. Clearly some problems lend themselves to one programming language better than another, and it’s reasonable to think that the same can be said for programming paradigms. It’s even reasonable to assume that in a large project, different parts warrant different approaches.
The Mountain Before Us
Let’s imagine an expedition tasked with climbing a mountain.
Alan says, “Let’s go up this steep slope to reach the top.”
Barbara replies, “That would be idiotic. That slope is too steep and dangerous. Half of us would die of exhaustion or injury before we made it.”
Alan says, “Nonsense! Any slope can be overcome with sufficient physical training.”
Barbara points to the road that winds back and forth up the side of the mountain. “We should go this way. The gradual slope will be far easier to overcome.”
Alan shakes his head. “That road is really long. We don’t have enough food to make it.”
“No! Any distance can be overcome if you just adhere to a strict rationing program.”
And so these two dunces gainsay each other until they get frustrated and set off in different directions.
Eventually, someone from Alan’s party staggers back and reports that the direct assault was a disaster. From now on, he’s a “road-and-rationing” man all the way.
Meanwhile, someone like Brian Will crawls back to the base camp to report that they followed the all best practices for rationing and it was still horrific. People starved. And so he swears he’s never taking the path again.
People arguing over which way is “right” are missing the key truth that both ways are technically “right” in the sense that you’ll get there and yet “wrong” in the sense that you can’t get there without casualties. This is not an either / or question. It’s entirely possible that this mountain is simply too big to be overcome safely. There are limits to how much energy the human body can put out in a day and there are limits to how far rationing can take you, and this remains true no matter how vigorously you point out the flaws with the other person’s approach. Their entire argument is based on the faulty assumption that one of these two paths will lead to perfect success. It clearly isn’t the other person’s, so it must be mine!
We live in a world with finite money to spend on programmers with finite mental capacity to write code in a finite amount of time on hardware of finite power. Sometimes we run into problems that are really hard to solve. There will be challenges where the solution won’t fit neatly into theoretical systems designed by people who have never seen a problem like this before. Sometimes a problem might create unmanageable complexity regardless of what dogmas you cling to, because some problems are really complicated.
In fact, we’re doomed to run into hard problems, because there’s no upper limit on how much stuff we want to accomplish. If you doubled the brainpower of every programmer on earth, then a lot of our current challenges would indeed be reduced to “trivial”. But by next week we’d be up against a new bunch of problems that had previously been clearly beyond us and are now just barely within reach. And then we’d end up right back here, arguing about how to think about problems that are too complicated to be thought about and trying to manage all of this expanding complexity.
My point is that there are no simple answers or perfect techniques. Sometimes this job will be really hard and there’s nothing you can do about it. I’m okay with people like Brain Will who prefer a PP approach to authoring code. I’m okay with the folks who favor the OOP approach. What I’m wary of are the people who go around repeating dogma and insisting that all problems can be solved if you follow the One True Way.
 In this context, a “bullet” is any damaging projectile: Missiles, lasers, bombs, shotgun pellets, etc.
The Best of 2019
I called 2019 "The Year of corporate Dystopia". Here is a list of the games I thought were interesting or worth talking about that year.
What was the problem with the Playstation 3 hardware and why did Sony build it that way?
Trusting the System
How do you know the rules of the game are what the game claims? More importantly, how do the DEVELOPERS know?
Raytracing is coming. Slowly. Eventually. What is it and what will it mean for game development?
Push the Button!
Scenes from Half-Life 2:Episode 2, showing Gordon Freeman being a jerk.