Kotlin 240 - I Blame You People
GitHub Decentralized Repo
GitHub Centralized Repo
I think there’s a simplification to be had, and none of you people suggested it to me. TAP TAP. Is this thing on?
In order to simplify my space objects, and to make them more cohesive, I’ve arranged for them to forward the interaction messages to Strategy objects. I’ve done this with explicit forwarding. They all implement a collisionStrategy
method and forward to a type-specific interact
method:
class Asteroid ...
override fun interactWith(other: SpaceObject, trans: Transaction)
= other.collisionStrategy.interact(this, trans)
They also all implement the val collisionStrategy
:
class Asteroid ...
override val collisionStrategy: Collider
get() = AsteroidCollisionStrategy(this)
An alternative implementation, often used, might have been to have the objects all implement the various interact
methods and do the forwarding there:
class Asteroid // not the way we do it
override fun interact(missile: Missile, trans: Transaction)
= collisionStrategy.interact(missile, trans)
That scheme would have the advantage that objects could optionally forward the messages, or implement the messages themselves, with no one being the wiser. My current scheme requires everyone to provide a forwarding object, which could be this
, and we always forward the message (perhaps to the same object).
That’s a bit of coupling between the objects, in that they all have to implement another item, collisionStrategy
, but it saves implementing the same trivial functions everywhere.
But here’s where you come into the picture. You didn’t remind me that there is a better way. Kotlin supports delegation in a particularly nice way. I have some tests that I wrote to help me understand it. They’re a bit weird, because I wanted to be sure that I understood, so let’s go through them bit by bit.
interface DoSomething {
fun doingIt(input: Int): Int
fun doingOtherThing(input: Int) :Int
fun register(client: DoerClient)
}
interface DoerClient {
fun provideNumber(): Int
}
We have here two interfaces. The first one, DoSomething
, is the one that we are going to do by delegation in our user classes below. The second interface allows various users of DoSomething
to modify its behavior.
class BasicDoer(): DoSomething {
private var client: DoerClient? = null
override fun doingIt(input: Int): Int = input + (client?.provideNumber() ?: 0)
override fun doingOtherThing(input: Int): Int = input + 7
override fun register(client: DoerClient) { this.client = client }
}
Above, we have a class BasicDoer
, that implements our DoSomething
interface. It allows you to “register” a client, which is used later in the doingIt
function, if it is provided. The calculations are just there so that we’ll have something to test.
This class is intended to be used by upcoming “real” classes, to support the DoSomething
interface. BasicDoer is analogous to our AsteroidCollisionStrategy and other Strategy objects.
class OneDoer(doer: BasicDoer = BasicDoer()): DoerClient, DoSomething by doer {
init { register(this) }
override fun provideNumber(): Int {
return 600
}
}
Here, we have a user class that needs to implement DoSomething
. Note that we have a constructor value doer
and that we say DoSomething by doer
, which means “when anyone sends me a DoSomething
message, have doer
do it.” We don’t have to write explicit forwarding methods.
Note, however, that we did override one of the DoSomething
functions, provideNumber
, and we have registered ourselves as the client.
Let’s look now at the asserts in the test that apply to this implementation:
assertThat(OneDoer().doingIt(2)).isEqualTo(602)
assertThat(OneDoer().doingOtherThing(1)).isEqualTo(8)
assertThat(OneDoer().doingOtherThing(-5)).isEqualTo(2)
Because we override provideNumber
, our call to doingIt(2)
executes this line:
override fun doingIt(input: Int): Int = input + (client?.provideNumber() ?: 0)
Since we’re registered as client, this code calls us back, gets the 600, and adds 2 to it. Result, 602, as advertised.
The other two calls are simpler, just adding 7 to the input value.
class TwoDoer: DoSomething by BasicDoer() {
override fun doingIt(input: Int): Int = 3*input
}
Here we have a simpler situation. Note that we don’t even save the BasicDoer
instance at all, nor do we register with it as client. We do, however, override the actual doingIt
function, providing our own, which we test:
assertThat(TwoDoer().doingIt(2)).isEqualTo(6)
And, of course 3 times 2 is 6, and this test also runs.
So the upshot of all this is that, given an interface, like our Collider:
interface Collider {
val position: Point
val killRadius: Double
fun interact(asteroid: Asteroid, trans: Transaction)
fun interact(missile: Missile, trans: Transaction)
fun interact(saucer: Saucer, trans: Transaction)
fun interact(ship: Ship, trans: Transaction)
}
We can create a class that implements that interface, like our AsteroidCollisionStrategy:
class AsteroidCollisionStrategy(val asteroid: Asteroid): Collider {
override val position: Point
get() = asteroid.position
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)
...
And instead of putting methods into our user class, Asteroid, to call those methods, we can simply have Asteroid declare that it implements Collider by AsteroidCollisionStrategy
and Voila! it hooks up and does the job.
I Blame You
And you, if you knew about this, you didn’t drop me a note, tweet me a tweet, or toot me a toot, reminding me that this was possible. You probably figured, quite mistakenly, that I had a plan, that I knew what I was doing, that I was probably going to fake “discovering” this feature later, and that I’d refactor it in in some smooth-devilish way, in that smooth, devilish way that I have, making everything just a bit better, all part of my cunning long-term plan of refactoring this program forever.
No, I tell a lie. I don’t really blame you. But I am slightly surprised that no one mentioned this possibility to me, except that, in truth, I think that I am writing these articles into a vast void of dark matter bereft of actual readers, just a silent expanse of nothingness, stretching outward, ever outward, light year after light year, perhaps never to be read, or perhaps to be read, long centuries from now, by some strange alien entity with seven eyes and three distinct genders that will briefly glance at my work and consign it to a remote footnote (they have nine feet, by the way) and move on to something more interesting.
Well, anyway, I’ve thought of it now, no thanks to you, but if you are actually reading this, thanks to you for that!
Let’s Do It
Now that we realize this is possible, it seems reasonable to do it. It should simplify the code a bit, and that’s always worth doing. The trick will be to do it incrementally. Imagine that what we had here wasn’t an Asteroids program with five different space objects, but a UniverseSimulationGame with planets and moons and suns and gas clouds and space ships and teleport rings and comets and masses of code supporting it all. And suddenly, some far away alien with seven eyes and three distinct genders gives us this idea, and we want to begin taking advantage of it. We can’t change everything all at once. We have to do it in very small steps.
I think we can do it one class at a time.
The Plan
A class can designate itself as the implementor of Collider, just by saying:
override val collisionStrategy: Collider
get() = this
If it does that, then it must implement Collider, and it should be able to use by
delegation to do it.
Let’s try one. We’ll do Asteroid, because I happen to have it open.
As soon as I say:
override val collisionStrategy: Collider
get() = this
IDEA tells me that I can’t do that because Asteroid isn’t Collider. It takes a bit of work, because AsteroidCollisionStrategy requires access to the asteroid and I can’t find a better way than this to provide it:
class Asteroid(
override var position: Point,
val velocity: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
private val splitCount: Int = 2,
private val strategy: AsteroidCollisionStrategy = AsteroidCollisionStrategy()
) : SpaceObject, Collider by strategy {
init {
strategy.asteroid = this
}
class AsteroidCollisionStrategy(): Collider {
lateinit var asteroid: Asteroid
This does compile and work just perfectly.
Assessment
So far, this isn’t a lot better, if it’s better at all. We haven’t reduced the code in Asteroid or AsteroidCollisionStrategy: we have increased both of them a bit. And I honestly don’t see how to get any improvement until they’re all converted. That’s a bit distressing, but the changes are easy. Let’s go ahead and then look back and see if there may have been a way to get benefit incrementally.
Carrying On
I do them all, Missile, Saucer, and Ship. Splat already implements its own Collider.
Now, we should be able to remove all references to collisionStrategy, everything everywhere all at once.
interface SpaceObject {
val position: Point
val killRadius: Double
fun interactWith(other: SpaceObject, trans: Transaction)
val collisionStrategy: Collider
fun draw(drawer: Drawer)
fun update(deltaTime: Double, trans: Transaction)
}
I’ll find all the accesses and remove them all. There are 22 of them.
Arrgh. I really wish I had committed that first bit. I’ve gone into a rat hole of unprecedented horror. Must roll back.
OK, what did I learn?
I learned that removing the forwarding object, collisionStrategy
, got me in trouble. There are a zillion references to it. Somewhere in the unwinding, I lost the thread. I’ll proceed more judiciously this time. Lesson learned again: small steps.
One More Time …
class Asteroid(
override var position: Point,
val velocity: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
private val splitCount: Int = 2,
private val strategy: AsteroidCollisionStrategy = AsteroidCollisionStrategy()
) : SpaceObject, Collider by strategy {
init { strategy.asteroid = this}
...
class AsteroidCollisionStrategy(): Collider {
lateinit var asteroid: Asteroid
...
Test. Rediscover need for this:
override val collisionStrategy: Collider
get() = this
Right. Test. Green. Commit: Asteroid implements collider by strategy.
Now Missile just the same. Green. Commit: Missile implements collider by strategy.
Now Saucer. Green. Commit: Saucer implements collider by strategy.
Now Ship. Green. Commit: Ship implements collider by strategy.
That’s better. Now all the implementors of collisionStrategy
are returning this
. I should be free to remove all the calls to it.
No, that’s not quite right. Take this example:
class Asteroid
override val collisionStrategy: Collider
get() = this
override fun interactWith(other: SpaceObject, trans: Transaction)
= other.collisionStrategy.interact(this, trans)
As we’re currently constituted, collisionStrategy
returns a Collider, and this
is a Collider only because, now, Asteroid implements Collider. So we can’t just remove that call through .collisionStrategy
.
I think we want a new rule, which is that SpaceObjects must implement all the interact methods, now that they all do so, as part of the SpaceObject interface.
Even then, though, we’ll have to go through and change the signature of collisionStrategy
. Let’s see what we can do.
We have these interfaces now:
interface SpaceObject {
val position: Point
val killRadius: Double
fun interactWith(other: SpaceObject, trans: Transaction)
val collisionStrategy: Collider
fun draw(drawer: Drawer)
fun update(deltaTime: Double, trans: Transaction)
}
interface Collider {
val position: Point
val killRadius: Double
fun interact(asteroid: Asteroid, trans: Transaction)
fun interact(missile: Missile, trans: Transaction)
fun interact(saucer: Saucer, trans: Transaction)
fun interact(ship: Ship, trans: Transaction)
}
Can we just empty Collider and move everything to SpaceObject? Not quite. But if we make Collider empty and make it inherit from SpaceObject, then it will have all the same things. Let’s try that and see what explodes.
interface SpaceObject {
val position: Point
val killRadius: Double
fun interact(asteroid: Asteroid, trans: Transaction)
fun interact(missile: Missile, trans: Transaction)
fun interact(saucer: Saucer, trans: Transaction)
fun interact(ship: Ship, trans: Transaction)
fun interactWith(other: SpaceObject, trans: Transaction)
val collisionStrategy: Collider
fun draw(drawer: Drawer)
fun update(deltaTime: Double, trans: Transaction)
}
interface Collider: SpaceObject {
}
I’ll try to compile and run the tests. This should be interesting. Ah. The strategies are Collider and we don’t want them to be SpaceObjects. Can’t do it that way. Roll back.
Where do we want to end up? We’d really just like to remove all the definitions and uses of collisionStrategy
from the system. We have implicit delegation using by
now and don’t need the explicit delegation.
Let’s try this. I’ll add the four interact
functions to SpaceObject. The two interfaces will overlap for now. That works just fine.
Now what about the calls to collisionStrategy. Here’s Asteroid again.
override fun interactWith(other: SpaceObject, trans: Transaction)
= other.collisionStrategy.interact(this, trans)
It will let me remove that now. A test fails. It’s my intermittent one. Grr. Should fix that but it never fails at a time when I’m open to looking at it. It fails because a random value isn’t quite random enough.
Commit: Asteroid no longer references collisionStrategy.
Let’s remove a few more of these. I remove the references from all the SpaceObjects, testing after each one. Commit: SpaceObjects no longer reference collisionStrategy
.
I use IDEA’s Replace In Files to replace all my .collisionStrategy
with nothing. Tests are green. Commit: No more references to collisionStrategy.
Now I should be able to remove all the definitions of that method. Can I Safe Delete from the Interface? Yes. It offers to delete the override definitions. I’ll allow it. Test. Green. Commit: Remove collisionStrategy from interface and all implementations thereof.
Assessment
Now why did I have to request the interact
methods both in SpaceObject and Collider?
If they’re not in SpaceObject, statements like this won’t compile:
class Asteroid
override fun interactWith(other: SpaceObject, trans: Transaction)
= other.interact(this, trans)
II tried making Collider inherit SpaceObject, and that got me in trouble. But can SpaceObject inherit Collider?
It seems that it can:
interface Collider {
val position: Point
val killRadius: Double
fun interact(asteroid: Asteroid, trans: Transaction)
fun interact(missile: Missile, trans: Transaction)
fun interact(saucer: Saucer, trans: Transaction)
fun interact(ship: Ship, trans: Transaction)
}
interface SpaceObject: Collider {
fun interactWith(other: SpaceObject, trans: Transaction)
fun draw(drawer: Drawer)
fun update(deltaTime: Double, trans: Transaction)
}
Tests are green. I have to try the game, this is too easy. Game works just fine. Commit: adjust interfaces Collider and SpaceObject, remove overlap, SpaceObject inherits Collider.
Let’s assess and sum up.
Summary Assessment
The good news is that we have eliminated the explicit forwarding of interaction messages through a required collisionStrategy
method. We now have most of the SpaceObjects doing interaction by delegation with Kotlin’s by
keyword, and one, Splat, that implements interaction explicitly, because it doesn’t really interact with anything. This does show that we can delegate but that we’re not required to.
The bad news is that I need the client object in the strategy, and therefore have to do that lateinit
trick. There may be a way to avoid that, which I’ll explore in a subsequent article.
For now, we’re a little bit better off, in that we’re closer to ideal Kotlin, and we have removed explicit indirection and replaced it with by
.
Stay tuned. I think we’re slowly moving to a simpler, better design. I blame you.