GitHub Decentralized Repo
GitHub Centralized Repo

Last night, I made a change that I now believe to be a mistake. In fact, I did it twice. Further: We begin to draw a conclusion.

The Mistake

The Friday Night Geek’s Night Out Zoom Ensemble last night (Tuesday of course) didn’t get around to looking at my code. I might be glad of that now, because I might have been embarrassed. Still, I’d like to know whether anyone would have seen the mistake that I made.

As frequent readers know, one of my prime cues that something in the code could be improved is duplication. When I see the same code in more than one place, I take it as a sign to do something about it, and I find that, usually, when I remove the duplication the code is a bit better than it was. So it should be no surprise that my eye was drawn to these instances of duplication in the Asteroids game:

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

    fun interact(ship: Ship, trans: Transaction) {
        checkCollision(ship, trans)
    }

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

class Ship ...
    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)
    }

In both Ship and Asteroid, these are the only cases for interact, and they are all identical. So in each case, I commented out those methods “for documentation”, and replaced them with this:

    fun interact(collider: Collider, trans: Transaction) {
        checkCollision(collider, trans)
    }

By accepting a more general first parameter, any Collider, I was able to collapse those three methods down to one.

That was a mistake. It was a harmless mistake, in that it doesn’t break anything, at least not now. But it’s a mistake. Excuse me for a moment while I reverse my mistake. Test, green, commit: remove mistaken open versions of interact, go back to type-specific ones.

Why was it a mistake? Well, because if, just if, someone were to pass a ship into Ship.interact or an asteroid into Asteroid.interact, they would be accepted. It’s not clear what might happen after that. Perhaps asteroids would start destroying each other. In the case of ships, the only likely possibility is that the ship in question would be the only ship we have, and it might “mutually” destroy itself.

Whatever would happen is certainly not what we intended.

So, in this case, removing duplication was a mistake, at least if done this way, by relaxing the constraints on the calling sequences. Maybe there is some other way to remove this apparent duplication. I suppose I could create special interfaces, like Collider, that just allowed the three classes through. That would be more code and far more obscure, so it doesn’t seem like a good idea to me, although if the interface were named MissileShipOrSaucer it might communicate well enough. I don’t think I’ll do that, but it would work.

Reflection

Now, I freely grant that because I haven’t used a language with strict compiler-time typing for years, probably decades, I am not as quick-thinking as I might be with Kotlin. In addition, Kotlin itself has some quirks that I’m sure I haven’t absorbed. For those reasons, and because it’s me, I forgive this mistake and learn from it. And it’s that last reason that is most important.

Improve, don’t blame.

Because we are just ourselves, mere humans, we can and will make mistakes. We’ll make some mistakes only once, and some we’ll make again and again. In Smalltalk, I used to forget to return results from methods, until I changed the default method that is provided by the browser to include ^result, which means “return result” in Smalltalk. After that, I couldn’t forget, because the method wouldn’t compile until I provided a result somehow. I made that mistake again and again until I figured out a way to help myself remember.

In Kotlin, of course, if I declare a method as returning a result:

    fun someMethod(arg): Thing {...}

I can’t forget to return the result because Kotlin and IDEA will remind me. And if I return a result but don’t declare the method that way, it’ll warn me about that. And if I forget both those things, when I call the method expecting a result, that probably won’t compile. So Kotlin and IDEA prevent me from making that mistake.

But sometimes, all we can think of to do is to remind ourselves not to do a thing. It helps to mention the mistake, to tell someone about it. They might have a useful idea, but mainly, we’re just trying to burn the issue into memory so that we’re less likely to do it again.

So, I give a rueful laugh at my fallibility, and remind myself that I am in fat fallible, and resolve not to get too casual about what I’m coding.

One more trick: a test.

Often, if we make a mistake in the code, and find it, we can write a test to be sure that we won’t make that mistake again. It’s usually quick and easy, and well worth doing. If the mistake caused an actual defect, it’s quite common to write a test that should run, see that it doesn’t, and then fix the problem. Back when I used to make recommendations, I used to recommend that we then go ahead and think about other places where a similar thing could happen, and write tests for those as well. I no longer make recommendations, or at least I try not to make very specific ones, so I no longer recommend that. I will say, however, that it’s a darn good idea.

In this case, I don’t know how to write a test that will show there is no implementation of interact that is too generic. If there is no such method, the test wouldn’t compile, and I can’t have a test in the system that won’t compile. So I don’t know of a decent way to test for this one.

So I’m left with trying to burn into my memory not to use generic entry points as often as I would in a duck-typing language. I hope it’ll stick. In this case, that’s all I’ve got, unless some friend sends me a better idea.

Are We There Yet?

This whole series or articles has been about converting the decentralized version of the game to a more centralized design. The decentralized version works with a bunch of tiny, semi-intelligent objects, interacting however they wish. Somehow, cooperatively, the result is an Asteroids game, while the core classes, Game, and SpaceObjectCollection contain no vestige of Asteroids game play.

In the decentralized version, there is no single place that knows there are asteroids, missiles, ships, saucers, and splats. There is no single place that knows that when a missile hits an asteroid, the missile is destroyed and the asteroid splits. On the contrary, the missile knows to destroy itself when it hits things, and the asteroid knows to split.

Now in the current version, some of that remains true. The asteroid still decides to split, and the missile still decides to die. I think that in an object-oriented implementation of Asteroids, that makes sense. But much more knowledge of the game has been drawn inside of SpaceObjectCollection and Game (and a new Game helper, Interaction).

  • This version keeps the object types distinct: the other basically ignores the types.

  • This version directly decides when to make waves and saucers and ships: the other had tiny objects that would notice that something needed to happen and do it.

  • This version has only asteroid, missile, ship, saucer, and splat in its main loop mix: the other version had a handful of other “special” objects as well

  • This version manages collisions carefully, with explicit calls interacting only pairs that need to interact: the other version allowed interaction of all combinations of pairs of object.

  • This version has type-specific methods throughout: the other version had a clever mechanism that allowed objects to sign up for just the messages they wanted to see. (Perhaps too clever.)

The question before us is: are we done? Have we moved far enough in the direction of more conventional standard implementation in a language with strict typing? Have we created enough of a difference between the versions to step back and assess the differences?

I think we are quite close, if not done. We might do one more experiment, perhaps implementing the small saucer, which is the only feature I can think of that’s missing. We might make some “unexpected” change, such as changing the saucer so that it doesn’t collide with asteroids. Something like that. We’d compare how difficult those changes were in the two versions, and perhaps draw some conclusions about which is “better”.

The thing is, my intuition tells me that those changes will be pretty similar in terms of difficulty, that is, “not very difficult”. Why? Because each of these designs is “good” in the sense of having responsibilities separate enough to make change easy. They’re both pretty well designed and well factored.

Analysis

Here’s an interesting fact. I just did wc -l *.kt on both versions. Here’s the result:

Decentralized: 1336 lines. Centralized: 1343 lines. That’s almost magically close. Game has increased from 84 lines to 147. We’ve removed classes like WaveMaker and ShipMaker from this version. We’ve added Interaction class at 60 lines.

OK, I’m going to bite the bullet and make a complete table for us to look at. I’ve highlighted significant differences. The lower value is arguably the winner, since fewer lines is generally better.

Decentralized Centralized File
73 75 Asteroid
57 57 AsteroidView
5 5 Collider
6 10 Collision
43 41 Controls
25 29 DeferredAction
84 147 Game
5 0 ISpaceObject
5 8 InteractingSpaceObject
0 60 Interaction
79 76 Missile
14 16 OneShot
141 147 Saucer
15 0 SaucerMaker
100 100 ScaleDisplay
14 0 Score
83 0 ScoreKeeper
132 131 Ship
22 0 ShipChecker
61 0 ShipMaker
25 25 ShotOptimizer
68 102 SpaceObjectCollection
34 31 Splat
28 28 SplatView
19 0 Subscriptions
44 44 TemplateProgram
40 56 Transaction
11 11 Tween
54 61 Universe
28 0 WaveMaker
1336 1343 TOTAL


Let’s look at just the ones I’ve decided were “significant”:

Decentralized Centralized File
84 147 Game
5 0 ISpaceObject
0 60 Interaction
15 0 SaucerMaker
14 0 Score
83 0 ScoreKeeper
22 0 ShipChecker
61 0 ShipMaker
68 102 SpaceObjectCollection
19 0 Subscriptions
40 56 Transaction
28 0 WaveMaker
1336 1343 TOTAL


The decentralized version “wins” by removing a half-dozen specialized objects, but picks up essentially the same amount of code in Game and Interaction. It also made heavy modifications to Transaction and SpaceObjectCollection, sorting out the type preservation.

The most amazing thing to me is that the total line counts are almost equal between the two versions.

We might possibly save more lines by folding the code for all the interactions into the Interaction object, and carefully optimizing it for space. The results might be hard to understand and difficult to change, but might be a bit smaller. I wouldn’t care to do that job. I think it would be hard to get right, and I don’t think it would save many lines of actual code. It might save some blank lines and lines containing just fun something or }.

But which version is “better”?

We may return to this question, but today my thoughts include:

  • Centralized is arguably better in that it’s more like what you might expect, a Game class that understands the game. The decentralized doesn’t include any visible abstraction of the Asteroids game at all. The centralized at least creates objects we can understand and makes decisions that are visible and make sense to us.

  • Centralized is arguably better in that it is closer to Kotlin’s preferred style as regards classes and data types: it uses them. The decentralized essentially bypasses all type checking to provide a very flexible, but quite unusual sort of “duck typing”, in a language carefully designed for type specification. The decentralized version subverts Kotlin’s preferred style.

  • Decentralized is arguably better in that it makes use of many more small cooperating objects, which is arguably a better object-oriented style. That said, that style is quite often not present in the code that the bulk of today’s teams actually write. We commonly small numbers of large objects rather than large numbers of small objects. So this advantage might be a disadvantage in the eyes of many programmers, who are used to big classes with everything jammed into them.

  • Decentralized is arguably better in that it is probably a better base for a very different game, ranging from one with more than one ship in the same space, to a completely different game like Space Invaders or Breakout. Any game with a bunch of objects flying about should fit in rather nicely. (I grant that this is an unsupported claim, but I could win a debate about it.)

Tentative Conclusion

Perhaps the conclusion should be “horses for courses”, but I think that I might give a narrow edge to the centralized version if I had to hand it over to standard OO programmers to maintain. I think they’d understand it more quickly. Even at the hands of the more evolved programmers I know, the game’s design seemed mysterious, because the actual notion of “Asteroids” isn’t expressed anywhere: it emerges from the cooperation of WaveMakers and ShipCheckers as well as the actual objects.

There might be a way to better introduce how the decentralized version works, but as it stands, you have to look around more than we’re accustomed to before you can begin to see what’s going on.

I would argue, because I’m the programmer who is most experienced with the decentralized version, that once you get it, it’s quite simple and nice. I would also accept that you do have to do a bit of juggling in your mind to sort out what will happen, and when.

So … I think the decentralized version is more complex, and that it is, as a result, also more flexible and more powerful. If all one were going to do was Asteroids, the additional complexity might not be worth it, but I’m not sure. I think it’s a narrow gap, and I think I’d give the real-world preference to the centralized version. As a learning platform, the decentralized version taught me more.

Inheritance vs Delegation

We ought not forget that there were really two decentralized versions, one that worked using implementation inheritance, providing empty default methods for interactions or other events that objects didn’t want to handle, allowing each object to implement just the things that concerned it.

At GeePaw Hill’s instigation and with his important help, we replaced that implementation inheritance with the elegant but strange Subscriptions object, which implements empty default lambda expressions for interactions or other events that objects don’t want to handle, allowing each one to subscribe to and then implement just the things that concern it.

I think there’s little question that inheritance of implementation is a dangerous and tricky thing, and that it is generally not recommended. There are famous examples showing how, sometimes, there is literally no way to get what you want. All that is true, but in the case we actually had, the inheritance was simple and almost always just a default non operation. Were it not for Hill’s mother having been frightened by implementation inheritance while he was in the womb, we might never have considered the original design to be a problem, and we might never have discovered the quite amazing Subscriptions object.

Even freely granting that, I think that the version with implementation inheritance worked quite nicely, in part because it was subverting the Kotlin strict typing model, and to a degree, I prefer it over the Subscriptions version.

Today’s Bottom Line

All these differences and preferences seem very narrow to me, and I am quite certain that different individuals will have different preferences, some quite strong compared to mine.

I’d like to hear from any of you who would care to comment. Best place may still be twitter, but I’m starting to prefer mastodon.social. I’m ronjeffries on both.

See you next time!