Dynamically Static God Objects?
Part of a short series on design, static vs dynamic languages, god objects, and our ability to understand the code.
It all started, doctor, last night, in a very wide-ranging Zoom chat between Chet, Hill, and myself. We were all over the map, from code to morality to the life of the cell to the mystery of what life itself is. But part of what we talked about was my Asteroids soon to be Asteroids + Space Invaders program, and the fact that it is not easy to understand how it works.
We agree that it’s hard to understand how it works. That said, I do understand how the program works, and I think I understand why it’s difficult to figure out. In that light, I propose to write a few articles around the topic of design, computer language, god objects, and understanding.
Asteroids as Model
Let’s use the Asteroids game as a model to think about. In that game—most of you know this—the computer displays some simple pictures of a two-dimensional space scene. There are asteroids of various sizes floating along in straight lines. There is a ship, and the player has the ability to make it turn, accelerate, and fire missiles. And there are missiles. There is also a saucer, which we may or may not talk about further.
As you play the game, certain things become clear. The asteroids can run right over each other with no effect, as if one went behind the other or something. If a missile hits an asteroid, the missile disappears, and the asteroid splits into two smaller asteroids, unless it was already of the smallest size, in which case it, too, disappears. You get points for shooting the asteroids. Finally, if the ship hits an asteroid, the asteroid splits or disappears as before, but the ship is destroyed.
Implementing Asteroids
There are surely any number of ways we could write a program to create this game. Let’s sketch a few and then discuss the differences.
- Procedural God Code
-
In a procedural language, we could write a program with some arrays representing asteroids, ships, missiles, and in that program’s main loop, we could move all the objects a little bit, check for any interactions, process the interactions, marking objects visible or invisible, or adding and removing from the arrays, and then, finally, draw the next frame of the game.
-
In that program, we would probably only even consider collisions between missiles and asteroids, and ship and asteroids. We would have no code at all about collisions between asteroids and other asteroids, because we know they never collide.
-
Naturally this program would have subroutines and functions and so on, but its essential design character would be a program that knew about asteroids, missiles, and ship, and controlled them to make the game. That is, in fact, how the original game was written.
- Object-Oriented God Code
-
In an object-oriented language, we would leverage the notion of objects to write the program. But we might still choose to have a central object that managed the game. It might be called Game. That object might have lists representing asteroids, missiles, and ships. When it wanted to move those objects, it might just send each one the message
move
, where the procedural god code had to actually move each one itself, adding a small velocity amount to each one’s position. In the OO version, the object would know its velocity and add it in, handling that detail for the Game. And each object would do movement in its own specific way, to the extent that there were differences. -
The objects would probably know how to draw themselves, or might know smaller objects that knew how to draw them, so that the Game would just tell each object to draw itself and all the differences would be down in the object, where the procedural god program had branches in it, or separate loops, drawing all the asteroids, then all the missiles, then the ship, whatever.
-
But in an O-O God design, the main logic for who interacts with whom—and how they interact—would still be in the God object. It would include code to interact missiles and asteroids, and it might actually check to see if the two objects were colliding and it might make the determination to remove a big asteroid, insert two new ones, and remove the missile.
-
Now as expert programmers, we know that we could probably always push more responsibility down into the individual objects, or lift it up into the Game, more or less any way we wanted to. But the “God” notion is that there is a higher-level object that knows best what should be done with the lower level guys. God decides who interacts and in general, what will happen when they do.
-
The essence of the idea is that the god object knows about multiple types of objects and manages their behavior within itself. The lower-level objects are not very intelligent, but perhaps contain useful functions to help the god do what is needed.
-
God objects are not very well thought of in object-oriented design circles. They generally take on too many responsibilities and do not get good leverage from the subordinate objects that they use. We want to create more subordinates, and to push responsibility “down” into them.
- Object-Oriented Demigod Code
-
As we push responsibility downward from a “god” object that knows about interactions among lesser objects, it seems that often we create smaller gods, demigods if you will. Some object that manages part of the story of what happens.
-
We might break our Game into a Mover that moves everything, and a Painter that draws everything, and some kind of Interactor that manages the interactions.
-
We would likely consider this design “better”, because each of the demigods has a more focused responsibility, just moving or drawing or interacting, and thus it’s easier to understand how the program works.
- Minimal-God Code
- We can imagine a sort of limiting point for pushing code downward into simpler objects. Sometimes its easy to see. Consider the process of drawing the objects. Let’s assume that there will always be a moment in the program where all the thinking is done for this frame and it is time to draw things. So there will always be a loop over all the things, drawing them. That loop might include the actual drawing, or it might not.
-
In other words, the loop might look at each object, determine its type (asteroid, missile, ship) and dispatch off to a block of code for drawing that kind of thing. Or, that loop might just send a “draw” message to each object and let it decide for itself how to draw.
-
It’s easy to see how moving and drawing can remove all the god-like aspects from the corresponding code. The upper level move code might tell everyone to move, but it doesn’t know how to move anyone. But what about interactions?
-
The problem is that interacting is concerned with two objects, not just one. It’s easy to see how to push one function like drawing or moving down into the individual objects. But how can we push interacting down?
A Two-Part Question
When we set out to “push interaction down”, we face a two-part question. “What does an asteroid do when interacting with a missile?”, and “What does a missile do when interacting with an asteroid?” In most cases our [demi]god object does two things on an interaction, one thing for each of the pair interacting.
With a god or demigod object, the god decides: this one dies, that one splits, move on, nothing to see here. To push that logic down, there’s what seems to me to be a wrong way and a right way.
We might somehow prioritize who gets the message. Suppose we want the missile to handle the missile-asteroid collision. Then the missile figures out if it is colliding with an asteroid, and if it is, tells the asteroid what to do, and then deletes itself. Similarly if the asteroid handles things, it splits or removes itself and then tells the missile to delete itself.
That seems wrong. One or the other object knows too much about the other or the one.
It is better, it seems to me, to have two interaction “events”. The asteroid is told, you are interacting with a missile, and the asteroid is like OK, I’ll split if I’m big, die if I’m small. And the missile is told it is interacting with an asteroid and it is all “OK, I’ll die”. Each object knows its own responsibilities.
But often, each object needs to know the type of thing its colliding with. And it probably actually does know how to get some general information from the other, such as its position. The missile wants to know if it is close enough to the asteroid to kill it. The asteroid wants to know if the missile is close enough to split it. So the two may have little conversations about position or other details, but they don’t manage the others’ behavior.
So, it seems to me that when it comes to interactions between objects in our game, we want each object type to manage its own behavior, in the light of knowledge of what it is interacting with.
And that’s where language comes in.
Language and Types
Static Typing
In a language like Kotlin or Java, we declare the type of the parameters as part of the methods we define. The compiler knows the type of everything it has to deal with.
In Kotlin we might have
class Asteroid()
fun interact(missile: Missile) {
...
}
fun interact(ship: Ship) {
...
}
And when we are interacting with a missile we could do one thing and when interacting with a ship we could do another. So we would design our whole program to take advantage of the fact that we can break out interact
behavior by the type of its parameter, Missile or Ship.
We would try never to forget the type of an object, so that we would always be prepared to split behavior on type as above. We could always collapse that behavior if we wanted to: suppose that asteroid interaction were the same for hitting a missile or hitting a ship. If so, one possibility is to code this:
class Asteroid()
fun interact(thing: SpaceObject) {
...
}
… where SpaceObject is a superclass or interface encompassing missiles and ships. As long as we weren’t going to call back out to some object that wanted them broken out, we could collapse our code in Asteroid.
But … as a guideline, we’d probably say, no, don’t do that. Try never to lose the type info because once you lose it you can never get it back. Instead, do something like this:
class Asteroid()
fun interact(missile: Missile) {
this.common(missile)
}
fun interact(ship: Ship) {
this.common(ship)
}
fun common(thing: SpaceObject) {
// the common code goes here
}
That way we keep the type available, in case there’s a future need for it. So in a language like Kotlin, where types are part of the language, Hill would advise us never to throw away type information. And I would fully agree.
Dynamic Typing
In a language like Ruby or Python or Smalltalk, the compiler generally doesn’t know the type of variables, and in particular, method parameters generally have no particular declared type. (In Python, you can “hint” but the compiler mostly doesn’t care.) Data type is dynamic, determined at run time, not compile time.
Typically, if an object has a method of a given name, say interact
, you can call that method. And it’s mostly up to the programmer to be sure they call it with the right kind of parameters, because it works like that all the way down.
Once you’ve called “interact” on Asteroid, it’s going to send messages to its parameters, and those parameters had better do the right thing with those messages. But as long as that happens, your dynamic language is perfectly happy.
So if our asteroid is going to ask the parameter for its position, so as to decide whether there’s a collision, the parameter must understand position
, and so on. But it could be a missile, an asteroid, an angel, anything with a suitable position.
In these languages, there is no defined type of a parameter, and therefore there is no method overloading such as we had above in Kotlin. So we do something instead. Again, there’s a good thing and a not so good thing.
The not so good thing is to check the type of one’s parameters. You could say something like this:
class Asteroid:
def interact(my_parameter):
if type(my_parameter) == Missile:
# do missile things
elif type(my_parameter) == Ship:
# do ship things
That’s generally considered to be bad form. It quickly generates very messy procedural code and, well, we just don’t like it. Better form is that if the type of the parameter is important, we announce the type as part of the call:
class Asteroid:
def interact_with_missile(missile):
# do missile things
def interact_with_ship(ship):
# do ship things
Often this is associated with a “double dispatch”. Since in general, code does not know the type of object it has in hand, if we need to know the specific type, we send a generic message to the unknown, which then calls back with its type:
class Missile:
def interact_with(something):
something.interact_with_missile(self)
So we start our interactions saying, for example:
thing1.interact_with(thing2)
thing2.interact_with(thing1)
And if thing1 is an asteroid and thing2 is a missile, the first call turns into
class Asteroid:
def interact_with(thing):
thing.interact_with_asteroid(self)
And the second turns into
class Missile:
def interact_with(thing):
thing.interact_with_missile(self)
And now the Asteroid automatically runs its missile-dependent behavior and the Missile runs its asteroid-dependent behavior, but no one had to explicitly check the types. One bounce and we have reconstituted the type information at the last minute.
We reconstitute the type information dynamically, when we need it, via double dispatch or a similar scheme.
These are two quite different ways of dealing with the situation where the receiver needs to know the type of its parameter. In Kotlin and similar languages, we retain type everywhere and overload methods to keep that knowledge. In Python and similar languages we often do not hold onto type and then reconstitute it if and when we need it.
In general, the dynamic language excels when we usually do not need to know the specific type of objects, just their general “interface” type, and when we need to reconstitute the type only rarely. Truth is, programs in static languages are generally better in the similar case where we write functions to interfaces rather than specific types. It just looks a bit different.
Impact on God-like Objects
These language differences have significant impact on the design of a function such as the loop that interacts all the objects. In Kotlin, it would make sense to retain each type of object in its own sub-collection and to call interact
with each type separately, so that the methods in the receivers would dispatch properly.
In a language like Python, we have less incentive to do that. We still could, by keeping the asteroids separate from missiles and explicitly calling interact_with_missle
as needed … but there is no reason to do so, because with the double dispatch in place, the upper method just calls interact_with
and the individual interact_with
sends the interact_with_missle
.
Suppose we needed a new object, a proximity bomb. In each case, all the individual objects need to deal with the new object. In Kotlin, we’d add an interact_with(bomb: Bomb)
method, in Python an interact_with_bomb
method.
In Kotlin, we’d have to add a new collection for the bombs, and everywhere where we send type-specific messages, we’d have to add a loop over those new objects.
In Python, we don’t have to do that, because the double dispatch in Bomb will sort it out for us.
Is that better? In some ways yes, and in some ways no. It’s less work at the top, but at the bottom it is critical to do it right. If we forget to implement interact_with
, or some object doesn’t implement interact_with_bomb
, we may not get a compiler warning, we might just get a run time crash. That would be bad.
Which Language is Better?
I’m not going there. Languages are different, our habits are different, our preferences are different. I prefer dynamic languages because they are less restrictive in what I can say. But they are more restrictive in the sense that if I say wrong things, bad things happen at run time that might have been detected at compile time in a more static language.
They’re different. The main point here is:
The language differences cause different design decisions.
In Kotlin, it’s a darn good idea never to lose type information. In Python, most of the general type information is in your head, and often you just don’t care about the details.
Disagreements
When Hill and I discuss the design of my program, he brings a strict typing focus to the table, as those are the languages he most uses. Some of his habits are quite valuable and do apply to my code, and he often brings an idea that I do well to adopt. But sometimes, my code does things that make his skin crawl, because in Kotlin or Java you’d never do that, or if you did you’d regret it. But it may be perfectly OK in Python.
Since we are friends, long time colleagues, and generally pretty confident, these disagreements generally work out well because we explore the ideas deeply and come away with better understandings on both sides, and quite often with better code. We know, deeply, that there are different ways and they all have trade-offs.
And yet, his reflexes are honed by years in strict typing and mine by years in dynamic typing, and so often we’ll look at something and have quite different reactions to it.
And sometimes, we get a design that is almost impossible in one language and quite elegant in another. And sometimes … and we have one now … we get a design that does have a certain elegance, but is not easy to understand.
We’ll explore that in one or more subsequent articles.
Summary
Language influences design. Language influences our habits. Our habits influence design. They all come together in an almost unlimited number of fascinating combinations.
In particular, static typing vs dynamic typing leads to different patterns of good code, and they often feel unnatural to those from the other school.
The more we practice, the more we understand, and the easier things can become. Like we used to say in the old days, “It’s all good!”
See you next time!