GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

Let’s compare the asteroid implementation in the flat version versus the centralized object-oriented version.

We’re studying the differences between these implementations, not to decide which is “better”, but just to sensitive ourselves to whatever design issues show up. Depending on the problem we’re solving and the architecture and language we’re using, we’ll find that different code fits our needs. Some of that might be objectively “better”, if there even is such a thing, but more likely it’s a matter of what fits the style, and what we’re comfortable with.

It follows, I think, that the more things we are comfortable with, the better we’re likely to do.

Here’s the Asteroid class from the centralized version:

// Centralized
class Asteroid(
    private var pos: Point,
    val velocity: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
    private val splitCount: Int = 2,
    private val strategy: AsteroidCollisionStrategy = AsteroidCollisionStrategy()
) : SpaceObject, Collider by strategy {
    init {
        position = pos
        strategy.asteroid = this
    }
    override val killRadius: Double =
        when (splitCount) {
            2 -> U.ASTEROID_KILL_RADIUS
            1 -> U.ASTEROID_KILL_RADIUS/2
            else -> U.ASTEROID_KILL_RADIUS/4
        }

    private val view = AsteroidView()
    val heading: Double = Random.nextDouble(360.0)

    override fun update(deltaTime: Double, trans: Transaction) {
        position = (position + velocity * deltaTime).cap()
    }

    override fun draw(drawer: Drawer) {
        drawer.fill = null
        drawer.translate(position)
        drawer.scale(U.DRAW_SCALE, U.DRAW_SCALE)
        view.draw(this, drawer)
    }

    fun getScore(): Int {
        return when (splitCount) {
            2 -> 20
            1 -> 50
            0 -> 100
            else -> 0
        }
    }

    fun scale() =2.0.pow(splitCount)

    override fun interactWith(other: SpaceObject, trans: Transaction)
        = other.interact(this, trans)

    fun splitIfPossible(trans: Transaction) {
        if (splitCount >= 1) {
            trans.add(this.asSplit())
            trans.add(this.asSplit())
        }
    }

    private fun asSplit(): Asteroid =
        Asteroid(
            pos = position,
            splitCount = splitCount - 1
        )

    fun distanceToCenter(): Double = position.distanceTo(U.CENTER_OF_UNIVERSE)
}

The most significant fact of this, of course, is that as an object, most everything about an Asteroid is right here in front of us. One nice thing about that is the killRadius. We know that each object in the game has a unique kill radius, representing its size, and thus how hard it is to hit. Just looking at this object, we can guess that in this version, each object will know its own kill radius and that it’ll be used as needed. Let’s compare that with what we’ve done, so far, in the “flat” version:

In the flat version, we break out different types of object explicitly, drilling down, finally, to some top-level code that knows the asteroid kill radius:

// Flat
private fun checkCollisions() {
    checkAllMissilesVsAsteroids()
    if ( Ship.active ) checkShipVsAsteroids(Ship)
}

private fun checkAllMissilesVsAsteroids() {
    for (missile in activeMissiles(SpaceObjects)) {
        checkMissileVsAsteroids(missile)
    }
}

private fun checkMissileVsAsteroids(missile: SpaceObject) {
    for (asteroid in activeAsteroids(SpaceObjects)) {
        checkOneAsteroid(asteroid, missile, U.MissileKillRadius)
    }
}

fun checkOneAsteroid(asteroid: SpaceObject, collider: SpaceObject, colliderKillRadius: Double) {
    if (colliding(asteroid, collider,colliderKillRadius)) {
        Score += getScore(asteroid,collider)
        splitOrKillAsteroid(asteroid)
        deactivate(collider)
    }
}

fun colliding(asteroid: SpaceObject, collider: SpaceObject, colliderSize: Double): Boolean {
    val asteroidSize = U.AsteroidKillRadius * asteroid.scale
    return collider.position.distanceTo(asteroid.position) <= asteroidSize + colliderSize
}

There, in colliding (which probably could use a more specific name) we see that our top-level function knows that it’s supposed to be called with an asteroid as first parameter and it has, baked into it, the expression for getting the kill radius. The five functions above are sorting down through the possibilities to ensure that we have an asteroid as first parameter. Contrast with the centralized version, where whatever we have will know its own radius and we won’t have to check.

However, it’s not all sweetness and light over in the centralized O-O code. We have an AsteroidInteractionStrategy that deals with collisions:

// Centralized
class AsteroidCollisionStrategy(): Collider {
    lateinit var asteroid: Asteroid
    override var position: Point = Point.ZERO
    override val killRadius: Double
        get() = asteroid.killRadius

    override fun interact(asteroid: Asteroid, trans: Transaction) =
        Unit
    override fun interact(missile: Missile, trans: Transaction) =
        checkCollision(missile, trans)
    override fun interact(saucer: Saucer, trans: Transaction) =
        checkCollision(saucer, trans)
    override fun interact(ship: Ship, trans: Transaction) =
        checkCollision(ship, trans)

    private fun checkCollision(other: SpaceObject, trans: Transaction) {
        Collision(asteroid).executeOnHit(other) {
            dieDueToCollision(trans)
        }
    }

    private fun dieDueToCollision(trans: Transaction) {
        trans.remove(asteroid)
        trans.add(Splat(asteroid))
        asteroid.splitIfPossible(trans)
    }
}

Seeing this, we expect that there will probably be such a strategy for each kind of object. We see that this is mostly boilerplate and that it all comes down to ignoring collisions with asteroids (the return of Unit above) and otherwise (we look at Collision.executeOnHit and assume that we know what it means) when there’s a collision with missile, saucer, or ship, we remove this asteroid, add a splat for a pretty little explosion, and ask the asteroid to splitIfPossible. That’s back in Asteroid, shown above.l

We might be interested in Collision, at least to be sure that we know what it does.

// Centralized
class Collision(private val collider: SpaceObject) {
    private fun hit(other: SpaceObject): Boolean
        = collider.position.distanceTo(other.position) < collider.killRadius + other.killRadius

    fun executeOnHit(other: SpaceObject, action: () -> Unit) {
        if (hit(other)) action()
    }
}

Right, it just checks to see if the two objects are within kill distance and if so, executes whatever block it’s given. Just as we suspected.

We might be wondering where the scoring is in this scheme, since it’s handled in the flat code with a simple Score+=getScore(asteroid,collider), which, if we look for it, looks like this:

// Flat
private fun getScore(asteroid: SpaceObject, collider: SpaceObject): Int {
    if (collider.type != SpaceObjectType.MISSILE) return 0
    return when (asteroid.scale) {
        4.0 -> 20
        2.0 -> 50
        1.0 -> 100
        else -> 0
    }
}

Ah, we check the missile type. (This is incomplete, by the way. When a saucer missile kills and asteroid, if we allow them do do that, the player doesn’t get the credit. We haven’t implemented saucer missiles yet, but we can imagine that in the code above we’ll need an additional condition.)

So in the centralized version, we might check the Missile object and its strategy to see if we find anything about scoring.

// Centralized
class MissileCollisionStrategy(): Collider {
    lateinit var missile: Missile
    override lateinit var position: Point
    override val killRadius: Double = U.MISSILE_KILL_RADIUS

    override fun interact(asteroid: Asteroid, trans: Transaction) =
        checkAndScoreCollision(asteroid, trans, asteroid.getScore())
    override fun interact(missile: Missile, trans: Transaction) =
        checkAndScoreCollision(missile, trans, 0)
    override fun interact(saucer: Saucer, trans: Transaction) =
        checkAndScoreCollision(saucer, trans,saucer.getScore())
    override fun interact(ship: Ship, trans: Transaction) =
        checkAndScoreCollision(ship, trans, 0)

    private fun checkAndScoreCollision(other: SpaceObject, trans: Transaction, score: Int) {
        Collision(other).executeOnHit(missile) {
            missile.score(trans, score)
            terminateMissile(trans)
        }
    }

    private fun terminateMissile(trans: Transaction) {
        missile.prepareToDie(trans)
        trans.remove(missile)
    }
}

We see that there’s some fancy prepare to die stuff. Our missiles do a cute little fizzle when they die in that version. And we see that we provide a score (or zero) to our Collision code, and we ask the missile to do the scoring. So we look back and find:

// Centralized
class Missile ...
    fun score(trans: Transaction, score: Int) {
        if (missileIsFromShip) trans.addScore(score)
    }

The Missile apparently knows if it is from the ship or not. We might explore why, we might not. In our future “flat” code for this case, we’ll probably have that check in our top-level getScore.

The Big Difference

It seems to me that there is one big difference in how we read the flat code versus how we read the object-oriented code. In the OO code, if we’re interested in how asteroid things are handled, we start at Asteroid and it leads us to its supporting cast, such as AsteroidCollisionStrategy, and Collision, and to its collaborators, such as Missile, which handles the final check in scoring.

So we start differently, and we bounce from method to method, passing through different classes as we follow the thread. Or, once we find out about a class like Collision or AsteroidCollisionStrategy, we can take a moment and review that class and build up a general understanding of it.

In the flat code, we’re faced with a large number of top-level functions. In our flat version now, there are already 48 top level functions in the program, and nearly 40 more test functions.

So if we want to know how asteroid collisions work, we have only two possibilities:

Start at the top
We can start at the top, in gameCycle and drill down through function after function, until we start finding functions that relate to asteroids. We’re never sure whether we’ve found them all: they have nothing in common other than their names and parameter lists — if we’re lucky!
Search for names
We can search for names, using a concordance if we have one, or just with a standard search such as provided by our IDE. For example, IDEA has a search that can use a regex, so we could search, maybe for fun.*[Aa]steroid and hope.

In a flat style, the larger the program gets, the more functions we have and the harder it is to find things. In the version I’ve written, I’ve already created four files, Component, Game, SpaceObject and the main, and I’ve grouped related functions into those files. But the Game file has most of them, so that’s not helping me much.

In a well-organized object-oriented style, we’ll get more and more objects, but each object will typically contain only a few related chunks of data and operations. Generally speaking, we’ll know which object to start with, but it’s fair to say that in a large enough program, knowing which object to look at does become an issue. But in a flat program of the same complexity, we won’t even have that classification to help us.

Which is Better?

Ha! You can’t trick me that easily. I’m not going to say which is better overall, but I will say that for me, the object-oriented style helps me understand the program better, because I’m pretty good at coming up with objects that make sense (at least to me and I hope to any reader), and I’m pretty good at noticing when an object seems to be wishing that another object were more helpful. I’m even pretty good at noticing when an object is doing more than one “kind” of thing, and using that as a hint that maybe there’s another object class that, if created, would be helpful.

In other words, I’m pretty good at using the object-oriented language, in particular classes and methods, to organize my work so that I can find things quickly.

I’m not as good at organizing a flat style like I’ve been working on here. We saw that yesterday, when there were three different places in the code that needed to change if I wanted asteroids to have a random angle of rotation. Clearly I didn’t see the common aspects of that code before I discovered that I needed to find it, or I’d have centralized it earlier. And because I hadn’t centralized it, I did each of the three things differently, not for a good reason so much as just because I was focused on the particular situation and had no general overview. Had I been working in an Asteroid class, the chances are better that I’d have seen Oh, there’s already a way of doing most of this, and I would have a better chance of keeping my stuff together.

I’m sure there are tricks of organization for working with code made up of all top-level functions. If I’d been doing that for the last three or four decades rather than working with objects, I hope I’d know some. As it stands, I don’t. Feel free to suggest some reading for me if you know of any.

How does a new person learn?

Many, perhaps most of the languages in common use today provide objects. But how does a new developer learn to use them well? Sadly, much of the code my friends and I encounter tells us that the new developer often does not learn these things as rapidly, or as well, as they might. We see code in object-oriented languages that makes very little use of objects created by the developers. Yes, it’ll use objects provided in the libraries and frameworks they use, but they don’t create their own little objects.

Why not? Because no one shows them how to do it. No one causes them to practice doing it.

History Matters

In my history, I had luxuries that many developers do not have.

I started my career in a job where what I was supposed to do was study new programming languages and techniques, so as to bring that information back to the organization I was a part of. Over the course of a couple of years, I studied Assembler, FORTRAN, LISP, Slip, IPL-V, even a bit of JOVIAL. I had time to read books about programming, and I enjoyed it.

I took a break to go back to grad school, and while I did adequately in those programs, mostly I spent my time learning programming techniques on bizarre machines like the Burroughs E-101 and the IBM 1620. I studied SNOBOL!

I’ll spare you the rest of my history: it spans eight decades. But my early experience caused me to be focused on learning as well as developing, so that I was always reading the books and papers about whatever we were doing. I was fortunately able to go to conferences, to learn and to meet people.

And, with all that, when I first started doing OO programming, and reading about it, I still wasn’t very good at creating objects that served me well. Sometimes I wonder if I’m even any good now, but I know I’m better than I was, because the last quarter century has exposed me to some real experts who have been kind enough to talk with me, to show me what they’ve done, and to advise me.

The average new developer doesn’t have the privileges I’ve had, and may not have developed the insane desire to read about programming after the day’s work is done. They may never meet someone who’s a real expert, they may never have anyone sit with them and say “Look, if we do this … and then this …” to help them see what’s possible.

And Therefore …

Well, that’s why I write these things, in the vain hope that once in a while, someone will stumble onto one of my articles and get an idea that will let them adjust their path toward thinking a bit differently, working a bit differently, learning a bit more, practicing a bit more, collaborating a bit more, experimenting a bit more.

What About “Better”?

Well, different. The object-oriented versions of this program, as we have seen in this article, and as we’ll probably see in a few more, offer a way of organizing the code to better reflect our ideas about it. In all the versions, we’re thinking about ships and saucers and asteroids. But those ships and saucers and asteroids are more present in the object-oriented version than they are in the flat version.

One of Kent Beck’s rules of simple design is “The code expresses all the programmer’s ideas about the program.”

Our ideas surely include “there are asteroids”. In one of these programs, the idea of asteroids is much more present than in the other.

And while object-oriented may or may not be better than flat, asteroids being more present is surely better.

Go now in peace, reflect on this, tweet or toot me up if you care to, and come see me next time.