-- John Carmack, maker of Doom & Quake
Your abstractions are a liability.
Often we jump to an abstraction in our code because it seems ‘obvious’ or just seems to fit our understanding of our domain. We want to make our code ‘clean’, so we look hard for ways to remove duplication and ‘code smells’.
As our codebase grows, our abstractions often pass their sell-by date, yet they persist as we attempt to work around them. Removing bad or redundant abstractions as our understanding of our codebase grows is essential.
As an example, consider this simplified game loop. This is pretty much how the Sol Trader inner loop worked until about a week ago.
(I’ve placed the methods within the class definition for brevity, but in C++ they would probably be in a different file. This is important.)
The problem is that the scope of
_running is propagated further than it should be. It’s possible to stop the game loop running from both the
draw() methods: in fact, anywhere in the
Game class at all. It’s also not clear what the designer of this class intended the behaviour to be. Where exactly should
_running be updated?
With a large and complex set of methods (initializing and tearing down a modern
game is no small feat) then the
_running could potentially be set from a
large number of places - and perhaps set and set again during the same frame!
Just because a variable is private doesn’t save you from a large class giving access to that variable to many code paths.
Let’s remove the
_running member variable and use a local variable instead:
_running into a local variable within the
run() method, we’ve stopped the state propagating throughout the class instance, and encapsulated the running logic where it belongs. It’s also clear now that the
update() method should decide when the game stops running, and that the we should draw the final frame before we quit.
You can take this further and inline the other objects:
The real problem has now become evident. It is an insufficient understanding of Object Oriented programming - namely putting everything into noun-based objects such as
Game. It turns out there’s not actually useful game abstraction here - we are better off with a simple
run() method. Functional programming, anyone?
It seems simple, but the amount of code I’ve been able to simplify with this two techniques is astonishing. I do end up with longer methods, but by collapsing unnecessary abstractions the code becomes more straightforward and clearer.
Removing useless abstractions by inlining methods and variables where appropriate helps us to see where the real seams of our code are. It helps us to pick apart unhelpful or mistakenly classified objects and leads us to a better understanding of OO.
The wrong abstraction is (far) worse than no abstraction at all.