GitHub Decentralized Repo
GitHub Centralized Repo

In our Zoom meeting last night, I got some possibly conflicting advice. I have an idea for how to resolve the conflict.

We took a look at the Asteroids program last night and tried a few small improvements, plus one that didn’t work. Perhaps I’ll tell you about that later, but I know why it didn’t work, and it’s not terribly interesting. What I’m more interested in is that GeePaw Hill gave me two bits of feedback that seem to me to be a bit in conflict.

Frequent readers know that I have waffled back and forth about retaining all the different object types separately in SpaceObjectCollection and Transaction, and combining the collections in various ways. Just now, I’ve settled on keeping just a few collections, and creating others dynamically as needed:

    val asteroids get() = spaceObjects.filterIsInstance<Asteroid>()
    fun colliders() = colliders
    fun deferredActions() = deferredActions
    fun missiles() = spaceObjects.filterIsInstance<Missile>()
    fun saucers() = spaceObjects.filterIsInstance<Saucer>()
    fun ships() = spaceObjects.filterIsInstance<Ship>()
    fun spaceObjects(): List<SpaceObject> = spaceObjects
    fun splats() = spaceObjects.filterIsInstance<Splat>()

We’re part-way through changing the accessors from requiring () to being properties. I think I like the properties form better but wasn’t sure until we tried one of them last night. But that’s not the point here. Hill made two comments, to which I’ll try to do justice here.

His first point was that in a strictly typed language like Kotlin, there’s a kind of fundamental design principle that once we know the type of an object, we never should throw that information away. I am not sure that I agree with this, which we’ll discuss below.

Hill went on to indicate that I could of course have the design any way I want it with his famous line “You don’t work for the code, the code works for you”. That notion I do agree with, and that’s why I have the code the way it is now: I like it better that way.

But there is a tension, which I’ve talked about during the waffling back and forth between the changes between having very little type information in SpaceObjectCollection, and having it all. We’re on the low side right now, and the code above makes it clear that at least parts of the program apparently wish we had a bit more, because we are recreating sub-collections of the various types. And those are all used, although a couple may only be used in tests.

If the typed collections were only used in tests, I could isolate them into extensions and Hill’s quite legitimate concern would be greatly lessened, because the need for tests to be a bit invasive isn’t all that uncommon.

So he has a point, and I mostly agree, with an observation that is, I think, a bit more than a quibble.

In an object hierarchy, I think it is entirely reasonable to have some operations that work at the level of the leaves, in our case, Asteroid, Missile, and so on, and other operations that apply higher up in the hierarchy, treating all the objects as their super-type and doing something that applies to all of them.

This would perhaps run into another of Hill’s concerns, which is that he does not like to inherit implementation: he likes all implementation code to be in the leaves. I’m a bit less driven by principles in my programming than Hill is, and I don’t mind saying that I’d be happy to be more sensitive to them than I am, because it never hurts to know more and have more insights. Well, hardly ever.

The Issue

So an issue with my current design is that when we create the various space objects, we know exactly what they are, asteroids and missiles and saucers and ships1, and then we stuff them into the SpaceObjectCollection, which forgets the types, and then when we interact the objects, we do a double dispatch interactWith which rediscovers the types we have just thrown away.

Now I like the balance we have, because the more detailed collections led to a much more complicated way of doing the interactions:

~~~kotlin
class Interaction(
    private val missiles: List<Missile>,
    private val ships: List<Ship>,
    private val saucers: List<Saucer>,
    private val asteroids: List<Asteroid>,
    private val trans: Transaction
) {

    init {
        missilesVsShipSaucerAsteroids()
        shipVsSaucerAsteroids()
        saucerVsAsteroids()
    }

    private fun saucerVsAsteroids() {
        saucers.forEach { saucer->
            asteroids.forEach { asteroid ->
                saucer.subscriptions.interactWithAsteroid(asteroid, trans)
                asteroid.subscriptions.interactWithSaucer(saucer, trans)
            }
        }
    }

    private fun shipVsSaucerAsteroids() {
        ships.forEach { ship ->
            saucers.forEach { saucer ->
                ship.subscriptions.interactWithSaucer(saucer, trans)
                saucer.subscriptions.interactWithShip(ship, trans)
            }
            asteroids.forEach { asteroid ->
                ship.subscriptions.interactWithAsteroid(asteroid, trans)
                asteroid.subscriptions.interactWithShip(ship, trans)
            }
        }
    }

    private fun missilesVsShipSaucerAsteroids() {
        missiles.forEach { missile ->
            ships.forEach {  ship ->
                ship.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithShip(ship, trans)
            }
            saucers.forEach {  saucer ->
                saucer.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithSaucer(saucer, trans)
            }
            asteroids.forEach {  asteroid ->
                asteroid.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithAsteroid(asteroid, trans)
            }
        }
    }
}

Once I had all those types, it only seemed to make sense to use them, replicating to a degree the flow of the original Asteroids game. That wasn’t strictly necessary, of course, since a virtual collection that treated all the objects the same would be possible, but once you do that, why maintain the lists, and we are back in the loop of waffling.

Still, both of Hill’s dicta are on point. What should we do?

Not Exactly a Plan

There is in fact an issue in the current design: it has type information, it loses the information, and then it reconstitutes it both by checking types explicitly, and by double dispatch. I think there’s no denying that that’s odd.

So what might we do? I know I don’t like the other end point with the complicated Interaction object. And while I do like the current design and the code does work for me, not the other way around, maybe there is another design with fewer legitimate objections.

So what if, instead of having lots of classes and throwing the class information away, we had fewer classes, perhaps just one key class “SpaceObject”? How might we handle things that we now handle with individual classes? Could we handle things by delegation, and provide different delegates to produce different behaviors?

An Asteroid might be a SpaceObject with an AsteroidCollisionStrategy and an AsteroidMovementStrategy, and an AsteroidDrawingStrategy. We’ll probably want shorter names, though.

I think I’ll try it. What would be ideal would be not to have to reconstitute type during interaction: I think all the other cases are pretty simple, because a given kind of space object always moves with the same strategy and always draws with the same strategy, but the collisions are do, at least somewhat, come down to both elements of the pair who are interacting.

Challenges

I see at least two big challenges.

If I’m to proceed as I generally prefer, I’ll have to find small steps that lead in the direction of a design without the asteroid missile saucer ship types being explicit at the space object level. Making this more difficult is that I don’t have a sense yet of the direction to be moving, so small steps are hard to choose.

There are things to try. Some of the objects have separate views and some have not. I can give them all their own view and that should help me build understanding. I’m fairly sure I can do a similar thing with the update function that allows objects to move.

So, I’ll let the idea perk for a day and start trying things. We might come to a simpler design that is still sensible and clear. Then again we might crash and burn.

It’ll be fun to find out. I hope you’ll follow along.



  1. Lions and tigers and bears oh my.