Kotlin 135: Refactoring / Clarity
My Zoom Buddies made an interesting observation. I choose to respond productively.
Last night in our Zoom session, I was displaying the code that I spiked for emergence, that conditionally destroys the ship on emergence, and, of course, to do so I had to explain the various objects involved. Toward the end of my remarks I said something about the interactions being hard to explain, and I think it was Hill who very gently commented that because it was so hard to explain, yes, it might be too complicated. As one is supposed to do, I took that as well-intended, even though he was certainly somewhat dissing my baby1, though I did do a quick explanation of how an asteroids with central control would be more complicated than you might think as well. Not that Hill didn’t have a good point: if it’s hard to explain, maybe it’s too complicated.
Today I want to explore that complication, and I’m planning to refactor a bit, mostly with names, to aid in making the program more clear. Let me start with some general observations about self-explaining code, comments, and the like.
Self-Documenting?
I do think code should be made quite clear, because its primary purpose is to communicate with humans, those who read our articles, more importantly those on our programming team, and of course, our future selves. If it were not the case that code is for humans, we’d code in binary, which would make computers a whole lot happier, I’m sure.
Some have taken the observation that code is for humans and asked us to put lots of explanatory comments in the code. That is not my way. Long ago, Kent Beck said “A comment is the code’s way of asking to be made more clear”, and I took that to heart. I try never to need a comment, and when I do, I only put one in after having thought hard about how to make it unnecessary.
When in courses or consulting I have been asked about comments “as documentation”, I’ve often added the observation that the most important comment in any code that I have to read is the phone number of the person who wrote it.
Agile methods, such as I espouse, using the historical good meaning of “Agile”, not the current polluted evil meaning, are about “individuals and interactions” and when someone needs to understand the code in the team’s system, it’s best if they sit down with someone who understands it and work together with them to improve it and to better understand it. Far better than just reading even “well-commented” code.
And while they sit together, they improve the code to make it easier to understand. It’s surely not possible to make all code easy to read and understand, but I’ve found that while that it true, the code we’re looking at right now can almost always be improved greatly. We rarely if ever hit the limits of how clear our code can be.
So today, I’m going to describe how the system works, and refactor to make the code reflect my description one the one hand, and let the code refine my description on the other.
Let Us Begin
One way to write an Asteroids program would be from the viewpoint of the Game, a kind of God which knows all things in it intimately, knows which ones are asteroids, which are missiles, and so on. This GameGod would, in its wisdom, move all the objects around according to complex rules that the GameGod knows, and when it so decides, it brings a missile into contact with an asteroid and, as it deems in its wisdom, destroys the ship and splits the asteroid. The gameGod then decides what score you get and updates the score on the screen. It then goes on to move and consider other asteroids, missiles, and so on. Later, when it feels like it, the GameGod decides whether you get a new ship, and if so, creates one.
That’s not even how the real universe works, but that’s not important right now2. We are, however, going to describe a game universe that is more like the real one than a GameGod universe is.
Game Objects
From the player’s viewpoint, the primary objects of the game are the ship, the asteroids, and the missiles. The player drives the ship around, causing it to fire missiles, while steering to avoid the asteroids. The asteroids move on their own, in straight lines. When one of the player’s missiles hits an asteroid, the asteroid splits, down to a certain smallest size, the player receives points for destroying the asteroid, and things carry on. In the mind of the player, all the asteroids, the missiles, and the ship itself are unique and more or less active, moving and colliding on their own.
If the player were to think about it, they might observe that there must be some hidden powers in the game. They’d observe that there is a score on the screen, which updates. That must be happening somehow. There are time delays between a ship being destroyed and another created. That must be happening somehow. And so on. The player might conclude that there is a GameGod controlling the details of the universe. In some games they would be correct. In this game, they would not be.
Our game has a number of objects that interact in ways we’re on a course to describe. But I must digress to describe our “game god”, because it’s not much of a god at all.
There Is No God
Our GameGod is hardly a god at all. It’s just a simple program that contains a collection of objects about which it knows very little, and it does just a few things when called upon to do so. Every sixtieth of a second, our game is told to execute its sole function: cycle
. It receives a “drawer”3 and the current time in seconds.
The Game only knows two things, a collection currently called flyers
, and the last time it was called. And when it is told to cycle, the game just does these things:
update
processInteractions
draw
In update
, the game first calls update
on each of its flyers
. We’ll come back to what they are. Whatever they are, when updated a flyer
can return one or more other flyers to be added to the game’s collection. After all the flyer have been updated, the game adds any new ones to the flyers collection.
In processInteractions
, the game calls colliders
, which returns a list of flyers to be removed. We’ll speak of colliders
in a moment. The game, upon receiving the list of flyers to be removed, removes them all from the flyers collection, essentially killing all of them. To make sure they appreciate their situation, after all the interactions are done, the game sends each newly dead flyer finalize
. It collects the returns from these calls and adds them to the flyers collection.
In draw
, the game calls draw
for each one of its flyers, passing in the drawer.
One more detail and we’re done with Game: colliders
.
The game’s colliders
function, through a bit of rigmarole, simply calls f1.interactWith(f2)
for each unique pair f1,f2 in the flyers collection. If it calls a.interactEith(b), it will not call b.interactWith(a): each unique pair is only considered once, and there’s no rule about which one will have the interactWith
function called, and which one is the parameter. It’s essentially random, as if we picked a random unique pair, picked on of the two at random, and called its interactWith
, passing in the other.
That is all that our “game god” does. To summarize:
update
each flyer, adding any new flyers it may return into the flyers list.- Using
colliders
, cause each pair of flyers to interact, collecting any flyers that should be removed. Remove them. finalize
each object that needed to be removed. If it returns any new flyers, add them back into the flyers list.draw
each flyer.
I hope you’ll agree that this isn’t much of a god. It makes no decisions, does not assess the sins of the objects, nor does it watch sparrows fall and shrug. In this universe everything that happens happens because an individual object, or a pair of objects, decides it should happen.
Circle of Life
- In
update
, any object can give birth to on or more another objects. One case of this is that the ship fires missiles duringupdate
, adding a missile to the mix every time you click the fire key. - In
colliders
, any pair of interacting objects can return objects that are to be destroyed. - Each object that is to be destroyed is told to
finalize
, and it can return new objects to be born and added to the flyers.
The Essential Point
The essential point is that the things that happen in the game are not due to some GameGod making grand decisions. They happen because one or two objects decide that something should happen. Let’s consider four key cases, a missile colliding with an asteroid, and an asteroid colliding with the ship, and then score-keeping and ship renewal.
Missile-Asteroid
Every pair of objects gets to interact. A missile, wherever it is, “interacts” with every asteroid. If they are close enough to be colliding, both the missile and the asteroid are returned, and therefore removed from the mix.
The asteroid and missile both get a chance to finalize. Each has a unique finalization function. The missile creates a splat
, a little flyer that displays fireworks where it explodes. “Ooo, pretty!” The asteroid does something a bit more complex: it creates a Score
object containing the score you earn for killing this size of asteroid. If the asteroid can split, it creates two new smaller asteroids. It returns either just the Score
or the Score
and the two new asteroids. These are all duly stuffed into the flyers
, to be processed hereafter. The asteroids go on about their business, drifting in space. We’ll come back to the Score
but I’ll say here that it gets swept up and the score posted on the screen.
Asteroid-Ship
Every pair of objects gets to interact. An asteroid, wherever it is, interacts with every other object. One of those is the ship. When the ship and an asteroid interact, if they are close enough to be colliding, the asteroid and the ship are returned, and therefore removed from the mix.
The asteroid behaves as above. Yes, you do get the score for destroying an asteroid with your ship: it’s just not very efficient.
The ship will be given a chance to finalize. We may look at this in more detail, but suffice to say that it doesn’t return anything to be created. It is gone from the mix. No ship will be updated, collided, nor drawn. It’s gone, absent, missing, outa here.
Scoring
When last we saw it, a finalizing asteroid had dropped a Score
object, containing your earned score for the strike, into the mix. What happens?
It turns out there is an object called ScoreKeeper
in the mix. It interacts with all the other objects on each cycle. If the other contains a score (greater than zero), the ScoreKeeper
object adds it to its internal tally of the total score, and returns the Score
, which causes it to be deleted. Only Score
objects actually contain a score value other than zero.
When the ScoreKeeper
is told to draw
, it displays the score on the screen, now containing the new tally.
Let’s review the circle of life for an asteroid and its score. In colliding with a missile (or ship), the asteroid is removed from the mix, but it finalizes by producing a Score
. At the same time, it may also produce two more smaller asteroids, which go about their asteroidal business. Shortly, the Score
object “collides” with the ScoreKeeper
, which returns the Score
to be destroyed, but records its score, which the ScoreKeeper
displays in its draw
function.
Objects and Interactions
Just as happens in the real universe, except that there is no score that we know of, everything happens because objects are interacting with each other, performing simple actions (delete this add that) that together sum up to make interesting and complex things happen on the screen of the game or the screen of the universe.
These same individuals and interactions allow for the ship to be renewed (and support hyperspace). That’s done by the ShipMonitor.
ShipMonitor
It turns out that there is another special object in the mix, called ShipMonitor. It was created with direct knowledge of the ship. (It holds the ship in a member variable.) When the ShipMonitor interacts with all the other objects in the mix, it notes whether the ship is among them. If not, the ship has been destroyed, and the ShipMonitor springs into action, after a short delay, a moment of silence if you will. After the delay, the ShipMonitor waits until the Ship’s restarting point (center screen) is clear, and when it is, it returns the ship (from update
), causing the ship to “exist” and be able to fly around again.
The code for its states is a bit complicated, but the behavior is simple: notice that you haven’t seen the ship, pull it out of your member variable and return it to be put back in the mix.
Tell ‘em what ya told ‘em
To re-summarize again … each object in the mix gets told update
. Typically, it moves. Optionally, it can return something, like a missile or a ship. If it does, that thing get put into the mix.
Each pair of objects interacts. Typically, they do nothing. Sometimes they collide, in which case both are usually returned to be forgotten by the Game. Sometimes they have side effects: ScoreKeeper
remembers the score from Score
, and returns it to be destroyed. It does not return itself, so that ScoreKeeper persists.
Each object removed from the mix is asked to finalize
. It can optionally return one or more objects to be added to the mix. We use this to split one asteroid into two, or to inject a Score
into the mix to be tallied.
No god, just objects interacting.
Refactoring
I find it useful talking about “the mix”, the collection of flyers that are interacting. And I observe that some flyers fly, and some just sit there, like the ScoreKeeper
and ShipMonitor
. Let’s do some renaming to make the code reflect the way we speak of it.
class Game {
val flyers = Flyers()
Let’s rename this member mix
. The methods of course change with the help of IDEA, like this one:
fun update(deltaTime: Double) {
val adds = mutableListOf<IFlyer>()
mix.forEach { adds.addAll(it.update(deltaTime)) }
mix.addAll(adds)
}
Do we like this word? Is it communicative enough if you don’t have me here talking about the mix? Should we perhaps call it universeContents
or allKnownObjects
or some other better term? Yes, we should rename it again, even though I do like mix
in common parlance. Let’s try allKnownObjects
. It’s local to this class but this is, after all, the “god” class4.
class Game {
val allKnownObjects = Flyers()
...
fun processInteractions() {
val toBeRemoved = colliders()
allKnownObjects.removeAll(toBeRemoved)
for (removedObject in toBeRemoved) {
val addedByFinalize = removedObject.finalize()
allKnownObjects.addAll(addedByFinalize)
}
}
...
And so on. Yes, I like that.
What about Flyers
, the class containing these objects? And the flyer interface IFlyer
and the concrete class Flyer
? I think we have some things to consider.
There are two kinds of objects in the mix (I really do like that word), two kinds of objects in the known objects … OK, rename that to knownObjects, removing the all, and speak of known objects. Done. Test. Commit: rename flyers in Game to knownObjects.
We have two kinds of “known objects”, the ones that move and collide and such, which are all instances of the single class Flyer … and the other known objects which I have been calling “special”. I’ve been troubled by the breadth of the interface for these objects, though it now has enough defaults so as to be mostly OK. Here’s that interface:
interface IFlyer {
val position: Point
// default position is off-screen
get() = Point(-666.0, -666.0)
val velocity
// something magical about this number
get() = Velocity(0.0, 100.0)
val killRadius: Double
// no one can hit me
get() = -Double.MAX_VALUE
val mutuallyInvulnerable: Boolean
// specials and asteroids are safe from each other
get() = true
// fake values for interactions
val elapsedTime
get() = 0.0
val lifetime
get() = Double.MAX_VALUE
val score: Int
get() = 0
fun draw(drawer: Drawer) {}
fun interactWith(other: IFlyer): List<IFlyer>
fun interactWithOther(other: IFlyer): List<IFlyer>
fun move(deltaTime: Double) {}
fun finalize(): List<IFlyer> { return emptyList() }
fun update(deltaTime: Double): List<IFlyer>
fun deathDueToCollision(): Boolean { return true }
}
The comments alone suggest that this code wants to be made better. The fact that there are no fewer than seven member variables, all defaulted, is odd.
And let’s at least organize the functions into those with defaults and those without:
fun interactWith(other: IFlyer): List<IFlyer>
fun interactWithOther(other: IFlyer): List<IFlyer>
fun update(deltaTime: Double): List<IFlyer>
fun deathDueToCollision(): Boolean { return true }
fun draw(drawer: Drawer) {}
fun finalize(): List<IFlyer> { return emptyList() }
fun move(deltaTime: Double) {}
Commit: tidying.
Let’s try to think a bit more clearly about this. There are certainly two separate kinds of known objects, the instances of Flyer, and the other “special” ones. In Flyer, the core method interactWith
is this:
override fun interactWith(other: IFlyer): List<IFlyer> {
return other.interactWithOther(this)
}
If other
is another Flyer, no harm done, we wind up in the Flyer method:
override fun interactWithOther(other: IFlyer): List<IFlyer> {
return when {
weAreCollidingWith(other) -> listOf(this, other)
else -> emptyList()
}
}
However, if other
is not a Flyer, but “special”, we are sent to the specialized interaction code in that class, which is what allows all the special flyers to interact in their own unique and special way. And, if we happen to have the special first, each special implements interactWith
, again to do its special kind of interaction.
Now let’s think about this a bit. When, in Flyer, we arrive at interactWithOther … we know with certainty that this
is a flyer … and that other
is a flyer as well, since otherwise we wouldn’t be here. But Kotlin doesn’t know this, and I’d be afraid of it if it did. I don’t like sentient machines.
What about methods like this one:
interface IFlyer ...
fun deathDueToCollision(): Boolean { return true }
That method is never called other than on a Flyer. It’s never called on a special. How do I know? I can look at the senders and see that they don’t use it. Can I remove it from the interface and remove override from the function in Flyer? Yes I can.
OK, one down, six to go. Test, commit: remove deathDueToCollision from IFlyer.
Can I remove finalize
the same way?
Not easily, because of this:
class Game ...
fun processInteractions() {
val toBeRemoved = colliders()
knownObjects.removeAll(toBeRemoved)
for (removedObject in toBeRemoved) {
val addedByFinalize = removedObject.finalize()
knownObjects.addAll(addedByFinalize)
}
}
I think it’s true that only Flyers are returned from colliders
, so the list types could be changed throughout … no. I can think of an exception: the Score. When the ScoreKeeper sees the Score, it returns the Score to be removed and finalized.
Let’s at least do this. We’ll rename the interface from IFlyer to something connoting that the implementor is a known object, an object in space, a space object, an active participant in the give and take of the universe, an entity (I’m trying lots of words here, hoping to get a good name), a ThingInSpace … but not necessarily a flyer, a moving object, a visible entity, a physical object, a potential space hazard … I’m going to rename the interface to ISpaceObject. I’m tempted to drop the I, but it seems a decent convention. Tests green, commit: rename IFlyer to ISpaceObject.
Now what about renaming Flyer to PhysicalObject? SolidObject? I think I like SolidObject.
I rename Flyers (the collection) to SpaceObjectCollection. Tests and files renamed. Better test and commit: Rename Flyers to SpaceObjectCollection, Flyer to SolidObject.o not know. I’ll push a meaningless commit.
Can we simplify anything else here?
Let’s at least break out some files. I’ll extract the interface to its own file. ISpaceObject. Now let’s move the typealias, constants, and functions off to their own file, Universe. Test, commit: move ISpaceObject to its own file. Move the typealiases, U and cap functions to Universe.kt.
Should probably have done that with separate commits.
Some Simplification
Now I’d like to look at the member vars on ISpaceObject, position and so on, to see how they are used.
Easiest way to do it, I think, is to remove them from the interface, and see what turns up in the compile errors. We’ll surely have to remove some overrides, but if there are implementations outside of SolidObject I’ll be concerned.
I’ll start with velocity
as I think it’s simpler. Only one test fails, this one:
@Test
fun `new split asteroids get new directions`() {
val startingV = Vector2(100.0,0.0)
val full = SolidObject.asteroid(
pos = Vector2.ZERO,
vel = startingV
)
val fullV = full.velocity
assertThat(fullV.length).isEqualTo(100.0, within(1.0))
assertThat(fullV).isEqualTo(startingV)
val halfSize = full.finalize()
// halfSize.forEach {
// val halfV = it.velocity
// assertThat(halfV.length).isEqualTo(100.0, within(1.0))
// assertThat(halfV).isNotEqualTo(startingV)
// }
}
The commented-out bit fails. If I can cast the ISpaceObjects in halfSize
to SolidObject …
@Test
fun `new split asteroids get new directions`() {
val startingV = Vector2(100.0,0.0)
val full = SolidObject.asteroid(
pos = Vector2.ZERO,
vel = startingV
)
val fullV = full.velocity
assertThat(fullV.length).isEqualTo(100.0, within(1.0))
assertThat(fullV).isEqualTo(startingV)
val halfSize = full.finalize()
var countSplits = 0
halfSize.forEach {
if ( it is SolidObject) {
countSplits += 1
val halfV = it.velocity
assertThat(halfV.length).isEqualTo(100.0, within(1.0))
assertThat(halfV).isNotEqualTo(startingV)
}
}
assertThat(countSplits).describedAs("always two there are").isEqualTo(2)
}
There are three objects in the halfSize
, two split asteroids and a Score. I suspect that weird default in velocity
was there to make this test pass. Now we skip any non solids and count the solids. Test is green. Commit: velocity member removed from ISpaceObject.
Let’s try removing mutuallyInvulnerable
. I see a test that may need changing.
This errors:
private fun weCanCollideWith(other: ISpaceObject) =
!this.mutuallyInvulnerable || !other.mutuallyInvulnerable
We can change this to check for SolidObject, which I kind of hate to do, or to cast it, which I also kind of hate. But narrowing this interface, I think, is better than overriding things mysteriously.
I refactor to this:
private fun weCanCollideWith(other: ISpaceObject) {
return !(this.mutuallyInvulnerable
&& other.mutuallyInvulnerable)
}
This, too, will not compile, but …
private fun weCanCollideWith(other: ISpaceObject): Boolean {
return if ( other !is SolidObject) false
else !(this.mutuallyInvulnerable && other.mutuallyInvulnerable)
}
This should pass the compile test … and maybe the actual tests. I have to remove the overridden val from ShipMonitor. Tests go green. Game works. Commit: remove mutuallyInvulnerable
from ISpaceObject interface.
I am pleased by these changes. Each one is a small step toward a simpler system. And my tests are bearing pretty good weight in giving me confidence in the changes, though I admit I did play the game this last time.
What else? I remove killRadius, change the override in SolidObject. Get this error compiling:
private fun weAreInRange(other: ISpaceObject) =
position.distanceTo(other.position) < killRadius + other.killRadius
Here again, we can do as we did before:
Had to add casts in this test:
@Test
fun `asteroid splits on finalize`() {
val full = SolidObject.asteroid(
pos = Point.ZERO,
vel = Velocity.ZERO
)
val radius = full.killRadius
val halfSize= full.finalize()
assertThat(halfSize.size).isEqualTo(3) // two asteroids and a score
val half = halfSize.last()
assertThat((half as SolidObject).killRadius).describedAs("half").isEqualTo(radius/2.0)
val quarterSize = half.finalize()
assertThat(quarterSize.size).isEqualTo(3)
val quarter = quarterSize.last()
assertThat((quarter as SolidObject).killRadius).describedAs("quarter").isEqualTo(radius/4.0)
val eighthSize = quarter.finalize()
assertThat(eighthSize.size).describedAs("should not split third time").isEqualTo(1)
}
I’m OK with that. Tests are green. Commit: remove killRadius from ISpaceObject.
What about position? I’ve been holding off on that one as possibly difficult. I just have to cast as SolidObject
in tests, which will error anyway if it isn’t, and change this one method to be like others:
private fun tooClose(other:ISpaceObject): Boolean {
return if (other !is SolidObject) false
else (ship.position.distanceTo(other.position) < U.SAFE_SHIP_DISTANCE)
}
We are green. Commit: remove position from ISpaceObject!
This is good. Let’s relax a bit and look at what we have wrought. Here’s ISpaceObject interface as trimmed down.
Reflection
interface ISpaceObject {
// fake values for interactions
val elapsedTime
get() = 0.0
val lifetime
get() = Double.MAX_VALUE
val score: Int
get() = 0
fun interactWith(other: ISpaceObject): List<ISpaceObject>
fun interactWithOther(other: ISpaceObject): List<ISpaceObject>
fun update(deltaTime: Double): List<ISpaceObject>
fun draw(drawer: Drawer) {}
fun finalize(): List<ISpaceObject> { return emptyList() }
fun move(deltaTime: Double) {}
}
We used to have seven member variables and now we’re down to three. That’s good. Those members, every one of them, remained in SolidObject, and are used there. There are four uses of is
or ~is
in the running code now. Let’s review them:
class SolidObject ...
private fun weCanCollideWith(other: ISpaceObject): Boolean {
return if ( other !is SolidObject) false
else !(this.mutuallyInvulnerable && other.mutuallyInvulnerable)
}
private fun weAreInRange(other: ISpaceObject): Boolean {
return if ( other !is SolidObject) false
else position.distanceTo(other.position) < killRadius + other.killRadius
}
class ShipMonitor ...
private fun tooClose(other:ISpaceObject): Boolean {
return if (other !is SolidObject) false
else (ship.position.distanceTo(other.position) < U.SAFE_SHIP_DISTANCE)
}
class AsteroidTest ...
halfSize.forEach {
if ( it is SolidObject) {
countSplits += 1
val halfV = it.velocity
assertThat(halfV.length).isEqualTo(100.0, within(1.0))
assertThat(halfV).isNotEqualTo(startingV)
}
}
Now let’s be clear: whenever we check an object to see what type it is, we have a clear sign that the object aren’t quite right: the objects and methods are best when they are designed so that no one can ever call a method with an object of the wrong type. So far, I’ve not managed to see my way through Kotlin’s type checking to make this possible. I am quite certain that the code will never send a non-Solid to those methods in SolidObject.
Now we used to finesse this issue by providing fake values for the non-Solid objects, so that their position was out of the normal range or their kill radius was negative infinity. This worked, allowed us not to check types, but it was obscure (in my view). It wasn’t clear why those defaults had to be there, and we had four members implemented with overrides in some classes and not others, which raised the question WHY??? in the minds of our readers, even me.
So, while I don’t like the type checking, I prefer it to the added complexity of all those magical values.
Shall we try to see if those other vals can be removed? How about elapsedTime
?
The ShipMonitor wants elapsed time but doesn’t hand it out to anyone else, so it need not be in the interface as far as it is concerned.
The SolidObject uses elapsed time in the case of the missile. It’s always updated, in every SolidObject, and checked in the LifetimeClock:
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
return if (other.elapsedTime > other.lifetime) {
listOf(other)
} else
emptyList()
}
The lifetimeClock checks all objects to see if they should time out:
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
return if (other.elapsedTime > other.lifetime) {
listOf(other)
} else
emptyList()
}
Here again it could just check SolidObjects and the others could do without elapsed Time.
I am tempted to add a method isSolid
or isPhysical
, to avoid the type checks. We’ll take that under advisement.
Anyway, elapsedTime
and lifetime
kind of go together, so let’s defer them for now. What about score? We know that no one has a score except for a Score object. But there again, we’d have to check the type using is
. Let’s not: the override covers every object except for Score
itself.
I think we’ll wrap the changes here. Let’s sum up.
Summary
We did some renaming of our objects, intending that they better express what they are. ISpaceObject
is our interface. SolidObject
is all the obvious real objects, asteroid, missile, ship (with more to come). The Flyers collection is renamed to SpaceObjectCollection and the variable in Game is called knownObject
.
We removed four required member variables from our ISpaceObject interface, position
, killRadius
, mutuallyInvulnerable
, and velocity
. That’s over half the required members gone now. A good thing.
The member functions remaining seem reasonable, but the member variables still look sus.
interface ISpaceObject {
// fake values for interactions
val elapsedTime
get() = 0.0
val lifetime
get() = Double.MAX_VALUE
val score: Int
get() = 0
fun interactWith(other: ISpaceObject): List<ISpaceObject>
fun interactWithOther(other: ISpaceObject): List<ISpaceObject>
fun update(deltaTime: Double): List<ISpaceObject>
fun draw(drawer: Drawer) {}
fun finalize(): List<ISpaceObject> { return emptyList() }
fun move(deltaTime: Double) {}
}
I think I want a comment in there:
interface ISpaceObject {
// fake values for interactions
val elapsedTime
get() = 0.0
val lifetime
get() = Double.MAX_VALUE
val score: Int
get() = 0
fun interactWith(other: ISpaceObject): List<ISpaceObject>
fun interactWithOther(other: ISpaceObject): List<ISpaceObject>
fun update(deltaTime: Double): List<ISpaceObject>
// defaulted, sometimes overridden
fun draw(drawer: Drawer) {}
fun finalize(): List<ISpaceObject> { return emptyList() }
fun move(deltaTime: Double) {}
}
Commit: added comment to indicate that the code needs to be more clear.
Overall, I think this is better. I’d like to sort out whether there’s a reasonable way to let the type system help us avoid those few calls to is
or as
. Those are definitely a code smell.
I’m tired. Been sitting here 5 1/2 hours. Long session but productive. See you next time!