Kotlin 149: Indirection
The Fundamental Theorem of Software Engineering is ‘We can solve any problem by introducing an extra level of indirection’.
Wikipedia tells us that this theorem was originated by Andrew Koenig, describing a remark by Butler Lampson, attributed to David J. Wheeler. You can read the brief article for a bit more information. My point here is that I plan to apply the theorem to resolve Hill’s friendly objection to the implementation inheritance in my SpaceObject hierarchy.
In my ongoing Slack conversation with Hill, some ideas have arisen, some due to ideas he has had, like passing a Transaction in to methods rather than having them create and return one, and ideas that I’ve had, trying to explain why some of the things he wants to do are breaking my fundamental design idea and — while perhaps it deserves to be broken — I don’t want to abandon that notion.
What is that notion?
There is no God object maintaining details of the global state. The global state of the game is contained entirely in the sum of the states of all the objects in it. The Game object has no concept of the types of the objects in the mix, and everything that occurs happens in interactions between two objects, or in generic messages like
update
sent to individual objects. The Game has no idea what you’re going to do inupdate
. The rules of the universe are that you’ll receive these messages, in this order, when your turn in the mix comes up. Everything else is down to the objects in the mix.
So. The basic concern is that, in order to make this work, and to avoid duplication of code, I have put concrete methods in the superclass of all the game objects. This is objected to, on principle, by many authors. Other authors perceive inheritance as simply a programmer’s hack to reduce duplication and as such expect exactly what’s going on here. Still, inheritance of implementation can lead to trouble, and sometimes to seeming contradictions, and it can certainly make things harder to understand. So I’m accepting the task of getting rid of my implementation inheritance. Better yet, I have an idea on how to do it.
Last night, I drew this picture, to share with Hill and, well, just because I wanted to:
I put the methods that the Game calls in that box toward the top right. Somehow, between that and the Slack Slings and Arrows I was wielding with Hill, I got the notion that these didn’t have to be methods that were called, they could be events to which individual objects could subscribe if they wanted to be given opportunities to run at those times.
At that point I went to bed and put myself to sleep thinking about that idea, perhaps dreaming about it, and thinking about it again as I woke up. And I’m not going to do events per se, but instead a table of functions.
Finally, an Actual Idea
The new scheme will be that each SpaceObject will return a map of function names, like UPDATE
, to actual functions in the object. So the Game, when it wants to give an object its change to update, will do something like this:
object.dispatch[UPDATE](deltaTime, transaction)
Fetch its dispatch map, fetch the function desired, call it with the agreed parameters. If a given object doesn’t want to be called on a given “event”, it simply has its dispatch return an empty function, {}. I think a dispatch will look like this. Here, hold on, I’ll TDD it up. I’ll use the TestInteraction object from yesterday, scrapping out its insides and starting anew:
class TestInteraction {
@Test
fun `NewThing test`() {
val thing = NewThingOne()
val t = Transaction()
thing.dispatch[Events.UPDATE](0.05)
thing.dispatch[Events.BEGIN_INTERACTION](t)
thing.dispatch[Events.INTERACT_WITH](t)
thing.dispatch[Events.END_INTERACTION](t)
assertThat(t.elapsedTime).isEqualTo(0.05)
assertThat(t.adds.size).isEqualTo(1)
assertThat(t.removes.size).isEqualTo(1)
}
}
Here, I have some imaginary object in mind that is going to receive some of those calls and somewhere along the way add something and remove something.
Let’s write the class … after some fumbling, changing the calls and such, I get this to run:
class TestInteraction {
@Test
fun `NewThing test`() {
val thing = NewThingOne()
val transaction = Transaction()
val dt = 0.05
// boilerplate calls inside cycle
thing.events[Events.UPDATE]?.invoke(0.05, transaction)
thing.events[Events.BEGIN_INTERACTION]?.invoke(dt,transaction)
thing.events[Events.INTERACT_WITH]?.invoke(dt, transaction)
thing.events[Events.END_INTERACTION]?.invoke(dt, transaction)
assertThat(thing.elapsedTime).isEqualTo(0.05)
assertThat(transaction.adds.size).isEqualTo(1)
assertThat(transaction.removes.size).isEqualTo(1)
}
}
enum class Events {
UPDATE, BEGIN_INTERACTION, INTERACT_WITH, END_INTERACTION
}
interface EventHandler {
val events: Map<Events, (Double, Transaction)->Unit>
}
class NewThingOne: EventHandler {
override val events : Map<Events, (Double, Transaction)->Unit> = mapOf(
Events.UPDATE to { dt: Double,t:Transaction -> update(dt, t) },
Events.END_INTERACTION to { dt: Double,t:Transaction -> endInteraction(dt, t) },
)
var elapsedTime = 0.0
private fun update(deltaTime: Double, trans: Transaction) {
elapsedTime += deltaTime
trans.add(Score(20))
}
private fun endInteraction(deltaTime: Double, t: Transaction) {
t.remove(Score(30))
}
}
This test runs, so we can consider this to be a successful proof of concept. We have an object NewThingOne
that only wants to implement UPDATE
and END_INTERACTION
. It provides a val events
that maps those two enum entries to functions calling its own private methods update
and endTransaction
.
I had to make all the invoked functions accept the same arguments to convince Kotlin to compile. There may be a clever way to allow the various functions to have different arguments, but passing in a double to all of them isn’t going to kill me, since it should allow me to unwind all that inheritance.
This form is not attractive:
thing.events[Events.BEGIN_INTERACTION]?.invoke(dt,transaction)
That ?.invoke
is there because Kotlin doesn’t know that the map is complete with all elements of the num
… and in fact it is not complete. So if it doesn’t provide anything to call, we skip the invocation. I am sure we can cover this nastiness with a function, and it’s possible that we can convince Kotlin that the map is complete. Those niceties, while valuable, were too far for me to reach in one go. Worst case, this only happens in one place, the Game’s cycle
method.
I can’t be entirely certain until I’m done but I am rather sure that this approach will let me remove all the methods like update
and beginInteraction
from the SpaceObject interface (and probably turn it back into an actual interface), with little difficulty. It’s just that each implementor of SpaceObject will have a single val
to override, its events map.
We’ll see about smoothing out the rough spots here. Perhaps my more Kotlin-experienced pals will be able to help me out. But this is going to simplify things a lot, I’m sure of it.
See you next time!