Hard to Understand
Python Asteroids+Invaders on GitHub
Here I speculate and comment on why my decentralized asteroids game is difficult to understand.
I do not dispute that it is difficult to see why the program linked above implements the game of Asteroids. I can report that in that design it is, however, easy to implement Asteroids. It’s just not easy to find the game logic when you’re done.
Experienced object-oriented programmers1 tend to write very small objects, and to push program behavior down into subordinate objects. Programmers with less experience with this style often find it difficult to see how the program works. I have a theory to explain this.
When first we begin to program, most of us tend to write out the whole program more or less as a big blob that begins at the beginning, goes on to the end, and then stops. You can more or less read it top to bottom and see what it does.
Then we begin to use functions and subroutines, and if we do it well, you can still read the top-level program and the function names tell you what is going on. If you want the details, you look down into the function of interest, and repeat the process. The functions tend to deal with a small subset of the overall program.
Programmers who are not yet used to writing code with functions often have problems understanding such code: they’re still used to the top to bottom style.
As we improve—according to me—we write more and more, smaller and smaller functions, and we name them and arrange them so that, again, we can read at any level and see what’s going on, and only if we want more detail do we need to look further down. And we can even readily find things that need changing, because even if it’s a detail, it will likely have a useful name, and very likely we can guess from the upper level which next level down will deal with what we’re looking for.
In an object-oriented language, we do the same thing, with the added convenience of objects, which are a nice way of organizing the functions into groups that make sense together, because they share common data. And just as the functions we write deal with a small part of the program’s functionality, the objects we use get smaller and smaller, sometimes becoming very specific, and other times quite general. We might have an object for CapitalizedExpenditure, or even VehicleCapitalizedExpenditure, very specific domain objects. But we might also have objects for DollarAmount or EuroAmount, very general numeric values with monetary behavior.
Most of the OO code we review involves a small number of objects of known type, and sends them a few messages to carry out some operation. At every level, we tend to be able to read the code and know what it is going to do, if not exactly how it will be carried out. We almost never care how DollarAmount does monetary arithmetic, but if we do care, we know right where to look.2
In an Asteroids game, we might expect to fairly easily find code that considers a pair of objects, say an asteroid and a missile. We might expect that it would know that it was dealing with an asteroid and a missile, and it might check to see if they were close together, and if they were, it might call the asteroid to split, and tell the missile to die, and so on. We would very much expect to find the code for this two-object interaction somewhere. And we’d expect the other two-object interactions to be near by, the asteroid-ship ones, and so on. By observing things like this, we would come to see how the program implements the game, and most of them would be pretty easy to find by following our nose.
In my Asteroids program, that’s not the case, and it does make it hard to understand.
Let’s digress a moment.
Independent Collaborating Objects
Independent collaborating objects show up fairly often in programming.
- Actors
- There is the Actor Model, a notion of concurrent-running objects that collaborate. It was possibly inspired by notions of real physical objects interacting. There were even a few attempts to implement this model.
- Simulation Objects and Events
- Languages like Simscript allowed the creation of objects that issued events and responded to events.
-
Events are typically issued by objects without knowledge of what other object, if any, will receive them, and other objects can “subscribe” to the events they care about, generally without regard to what objects may send them.
- Multi-threading
- Many languages support threads, and we often use those by allowing different objects to run on different threads and to communicate with each other. This sort of thing generally requires careful synchronization and often leads to trouble.
- Micro-services
- Many programs use separately-hosted services that perform small (or sometimes large) services for them. The typical connection between these objects is asynchronous and quite likely event-driven.
All these schemes—and there are surely more like them—partition the program’s design into a number of separate aspects that run more or less independently and affect each other in a kind of arms-length fashion.
And they all have, more or less, the property that it is not easy to see where things happen. The simplest implementations may still have enough top-down character to allow us, if we start with the right one, to see a bit of the orchestration: this one sends a message to that provider and then when that comes back does this other thing.
But the closer the individual components get to truly collaborating to make something happen, the less able we are to find that something.
- Baseball
-
The game of baseball comes to mind. Where is the game? (Much of the time this is a question you might ask as you wait for something to happen.) This person stands there with a stick. Every now and again he swings the stick. Why? That one crouches behind the stick guy, making strange gestures. Once in a while he seems to have a ball in his hand and he stands up and throws it.
-
Here’s one standing on a small hill. He has a ball and he rears back in an odd posture and throws it. Oh, if you can even see the ball, which is unlikely, you see that he is throwing it to the crouching guy. If you wait long enough you might see the guy with the stick hit the ball and it might even go out into the field where these guys are standing around mostly spitting.
-
And once in a while something truly interesting happens. The hill guy throws, the stick guy hits and starts running up the line. A guy out in the field catches the ball. The stick guy kind of peels off. Meanwhile, the guy who was on a place we know of called first base runs toward second, and the one on third runs for home. The guy out in the field amazingly throws the ball all the way to the crouching guy, who gets in front of the running guy, who slides to the ground feet first, and crouching guy hits him with the ball. Meanwhile the guy from first passes second and heads for third, and crouching guy throws the ball to the guy near third. Running guy turns back and third throws the ball to second. Third and second creep in on the runner, tossing the ball back and forth until one of them tags him.
-
Where is the game of baseball? It exists only in the moment-to-moment actions of the players, subject to a set of rules that almost no one has ever read but that many of us understand more or less well anyway. Most of us learned by watching and playing, not by finding the rule book and poring over it.
In sufficiently powerful or complicated uses of actors, events, multi-threading, micro-services, or my Asteroids program are like that: the game is in the interaction and it is not expressed anywhere in the code!
Oh, sure, you can find it, spread around like raisins in your toast:
Oh, I get it, the stick guy wants to run around the square without getting tagged but he has to hit the ball first. Oh and if someone just catches it, he doesn’t get to go.
Oh, I get it, look here: the Asteroid will split if it’s hit by a missile, unless it is too small, and then it’ll just die. Oh, and look over here, if the missile hits something, it dies instead of carrying on.
Easy to Write, Hard to Read
What is interesting about systems written this way is that they are relatively easy to write. It’s fairly easy to think about each component separately and decide what it will do upon each possible interaction. And if you think about both sides, what each side does is pretty simple and you get missiles splitting and destroying asteroids.
To an important degree, the organization of the code in a decentralized system like this is easy and obvious. All the asteroid stuff is in the asteroid. But the interaction, the place where we expect to find “when an asteroid and missile collide, the asteroid splits or if too small, dies, and the missile always dies”? That sentence that you and I would share about the game is nowhere in the code.
Suppose I was trying to tell you how Asteroids worked, and we were just a little ways in and I said
“So there are missiles and asteroids, and if a missile hits an asteroid, the missile dies”.
And you asked “What happens to the asteroid?”
And I say “We’ll talk about that. If the missile hits a ship, the missile dies.”
And you’re like “What happens to the ship?”
And I’m like “If the missile hits a saucer, the missile dies.”
And you’re all “Saucer? What about the Saucer?”
Hours later, I tell you “If an asteroid is hit by a missile, the asteroid either splits or dies.”
And you ask “When a ship or saucer is hit, do they split or die too?”
And I tell you more about what the asteroid does.
First of all, you’d probably hit me, and second, by the time we got around to scoring you’d probably have forgotten the Asteroid altogether.
You’d expect a better, sort of top-down, more and more detailed description of the game. And when you read the code, you might well expect to find a similar top-down, repeatedly refined description of the program.
And in this design, you will not find it. It really isn’t there.
The World
Whether we look at some simple game like baseball, or something complicated like how you bend your finger, quite often when we look to see “where it is”, we find that it isn’t in just one place. The brain (we imagine) decides. It sends a message to a nerve. The message travels down and down and twitches a muscle. That’s rigged between two bones that have a somewhat flexible joint between them. The muscle shortens, and with luck the finger bends.
And that, of course, is about ten thousand times less complicated than it really is.
The real world is made up of innumerable little systems interacting, combining to produce what we think of as “what’s really happening”. Schemes like events and actors are programming design styles that mirror interacting systems, although in a much simpler way than most things in the real world, even just baseball.
Yabbut
Yeah, but programs like that are hard to understand and when it comes to how the cells in my body resulted in this article, we all have no idea at all. That’s no way to design a program that people can understand. That’s why we mostly write, if not “god objects”, then “demigod” and “minimal god” objects, as I talked about in the previous article.
So I don’t claim that this version of asteroids is ideal for understanding. But it does seem to be quite good—I’d never say ideal—when it comes to easily implementing Asteroids … or even Space Invaders.
When you’re working with it, it’s easy to understand … once you understand.
Conclusion?
Well, none, really. This whole decentralized design was done just to see what happened, what it felt like to use, and what it would teach us. Among the learnings, we find that it has much in common with some powerful design approaches that we have available to us, and that like this program, those approaches often have the characteristic that they are easy to write because of the simple cohesive character of the individual objects, and difficult to understand because the system’s overall behavior is partitioned among many objects.
We have these things in our bag of tricks. The great Kent Beck once advised us to keep certain techniques pretty deep in that bag, and to try to draw solutions only from the shallow bits. We should keep things as simple as we can: use the deep stuff rarely, with care, and only when we must.
Is this design easy, or is it hard? Is it elegant? Is it more complex than a procedural game, or less complex? When would something like this help us? When would it hurt?
I think it’s a fascinating question. Does this kind of design ever become the style we would choose because it’s most understandable of the things we might do? I think that sometimes, it might.
Think about that. Or don’t, I’m not the boss of you.
Thanks for reading!
-
OK, “not all experienced object-oriented programmers”. I suppose I can’t even say “the best”, but I want to. ↩
-
I’m reminded of a story that Chet recounted recently. We were teaching a Scrum Developer class. The team was supposed to implement FederalTax as 25 percent of GrossPay. In one team’s code we found
fed_tax=gross_pay*25
. Elsewhere, we found a getter for federal tax, which wasreturn fed_tax/100.0
. That is exactly how you don’t do tax, spread over two separate locations. We all know how that happened. They forgot to say0.25
and when they saw the answer was off by 100, they divided it out. Cohesion, my siblings, cohesion is important. ↩