GitHub Decentralized Repo
GitHub Centralized Repo

‘Your scientists were so preoccupied with whether they could, they didn’t stop to think if they should.’

Yesterday, we were speculating about the not-really-conflicting bits of advice I got from Hill, saying on the one hand that it seems like a fundamental notion of type-centric design not to forget a type once you know it, and on the other hand, that the code works for you, you don’t work for the code. We were speculating “what if we removed the different space object types, just had SpaceObject, and delegated differences in behavior to objects contained inside the space object?”

I have no doubt that we could do this, and it is tempting to do it just to learn how to do it well. This is particularly true since Kotlin has some rather nifty delegation built in, using the by keyword. So I think we could, I think we’d learn from it, and we might even get a cool baby T Rex out of it.

As a learning exercise, I can justify almost anything. In a real product, while I do very much recommend experimentation as an early part of any design thinking, I also believe that it makes sense to think a bit about what would and would not happen were we to make certain changes. And that, unfortunately, is what I was thinking about this morning before rolling out of bed.

The core of the game in GameCycle almost doesn’t care about the types of the objects in the mix. It mostly just iterates over them all, certainly in the very central notions of interacting (colliding) and drawing. It does, however, make a few distinctions. Let’s talk about those:

DeferredAction
We have a nice little object that contains an action to be done at some future time. Every time the clock ticks, all those actions are given a chance to decide whether to do their action. They do things like create a new wave of asteroids after 4 seconds, or run another saucer after 7 seconds.

The DeferredActions are kept separately in SpaceObjectCollection, and we update them separately, to ensure that the timers get first shot at making things happen.

Colliders
Of the five types of space objects, asteroids, missiles, saucers, ships, and splats, splats are different in that they never interact or collide with anything. In a fit of over-design, I chose not to make them implement the Collider interface, and to make SpaceObjectCollection keep the colliders separate from the others.

The upshot is that there’s a type-check in SpaceObjectCollection and a separate collection containing everything but splats. The code that generates pairs for interaction uses the collider collection rather than the full space object collection.

This is slightly faster, because there are fewer pairs to consider, and results in a bit less code because Splats don’t implement interaction. Not much of a savings, but you could make a case for it.

Virtual Collections
There is a complete set of virtual collections defined on SpaceObjectCollection, to fetch all the asteroids or missiles or saucers or ships or splats. Most of these are actually used in the game.

The asteroids collection is used (in SpaceObjectCollection) to answer whether it is safe for the ship to emerge. We don’t rez it until the area around the center is fairly clear. The missiles collection and saucer collection are used for the same purpose. Those collections have no other purpose in the game, but are also used in tests that want to check to be sure that things are created or destroyed. We also use the asteroids collection to know whether to create a new wave, if I’m not mistaken. And we use the count of asteroids in deciding whether you survive an entry to hyperspace.

This last one might give us trouble, as I’ll discover in a moment.

The ships collection is used to decide whether the ship is present, so that after it’s destroyed, we’ll create a new one if the player still has available ships. It, too, is used in tests.

The splats collection is used only in tests.

How does this apply to our question?

How does this quick analysis impact the “should”? Well, if we were to change over to there being only SpaceObject and none of its typed implementors, Asteroid, Missile, and so on, and yet we still need to break out those sub-collections for the game, then throwing away the types is arguably a bad idea. The fact that we threw them away at design time instead of run time is arguably a bigger mistake than throwing them away at run time. At run time, we can always get them back.

If we needed those collections, we’d surely wind up with some kind of “type” flag in the SpaceObject, so that they could answer questions like areYouAShip. That’s just a class by another name, and again, it’s a sign that we probably shouldn’t have thrown away key information.

What about testing?

We could give testing some kind of a pass, and let it implement extension methods for test convenience. I would not think that, in general, we’d add types at run time solely for the convenience of our tests. That’s not an absolute, however, because difficulty in testing is a key signal that the code may not be right, so we shouldn’t just wave away the needs of the tests. Here, though, I don’t rate them as providing a very strong argument for breaking out the space object types.

What about in the game though?

I think we isolated two needs for type information in the game itself. First, it wants to know whether it is safe to rez the ship, and it checks missiles and saucers for existing, and asteroids for being close to center. The other case is that we need to know whether the ship is present. Could we finesse these two needs so that we wouldn’t need the collections? I think we could.

We know that every colliding object is sent the message interactWith at least once and usually many times, during the course of interaction processing. Suppose, for example, that the ship’s implementation of interactWith was changed so that it sent a message back to the game cycler? We’d have to provide the cycler as a parameter to interactWith, but it could look like this:

class Ship
    override fun interactWith(other: Collidable, trans: Transaction, cycler:GameCycler) {
    	cycler.shipIsPresent()
    	other.collisionStrategy.interact(this, trans)
    }

Similarly, saucer and missile could send unsafeToEmerge, and asteroids could send the message if they’re near the center.

Now these messages would be sent multiple times if we weren’t careful. For the cases described so far, that would be OK, we’d be told multiple times not to emerge, or multiple times that asteroids still are present. But we have that one case … when you go to hyperspace, your chance of surviving is a function of the actual number of asteroids present. How might we manage that?

Well … the ship will be sent interact(asteroid, trans) for each asteroid in the mix. So if ship were to zero a counter before interactions begin, and tick the counter there, it would always know how many asteroids are out there.

Our scientists say we could do it.

OK, I think that we could do without all the sub-collections in the game, and it wouldn’t be much more complicated than it is now. It would be a bit more like the decentralized version we first did, which managed all kinds of things with messages between the various objects in the mix. So the question is whether we should do it.

If we do it, we’ll be able to simplify SpaceObjectCollection a bit. We’d remove the special handling for splats by giving them empty behavior for collisions. We’d remove the virtual collections and make them extensions for the tests to use, or otherwise resolve the concerns of the tests.

But should we actually remove all the special types? I don’t see much advantage to that. We do already have delegates that are handling the interactions, which we did to make the individual classes more cohesive. If we wanted to, we could do the same for movement and drawing and any other responsibilities that the objects might have. Doing that would be driven by design considerations in the objects themselves.

Arguably, if a consideration inside SpaceObjectCollection is driving us to redesign a whole raft of other objects, we should conclude that the problem is in SpaceObjectCollection, not all the other objects.

We shouldn’t. No T Rex for you.

I conclude, at least today, that it would not be a good thing to create a single SpaceObject that takes on multiple aspects by delegating behavior to sub-objects. We might do that if the objects came in combinations, like an asteroid that flew like a ship or a missile that splits on collision, but in our actual game, each object of type X would fly like an X, draw like an X, and interact like an X. Pluggable behavior pays off when we are mixing up the plugs, not when the users are all the same. (It might pay off by simplifying the individual objects, but that’s not an argument for forgetting their types.)

Maybe I should generalize Hill’s observation, which I paraphrased as:

His first point was that in a strictly typed language like Kotlin, there’s a kind of fundamental design principle that once the code knows the type of an object, we never should throw that information away.

Perhaps we should be thinking something like this:

In any design, there’s a fundamental principle that once the developer knows the type of an object, they should never throw that information away.

This is, of course, just another way of expressing one of Kent Beck’s Rules of Simple Design, which I generally list this way:

The code is simple enough when, in priority order:

  1. It passes all the tests;
  2. It contains no duplication;
  3. It expresses all our design ideas about the code;
  4. It minimizes programming entities.

It’s interesting how good ideas come at us from all directions. It’s amazing how often we ignore them knocking at our heads, begging to be let in.

Summary

Today’s considerations leave me convinced not to use broad delegation try to resolve the tension between simplicity in SpaceObjectCollection and the need for typed collections. There’s tension, and I’ve balanced the tension to my liking for the present. Mangling all the relatively simple individual classes would not be a good way to make SpaceObjectCollection simpler.

Maybe there are changes available that will improve things. But massive chunks of delegation are probably not the changes we’re looking for.

MetaSummary

The code we create will have tensions in it, forces pulling us in different directions. Efficiency dictates this, clarity dictates that, size dictates this other thing. And the design principles we learn also pull us in different directions. We want more cohesion and less coupling, but to get to maximum cohesion and minimum coupling seems to be impossible sometimes. We want to avoid depending on concrete implementations, but inverting responsibility requires us to build more and stranger objects.

It rarely comes out even. At any moment in time, our program seems to want things that contradict each other. Our asteroid program wants individual collections for some good reasons, and wants not to have them for other good reasons.

I think there is value to thinking about these matters, especially in the context of real code, because in practice we’ll be making trades between the various principles, with a pragmatic eye on getting the job done today while leaving us flexible for tomorrow. So I don’t mind a couple of mornings spend wondering about “what if”.

That’s true especially if it leads me to write a little code to see what it might look like, so we might write a bit of code soon, to try to nail down these ideas a bit more firmly, even though I’m pretty happy with my thinking so far.

Tomorrow

Maybe we’ll find something better tomorrow. Of course, one of these days, we either need a different project or a different language to build Asteroids in.

I can’t wait to find out what I do. At least there won’t be Velociraptors up in the house.