GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

Last night we briefly discussed a comparison between my versions. Let’s think about that. Alligator?

It all started, Doctor, when I was a bit bored with the discussion of movies and television that I’ve never watched and never will, and so I took a line count of the current “flat” version of the program.

It is 516 lines right now, compared to 1344 lines for one of the other two versions. I think we’re about 80 percent equivalent right now, with saucer firing and steering to do, a bit of addition to collisions, and hyperspace.

Estimating size at equivalence, I’d be pretty confident that we’d come in under 800 lines, or about 60% of the size in lines of the other versions, for the same capability.

After I announced the figures to the Friday Geek’s Night Out Zoom Ensemble last night (Tuesday, of course) we spoke briefly about what conclusions, if any, could be drawn. I think it was Chet who pointed out that in Kent Beck’s four rules of simple code, minimizing the number of lines comes last, behind running all the tests, expressing all our ideas, and removing duplication.

GeePaw Hill thinks I could (and should?) write maybe 25 articles comparing the versions, talking about their differences. This would be in aid of deciding which is “better”, except that we all believe that “better” isn’t a term that can be applied that way. Is a butterfly better than an alligator?

Let me riff on the topics I recall from last night, and on whatever tunes they bring to mind.

Horses for Courses

The flat version is very procedural and really just flows one thing after another in a pretty clear fashion. Take the game cycle for example:

    updateEverything(spaceObjects, deltaTime, width, height)
    drawEverything(spaceObjects, drawer)
    checkCollisions()
    drawScore(drawer)
    checkIfShipNeeded(deltaTime)
    checkIfSaucerNeeded(deltaTime)
    checkIfAsteroidsNeeded(deltaTime)

For fun, one of these days, I’m going to inline everything into one big function, or as close as I can get to that, just to see what it looks like. The thing is, it will be possible to do that. In fact, watch this for the first level of doing that:

{
    for (spaceObject in spaceObjects) {
        for (component in spaceObject.components) update(component, deltaTime)
        if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
        move(spaceObject, width, height, deltaTime)
    }
    for (spaceObject in spaceObjects) {
        if (spaceObject.active) draw(spaceObject, drawer)
    }
    for (missile in activeMissiles(SpaceObjects)) {
        for (asteroid in activeAsteroids(SpaceObjects)) {
            if (colliding(asteroid, missile, U.MissileKillRadius)) {
                Score += getScore(asteroid, missile)
                if (asteroid.scale > 1) {
                    asteroid.scale /= 2
                    asteroid.velocity = randomVelocity()
                    spawnNewAsteroid(asteroid)
                } else deactivate(asteroid)
                deactivate(missile)
            }
        }
    }
    if (Ship.active) {
        for (asteroid in activeAsteroids(SpaceObjects)) {
            if (colliding(asteroid, Ship, U.ShipKillRadius)) {
                Score += getScore(asteroid, Ship)
                if (asteroid.scale > 1) {
                    asteroid.scale /= 2
                    asteroid.velocity = randomVelocity()
                    spawnNewAsteroid(asteroid)
                } else deactivate(asteroid)
                deactivate(Ship)
            }
        }
    }
    drawScore(drawer)
    if (!Ship.active) {
        shipGoneFor += deltaTime
        if (shipGoneFor > U.ShipDelay) {
            dropScale = U.ShipDropInScale
            Ship.position = Vector2(U.ScreenWidth / 2.0, U.ScreenHeight / 2.0)
            Ship.velocity = Vector2(0.0, 0.0)
            Ship.angle = 0.0
            Ship.active = true
            shipGoneFor = 0.0
        }
    } else {
        dropScale = max(dropScale - U.ShipDropInScale * deltaTime, 1.0)
    }
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) {
        saucerGoneFor = 0.0
        if (!Saucer.active) {
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(saucerSpeed, 0.0)
            saucerSpeed *= -1.0
        } else {
            Saucer.active = false
        }
    }
    if (activeAsteroids(SpaceObjects).isEmpty()) {
        AsteroidsGoneFor += deltaTime
        if (AsteroidsGoneFor > U.AsteroidWaveDelay) {
            AsteroidsGoneFor = 0.0
            activateAsteroids(nextWaveSize(currentWaveSize))
        }
    }
}

Lovely, huh? Of course that’s just the beginning of the unwinding we could do. Of course, in unwinding those functions, IDEA also unwound some tests for those functions, tests that we’d almost certainly never have written if we wrote the program out longhand as above.

But which is more understandable? Of course that depends on the individual and their experience and the focus of their work. A procedural programmer who has only ever worked in C would probably find the flat version easier to understand, because it doesn’t have so many of those pesky objects, whatever they are. An experienced O-O programmer might find the other versions easier, because if you want to know how the asteroid works, you mostly just have to look at the Asteroid class.

And the decentralized version? My experience with the Zoom Ensemble is that no one understands it out of the box. I guess I’d have to say that I understand how it works, but that even I can’t tell you exactly how it works without looking at the code. Why? Because it’s built of independent inter-operating objects, where one looks for another and maybe creates a third that does something to the other and then goes away. The game “emerges” from the interactions of these independent objects.

I think it’s incredibly elegant, but experience suggests that while it is quite easy to change how it works, once you know how, it’s not easy to get up to speed on how.

The centralized version is quite object-oriented as well, and it does the usual O-O thing of deferring responsibility to other objects. And it has a nice double dispatch approach to deciding what’s interacting with what.

In the flat version, when two objects collide, we deal with both of them right then and there. We have no transactions, no special collections, just a fixed array of space objects.

I think I’d nod if someone were to argue that the flat version is simpler on a number of dimensions.

Speaking of Tests

Speaking of tests, there are 28 in the flat version, and 85 in the centralized version. Given that these two programs do essentially the same thing, play an Asteroids game, what might we guess about the two programs in terms of the chances that they are correct? What would we guess about our chance of making changes without making mistakes? We even know that the same person wrote both programs.

My guess is that there’s a somewhat larger chance of a defect in the flat version and that the flat version is quite a bit more prone to errors when making changes.

Given that tests are one of the ways we express ideas in our code, it seems likely that the centralized version expresses more of my ideas than the flat version.

Expression of Ideas

The flat version does a pretty decent job of expressing the procedural ideas of asteroids, at least until I unwind the functions, as I do above. Someone commented last night that I’m pretty good at factoring: I tend to pull out nameable chunks of functionality, as we see in the original game cycle code above. But that’s a skill that not all programmers have. It comes with practice, and for some new programmers, there’s nothing inducing them to learn to do it. They tend to just code in line and to make separate functions less frequently than I might.

This means that the ideas they have are less well expressed in the code. When you look at the unwound version above, if I asked you to find the code where we decide whether to do a new wave of asteroids, it would probably take you a little while to find it. It would take far less often in the first version. Hint: checkIfAsteroidsNeeded(deltaTime).

But where does the flat version, as in the top display, stand on expression of ideas compared to the other two versions? Its comparable code is in an object called GameCycler:

    fun cycle(deltaTime: Double, drawer: Drawer?= null) {
        tick(deltaTime)
        beforeInteractions()
        processInteractions()
        U.AsteroidTally = knownObjects.asteroidCount()
        createNewWaveIfNeeded()
        createSaucerIfNeeded()
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }

I’d say that’s about as expressive as the flat version, and it’s just about as easy to find, since the main program will lead you right to it.

Maybe the difference is about density of ideas, or its distribution. All the ideas we have are pretty much banged into procedure in the flat version, and so some of them are easy to find and some you have to figure out the code. In the more object-oriented versions, information and ideas are broken out into smaller chunks, separate objects, each of which is mostly understandable on its own. I should emphasize mostly, because they do interact and rely on each other as well.

I think it’s likely that more ideas are expressed in the O-O versions, which is good in that we can understand what the programmer intended, and not so good in that there are more ideas to attract the eye when we’re trying to juggle the ones we have.

Change

The big question, one that we can’t answer right now, is how hard this thing is to change. Hill made the point that Asteroids has a pretty solid spec and when it’s done, it’s done. These days, most programs are never done. When people like programs, they want more features, get more ideas, so developers spend years improving the same program until they just can’t improve it any more.

In future articles, maybe, and I’m really not promising to do this, we might consider some kind of substantial change to the Asteroids program, and assess how hard it is to make the change in each version. I am hesitant to do that for a few reasons, including:

  1. It sounds incredibly tedious;
  2. I don’t have any sensible ideas that seem interesting;
  3. It would be hard to assess how difficult each one was, because learning from one would impact the next;
  4. It still sounds boring.

However … in the scheme of today’s programming, this is the big question: what approaches to programming and design are going to give us the best chance of making the changes that will be demanded of us?

I do have some guesses:

  1. More good tests will make changes easier;
  2. A lot of bad tests will make changes harder;
  3. It’s hard to know the difference between good and bad tests in the moment;
  4. Separating ideas out is valuable;
  5. Functions can help separate ideas;
  6. Objects can help separate ideas;
  7. Functions and objects done poorly can entangle ideas;

It ain’t easy. Programming ain’t easy. The tools and techniques can help us, but in the end, we have to learn to use them well, in a fashion that lets us evolve the program, essentially forever.

That’s what makes it fun … and what can make it frustrating.

Conclusion

Conclusions? I have none. I have observations. This program is smaller and that will make it easier to work with in some ways. The other program is more modular and that will make it easier to work with in some ways.

We’ll see what we see. Mostly, we probably benefit from practicing and thinking while we do it.

See you next time!