GitHub Decentralized Repo
GitHub Centralized Repo

The new Interaction object is in place. Now let’s work to unwind the calls through Subscriptions, moving toward simplifying the hierarchy. Added in Post: Woot!

Over the past few articles, I’ve been moving toward what we might call “type preservation”, that is, programming so that we don’t hide the specific class of objects. Instead of specifying method parameters with an interface or high-up superclass, we create methods specific to the actual classes of the parameters. The primary reason for this change is to simplify the design. In particular, we have a very clever mechanism, Subscriptions, that allows each specific subclass to field a generic message in a way that’s specific to the class. That scheme allowed the top level of the game to be ignorant of the specific classes of the objects: they were all just “SpaceObject” as far as the top level was concerned.

In my historical past, this scheme made sense, because I’m used to programming in languages with “duck typing”. These languages freely permit you to build a collection of any kind of object, and yet when you call methods on those objects, you get the specific type-dependent versions of everything.

In order to make my original Kotlin Asteroids design work, I used implementation inheritance to provide default methods for objects that didn’t need to respond to certain messages. That allowed me to implement each specific object with just the methods that were needed, without a lot of overridden methods doing nothing. At the time, that seemed like a reasonable thing to do.

The brilliant and beautiful GeePaw Hill has a deep dislike for implementation inheritance, and as we two worked to understand each other, he came up with the Subscriptions object, which allows my objects to have a default implementation for calls they don’t need, with specific implementations when they are needed. It’s a very nifty idea and it works well. The code for that is still present in today’s version, and it remains in the decentralized version. Links to both gitHub repos are above.

Anyway … the core idea for this series is that I wanted to move from the highly decentralized style of the first version to a more conventional design, where the central game object knows what’s going on. We are a good way along that path currently.

Yesterday, I test-drove and installed a new object, Interaction, whose job is to be given all the specialized collections of objects, asteroids, missiles, ship, and saucer, and to check all the desired collisions between them. By the nature of the game, this isn’t everything against everything, in particular because asteroids do not collide with each other. Now that the Interaction object is in place, we can call specific methods on specific objects, which will allow the custom behavior that we need i some interactions.

Today, I plan to do more unwinding, and I’ve noticed something interesting, something whose implications I’m not clear about. We’ll come to that shortly.

Let’s begin by reviewing the Interaction:

Interaction

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 {
        missilesVsMissileShipSaucerAsteroids()
        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 missilesVsMissileShipSaucerAsteroids() {
        missiles.forEach { missile ->
            missiles.forEach { other ->
                if (other != missile ) {
                    missile.subscriptions.interactWithMissile(other, trans)
                    other.subscriptions.interactWithMissile(missile, trans)
                }
            }
            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)
            }
        }
    }
}

This is a bit long, because it breaks out all the possible combinations of interacting objects. But we can see that the pattern is always the same, we get a pair of objects a and b and we interact a vs b and b vs a, and we express to each receiver the class of the parameter. For example:

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

When we tell the saucer to interact, we tell it that it is interacting with an Asteroid, and when we tell the asteroid to interact, we tell it that it is interacting with a Saucer. That allows each object to behave differently, depending on what kind of thing it’s interacting with.

Now, if you’re thinking that there is a better way to do this, you’re right, given that the caller knows the class of the parameter, as we do here. In the decentralized version, we had lost that information, in the interest of having a collection of space objects without preserved type. In that design, it seemed to make sense. In this design, we can do better, as we’ll see shortly.

The core notion, however, is that the receiver has the possibility of behaving differently depending what it is interacting with. In the decentralized version, some of those interactions were quite interesting. Now that we are down only to actual space objects, asteroids and all that, there is less variation. We will want to take advantage of that if we can.

Now our main interest here is to get ride of the indirection through subscriptions. Let’s look at a specific case. I think missile vs missile might be easy. Here’s the patch of code for that:

    missiles.forEach { missile ->
        missiles.forEach { other ->
            if (other != missile ) {
                missile.subscriptions.interactWithMissile(other, trans)
                other.subscriptions.interactWithMissile(missile, trans)
            }
        }

Both our objects here are missiles, so they both use the subscriptions from Missile class:

    override val subscriptions = Subscriptions(
        interactWithAsteroid = { asteroid, trans ->
            if (checkCollision(asteroid)) {
                if (missileIsFromShip) trans.addScore(asteroid.getScore())
                terminateMissile(trans)
            }
        },
        interactWithSaucer = { saucer, trans ->
            if (checkCollision(saucer)) {
                if (missileIsFromShip) trans.addScore(saucer.getScore())
                terminateMissile(trans)
            }
        },
        interactWithShip = { ship, trans ->
            if (checkCollision(ship)) terminateMissile(trans)
        },
        interactWithMissile = { missile, trans ->
            if (checkCollision(missile)) terminateMissile(trans)
        },
        draw = this::draw,
    )

We can see above that there are a few different bits of behavior here. We are just interested in changing things so that our loop above doesn’t have to refer through subscriptions. We can extract a method to do that. We’ll call it interactWithMissile, but we’ll notice something important about that.

Extract:

        interactWithMissile = { missile, trans ->
            interactWithMissile(missile, trans)
        },
        draw = this::draw,
    )

    fun interactWithMissile(missile: Missile, trans: Transaction) {
        if (checkCollision(missile)) terminateMissile(trans)
    }

IDEA made it private. I made it public, because I intend to call it here:

    missiles.forEach { missile ->
        missiles.forEach { other ->
            if (other != missile ) {
                missile.interactWithMissile(other, trans)
                other.interactWithMissile(missile, trans)
            }
        }

Test, expecting Green. Green it is. Commit: missile vs missile avoids subscriptions.

Now, in principle we ought to be able to remove the interactWithMissile from Missile’s subscriptions, but we may well have tests that exercise it. We’ll remove it and test. We’re in luck, all the tests run. This makes me suspect that we’re missing a test on missile vs missile. I make a note. Be that as it may, I’m confident that the game is still working. We do have a fairly robust test of the Interaction object, and this change, from its viewpoint, is a simple refactoring.

We have about a half dozen more such changes to make, and most of them will involve two classes. I’ll do another one in detail, then maybe skim the others. Let’s do ship vs missile, in the same loop as missile vs missile:

        ships.forEach {  ship ->
            ship.subscriptions.interactWithMissile(missile, trans)
            missile.subscriptions.interactWithShip(ship, trans)
        }

The missile variable is bound in the outer loop, over missiles. To remove the one-rail shot through subscriptions, we need a method on ship and one on missile. Missile has this in subscriptions:

    interactWithShip = { ship, trans ->
        if (checkCollision(ship)) terminateMissile(trans)
    },

I’ll just extract that without prejudice:

        interactWithShip = { ship, trans ->
            interactWithShip(ship, trans)
        },
        draw = this::draw,
    )

    fun interactWithShip(ship: Ship, trans: Transaction) {
        if (checkCollision(ship)) terminateMissile(trans)
    }

Similarly in Ship:

    override val subscriptions = Subscriptions(
        interactWithAsteroid = { asteroid, trans -> checkCollision(asteroid, trans) },
        interactWithSaucer = { saucer, trans -> checkCollision(saucer, trans) },
        interactWithMissile = { missile, trans -> checkCollision(missile, trans) },
        draw = this::draw,
    )

Extract:

    override val subscriptions = Subscriptions(
        interactWithAsteroid = { asteroid, trans -> checkCollision(asteroid, trans) },
        interactWithSaucer = { saucer, trans -> checkCollision(saucer, trans) },
        interactWithMissile = { missile, trans -> interactWithMissile(missile, trans) },
        draw = this::draw,
    )

    fun interactWithMissile(missile: Missile, trans: Transaction) {
        checkCollision(missile, trans)
    }

Now I can remove the references to subscriptions from that loop:

        ships.forEach {  ship ->
            ship.interactWithMissile(missile, trans)
            missile.interactWithShip(ship, trans)
        }

I expect this to run. It does. Can I remove the subscriptions? Try the missile one. Green. Try the ship. Also green. I am a bit worried about my tests, worried enough that I try the game. It doesn’t take long for the saucer to shoot me down. Missile vs ship works.

Let’s reflect, though.

Reflection

A scan of the tests shows me a fair number of checks for saucer-asteroid, ship-asteroid, and a couple of others, but this area has a lot fewer tests than I’d like to see. I’ve made a sticky note in my “Jira” pile to do more such tests. An important question, however, is whether we need to do them before we continue this refactoring.

I argue that we are safe, because the changes we make are quite simple and rote: extract a method and call it. And the Interaction tests are actually quite comprehensive:

class InteractionTest {
    val target = Point(100.0, 100.0)

    @Test
    fun `empty returns empty`() {
        val missiles = mutableListOf<Missile>()
        val ships = mutableListOf<Ship>()
        val saucers = mutableListOf<Saucer>()
        val asteroids = mutableListOf<Asteroid>()
        val trans = Transaction()
        Interaction(missiles, ships, saucers, asteroids, trans)
        assertThat(trans.adds).isEmpty()
        assertThat(trans.removes).isEmpty()
    }

    @Test
    fun `missile and missile`() {
        val missile1 = Missile(target).also { it.position = target }
        val missile2 = Missile(target).also { it.position = target }
        val trans = Transaction()
        Interaction(listOf(missile1, missile2), emptyList(), emptyList(), emptyList(), trans)
        assertThat(trans.removes).contains(missile1)
        assertThat(trans.removes).contains(missile2)
    }

    @Test
    fun `missile and ship`() {
        val missile = Missile(target)
        missile.position = target
        val ship = Ship(target)
        val trans = Transaction()
        Interaction(listOf(missile), listOf(ship), emptyList(), emptyList(), trans)
        assertThat(trans.removes).contains(ship)
        assertThat(trans.removes).contains(missile)
    }

    @Test
    fun `missile and saucer`() {
        val missile = Missile(target).also { it.position = target}
        val saucer = Saucer().also { it. position = target }
        val trans = Transaction()
        Interaction(listOf(missile), emptyList(), listOf(saucer), emptyList(), trans)
        assertThat(trans.removes).contains(saucer)
        assertThat(trans.removes).contains(missile)
    }

    @Test
    fun `missile and asteroid`() {
        val missile = Missile(target).also { it.position = target}
        val asteroid = Asteroid(target)
        val trans = Transaction()
        Interaction(listOf(missile), emptyList(), emptyList(), listOf(asteroid), trans)
        assertThat(trans.removes).contains(asteroid)
        assertThat(trans.removes).contains(missile)
    }

    @Test
    fun `ship and saucer`() {
        val ship = Ship(target)
        val saucer = Saucer().also { it.position = target }
        val trans = Transaction()
        Interaction(emptyList(), listOf(ship), listOf(saucer), emptyList(), trans)
        assertThat(trans.removes).contains(saucer)
        assertThat(trans.removes).contains(ship)
    }

    @Test
    fun `ship and asteroid`() {
        val ship = Ship(target)
        val asteroid = Asteroid(target)
        val trans = Transaction()
        Interaction(emptyList(), listOf(ship), emptyList(), listOf(asteroid), trans)
        assertThat(trans.removes).contains(asteroid)
        assertThat(trans.removes).contains(ship)
    }

    @Test
    fun `saucer and asteroid`() {
        val saucer = Saucer().also { it.position = target }
        val asteroid = Asteroid(target)
        val trans = Transaction()
        Interaction(emptyList(), emptyList(), listOf(saucer), listOf(asteroid), trans)
        assertThat(trans.removes).contains(asteroid)
        assertThat(trans.removes).contains(saucer)
    }
}

I think we’re good. Interaction is managing all the collisions, and it is well tested. If we do make a mistake, those tests are likely to find it. We’ll carry on.

Remove More Subscriptions

    saucers.forEach {  saucer ->
        saucer.subscriptions.interactWithMissile(missile, trans)
        missile.subscriptions.interactWithSaucer(saucer, trans)
    }

I’m going to try this a bit differently this time. I’ll remove the .subscriptions, one at a time, and let IDEA prompt me to build the method. Then I’ll fill it in from the target class’s subscriptions.

That turns out to be a bad idea. Extracting is easier:

        interactWithMissile = { missile, trans ->
            interactWithMissile(missile, trans)
        }
    )

    fun interactWithMissile(missile: Missile, trans: Transaction) {
        if (missile == currentMissile) missileReady = false
        checkCollision(missile, trans)
    }

Test. Green. Do the other side.

        interactWithSaucer = { saucer, trans ->
            interactWithSaucer(saucer, trans)
        },
        draw = this::draw,
    )

    fun interactWithSaucer(saucer: Saucer, trans: Transaction) {
        if (checkCollision(saucer)) {
            if (missileIsFromShip) trans.addScore(saucer.getScore())
            terminateMissile(trans)
        }
    }

Test. Green. Commit: missile vs saucer avoids subscriptions. missile vs ship avoids subscriptions. (I forgot to commit last time. No harm done.)

If you’ll permit, I’ll just tick through these. I’ll report if anything interesting happens, but I expect nothing at this level. I’ll just be adding the methods and will save removing the subscriptions until Interactions isn’t using them at all.

Well. It’s worth reporting that Asteroid already has all the methods broken out, in a very odd way.

    override val subscriptions = Subscriptions(
        interactWithMissile = { missile, trans -> interactWithMissile(missile, trans) },
        interactWithShip = { ship, trans -> interactWithMissile(ship, trans) },
        interactWithSaucer = { saucer, trans -> interactWithMissile(saucer, trans) },
        draw = this::draw,
    )

    private fun interactWithMissile(missile: Missile, trans: Transaction) {
        if (Collision(missile).hit(this)) {
            dieDuetoCollision(trans)
        }
    }

    private fun interactWithMissile(ship: Ship, trans: Transaction) {
        if (Collision(ship).hit(this)) {
            dieDuetoCollision(trans)
        }
    }

    private fun interactWithMissile(saucer: Saucer, trans: Transaction) {
        if (Collision(saucer).hit(this)) {
            dieDuetoCollision(trans)
        }
    }

I note that they’re all called “interactWithMissile” even though they don’t all involve missiles. It works, of course, because the overload sorts out the right one. For now, let’s rename these to the right names, despite the fact that they are all identical internally. I plan to come back for that issue. For now, I’ll give them the expected unique names.

    override val subscriptions = Subscriptions(
        interactWithMissile = { missile, trans -> interactWithMissile(missile, trans) },
        interactWithShip = { ship, trans -> interactWithShip(ship, trans) },
        interactWithSaucer = { saucer, trans -> interactWithSaucer(saucer, trans) },
        draw = this::draw,
    )

    private fun interactWithMissile(missile: Missile, trans: Transaction) {
        if (Collision(missile).hit(this)) {
            dieDuetoCollision(trans)
        }
    }

    private fun interactWithShip(ship: Ship, trans: Transaction) {
        if (Collision(ship).hit(this)) {
            dieDuetoCollision(trans)
        }
    }

    private fun interactWithSaucer(saucer: Saucer, trans: Transaction) {
        if (Collision(saucer).hit(this)) {
            dieDuetoCollision(trans)
        }
    }

That’s “better”. Anyway I can remove the subscriptions on that side:

        asteroids.forEach {  asteroid ->
            asteroid.interactWithMissile(missile, trans)
            missile.subscriptions.interactWithAsteroid(asteroid, trans)
        }

Now the other.

    override val subscriptions = Subscriptions(
        interactWithAsteroid = { asteroid, trans ->
            interactWithAsteroid(asteroid, trans)
        },
        interactWithSaucer = { saucer, trans ->
            interactWithSaucer(saucer, trans)
        },
        draw = this::draw,
    )

    fun interactWithAsteroid(asteroid: Asteroid, trans: Transaction) {
        if (checkCollision(asteroid)) {
            if (missileIsFromShip) trans.addScore(asteroid.getScore())
            terminateMissile(trans)
        }
    }

Test, green, commit: asteroid vs missile avoids subscriptions.

OK, back to silent running. I’ll record the commits unless something interesting happens.

Commit: saucer avoids all subscriptions.

Commit asteroid avoids all subscriptions.

Commit: ship avoids all subscriptions.

We’re done with this phase. Interaction has no more references to subscriptions:

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 {
        missilesVsMissileShipSaucerAsteroids()
        shipVsSaucerAsteroids()
        saucerVsAsteroids()
    }

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

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

    private fun missilesVsMissileShipSaucerAsteroids() {
        missiles.forEach { missile ->
            missiles.forEach { other ->
                if (other != missile ) {
                    missile.interactWithMissile(other, trans)
                    other.interactWithMissile(missile, trans)
                }
            }
            ships.forEach {  ship ->
                ship.interactWithMissile(missile, trans)
                missile.interactWithShip(ship, trans)
            }
            saucers.forEach {  saucer ->
                saucer.interactWithMissile(missile, trans)
                missile.interactWithSaucer(saucer, trans)
            }
            asteroids.forEach {  asteroid ->
                asteroid.interactWithMissile(missile, trans)
                missile.interactWithAsteroid(asteroid, trans)
            }
        }
    }
}

Nice. Now we can go through and remove those definitions from the subscriptions of all the objects. In fact, we can remove them from Subscriptions itself, and let it guide the changes:

class Subscriptions(
    val interactWithAsteroid: (asteroid: Asteroid, trans: Transaction) -> Unit = { _, _, -> },
    val interactWithMissile: (missile: Missile, trans: Transaction) -> Unit = { _, _, -> },
    val interactWithSaucer: (saucer: Saucer, trans: Transaction) -> Unit = { _, _, -> },
    val interactWithShip: (ship: Ship, trans: Transaction) -> Unit = { _, _, -> },

    val draw: (drawer: Drawer) -> Unit = {_ -> },
)

I’ll remove them one at a time and fix what comes up.

I quickly find that we have an override method, callOther, that is never called.

interface SpaceObject {
    val subscriptions: Subscriptions
    fun callOther(other: SpaceObject, trans: Transaction)
    fun update(deltaTime: Double, trans: Transaction)
}

I reckon I can remove that and then all the overrides. I’m quickly green. Commit: remove callOther everywhere.

Now back to the task of removing all those subscriptions from Subscriptions and the individual classes.

I remove interactWithAsteroid and get a number of tests failing because they are calling through subscriptions. I think I can just change them all not to do that, but to call directly.

That works and I’m quickly green. Commit: interactWithAsteroid removed from Subscriptions.

Kotlin is trying to tell me that none of the remaining interactWith subscriptions is used. I am surprised but happy to remove them and see how it goes.

It’s confused. They are used, but let’s at least try removing them from all the objects at once.

Done. We are green. Commit: all interactWith methods removed from subscriptions throughout system.

I have to try a celebratory and confirming game. All is well.

One more thing:

Removing Subscriptions Entirely

This is a notable milestone. There is only one function left in Subscriptions, namely draw. It’s clearly no longer carrying its weight. Starting at SpaceObject, we just have this:

interface SpaceObject {
    val subscriptions: Subscriptions
    fun update(deltaTime: Double, trans: Transaction)
}

class Subscriptions(
    val draw: (drawer: Drawer) -> Unit = {_ -> },
)

Let’s unwind that. We’ll add draw directly to SpaceObject, and remove the reference to subscriptions.

import org.openrndr.draw.Drawer

interface SpaceObject {
    fun draw(drawer: Drawer)
    fun update(deltaTime: Double, trans: Transaction)
}

I think this will cascade some compile errors asking me to remove all the subscriptions, and to have a draw method everywhere.

IDEA walks me through all the cases, adding override to draw and deleting subscriptions. We are green.

The only odd thing was that I had to put an empty draw on DeferredAction. That will be able to be removed shortly. Now I can safe delete Subscriptions class.

Green. Commit: all interactWith methods removed from subscriptions throughout system. Subscriptions class removed.

Summary

Woot!

This is a happy day. After a long series of refactorings, with no missteps worth noting, we have slowly and safely removed the entire subscriptions model from the game, and isolated object interaction in a single class, Interaction, which calls type-specific methods in the various space object classes. This is a good thing.

Are we done-done? I think we’ll find things that we’d like to do. One possibility is to remove the type-specific part of the method name. Here’s an example:

    fun interactWithSaucer(saucer: Saucer, trans: Transaction) {
        checkCollision(saucer, trans)
    }

    fun interactWithAsteroid(asteroid: Asteroid, trans: Transaction) {
        checkCollision(asteroid, trans)
    }

    fun interactWithMissile(missile: Missile, trans: Transaction) {
        checkCollision(missile, trans)
    }

    private fun checkCollision(other: Collider, trans: Transaction) {
        if (weAreCollidingWith(other)) {
            collision(trans)
        }
    }

There are two issues here worth mentioning. First, there isn’t much value to the unique names “WithSaucer” and so on, because the type signatures clearly indicate what we’re interacting with. That’s the case throughout, and arguably it would be better to rename all these specialized methods to a single name, like interact, which would serve for all.

Second, in this case, we also have complete duplication between the methods, and that will be the case in other classes. Let me make the first change to emphasize that duplication.

    fun interact(saucer: Saucer, trans: Transaction) {
        checkCollision(saucer, trans)
    }

    fun interact(asteroid: Asteroid, trans: Transaction) {
        checkCollision(asteroid, trans)
    }

    fun interact(missile: Missile, trans: Transaction) {
        checkCollision(missile, trans)
    }

Commit: rename specialized interactWith methods all to interact

I think that over time, we’ll make the same change throughout, removing all the type-specific names.

My instincts are to eliminate that duplication, but it seems that it’s the price I pay for having all my types preserved. Yes, we could create a method referring to the common interface, as long as we have one, but if there is any object that doesn’t have perfect duplication for all these, we probably can’t do it.

I think we’re stuck with this, but as we’ve done here, we can get the duplication down to the one line by extracting whatever is inside.

I’ll review this with the gang during Tuesday’s Friday Night Geek’s Night Out Zoom Ensemble, but I think we’ll probably settle for this.

Bottom line, I think I like what we have here. But there is an issue we might consider.

It is a “rule” that all these classes must be able to interact with all, or most, of the others. But we no longer have that rule in the code. Of course, if we did call interactWithFoo on Ship, we’d get a compile time error, but you might wish to make the case for requiring all the interaction methods by way of an interface. We’ll think about that … later.

For now, we have simplified things substantially. This is a good thing and I am well pleased.

We’ll quit while we’re ahead. See you next time!