Kotlin 232 - Strategy?
GitHub Decentralized Repo
GitHub Centralized Repo
Delegation using Kotlin’s
by
seems not to be quite the thing, though I believe I could make it work. I think that late yesterday I invented the Strategy pattern. Let’s find out.
The Thinking
No, but seriously, I make no claim to having invented anything, not XP, possibly not story points1, and certainly not the Strategy pattern. However, in thinking about how to off-load the collision detection from the space objects, I had an idea for how to do it and then realized that what I was thinking of doing was very likely the Strategy pattern. I first saw Strategy described in Design Patterns, by Gamma, Helm, Johnson, and Vlissides, way last century.
I won’t try to give a formal description of Strategy here, just to implement an idea which I think is best described as that pattern. Now, the Strategy pattern actually allows an object’s behavior to vary at run time, which we’ll not be doing … so perhaps we’re doing something a bit less than Strategy. Not important.
What I want to accomplish is to move our space objects’ specific collision-handling behavior out of the main object and into another of a set of objects, each of which provides the collision logic for one or more of the space objects. What I propose to do is conceptually simple, and mysteriously similar to the Subscriptions we used in the decentralized version.
The Idea
The idea goes like this: Each space object implements interactWith
, and they all do exactly the same thing:
override fun interactWith(other: Collider, trans: Transaction)
= other.interact(this, trans)
This is just a “double dispatch”, which has the effect of introducing the type of this
to other
, so that the implementations of interact
in each space object are completely type-specific and can therefore adjust their behavior based on what is colliding with them.
For our first change, we will modify the implementations of interactWith
to refer to a strategy in the other
and send the message to it. We’ll begin with the strategy simply being the other
object, as it is now. This will have no behavioral effect. Then, we’ll provide a different strategy for some object and move behavior over to it.
Let’s just try it. I think it’ll be more clear.
The Foundation
We’ll need to add strategy
to the Collider interface. I do think we’ll wind up dividing up this interface as we go forward, but for now, that should drive out what we need.
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)
fun interactWith(other: Collider, trans: Transaction)
val strategy: Collider
}
OK, this is already weird, but until I understand better what I’m doing I won’t be sure how to divide this up. I think we’ll wind up with one interface for just strategy
and interactWith
, and one for the rest. For now, this will work. Probably. (It turns out: “Not quite”.)
Now I’ll have to implement that val in each of my Colliders. The compiler will lead me.
class Asteroid(
override var position: Point,
val velocity: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
override val killRadius: Double = U.ASTEROID_KILL_RADIUS,
private val splitCount: Int = 2
) : SpaceObject, Collider {
override val strategy: Collider
get() = this
I think I just do this in each class and we should be good to go.
Yes! I love when a plan comes together! Green. Commit: strategy implemented as this
.
Now we’ll change all the implementors of interactWith
to use strategy. This should be benign as well. Throughout:
override fun interactWith(other: Collider, trans: Transaction)
= other.strategy.interact(this, trans)
Green. Commit: All interactWith
invoke strategy
.
I think we should rename this to collisionStrategy
, don’t you? Done. Green. Commit: rename strategy
to collisionStrategy
.
A CollisionStrategy
The foundation is in place. There’s nothing for it but to do it. Which one would be easiest? It’s pretty clearly Ship:
class Ship ...
override fun interact(asteroid: Asteroid, trans: Transaction) = checkCollision(asteroid, trans)
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) { }
override fun interactWith(other: Collider, trans: Transaction)
= other.collisionStrategy.interact(this, trans)
private fun checkCollision(other: Collider, trans: Transaction) {
Collision(other).executeOnHit(this) {
trans.add(Splat(this))
trans.remove(this)
}
}
OK, before I do this, I’m tempted to do the splitting of the Collider interface. Why? Because even if I do move this over to a ShipCollisionStrategy, I’m going to have to leave all four of the interact
methods here. But I am impatient, and I’m not dead certain just how to divide things up, so I’ll go ahead and let the code participate in the decision.
I want a new class, ShipCollisionStrategy. It goes like this:
class ShipCollisionStrategy(val ship: Ship): Collider {
override val position: Point
get() = ship.position
override val killRadius: Double
get() = ship.killRadius
override val collisionStrategy: Collider
get() = this
override fun interact(asteroid: Asteroid, trans: Transaction) = checkCollision(asteroid, trans)
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) { }
override fun interactWith(other: Collider, trans: Transaction) {}
private fun checkCollision(other: Collider, trans: Transaction) {
Collision(other).executeOnHit(ship) {
trans.add(Splat(ship))
trans.remove(ship)
}
}
}
Note that I require my client, the ship, to be provided and I defer access to the position
and killRadius
to it. The collisionStrategy
and interactWith
don’t belong here, but we’ll sort that in a moment.
Now, back in Ship, I’ll change its definition of strategy
and decommission its interact
implementations, just to be sure.
override val collisionStrategy: Collider
get() = ShipCollisionStrategy(this)
override fun interact(asteroid: Asteroid, trans: Transaction) {}
override fun interact(missile: Missile, trans: Transaction) {}
override fun interact(saucer: Saucer, trans: Transaction) {}
override fun interact(ship: Ship, trans: Transaction) {}
I expect this to work. It doesn’t, quite. I am surprised. I was so sure this was simple. Let’s see what happened:
@Test
fun `ship asteroid collision`() {
val asteroid = Asteroid(Point.ZERO)
val ship = Ship(Point.ZERO)
ship.position = asteroid.position
val trans = Transaction()
ship.interact(asteroid, trans)
assertThat(trans.removes).contains(ship)
asteroid.interact(ship, trans)
assertThat(trans.removes).contains(asteroid)
}
Ha! Can’t call interact
on ship. Got to call through the strategy.
@Test
fun `ship asteroid collision`() {
val asteroid = Asteroid(Point.ZERO)
val ship = Ship(Point.ZERO)
ship.position = asteroid.position
val trans = Transaction()
ship.collisionStrategy.interact(asteroid, trans)
assertThat(trans.removes).contains(ship)
asteroid.interact(ship, trans)
assertThat(trans.removes).contains(asteroid)
}
Let’s see if this runs. It does. Whew. What else? Same thing, three more times. Green. Commit: ship now uses ShipCollisionStrategy for collisions. According to warnings, I remove the checkCollision
from Ship as no longer used.
Now, before we conclude that we’re done and move on, let’s change the interfaces to be what we need. We have this:
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)
fun interactWith(other: Collider, trans: Transaction)
val collisionStrategy: Collider
}
We need one containing interactWith
and collisionStrategy
, and one containing the four interact
methods plus position
, and killRadius
.
More difficult, we need two names. Let’s call the small one Collidable and the rest remains Collider. I’m going to put these in the same file for now.
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 Collidable {
fun interactWith(other: Collidable, trans: Transaction)
val collisionStrategy: Collider
}
Now I need to change my Ship to be just a Collidable. I think the other three will have to do both for now. ShipCollisionStrategy is already a Collider.
As soon as I set Ship to Collidable instead of Collider, I am required to delete all this:
override fun interact(asteroid: Asteroid, trans: Transaction) {}
override fun interact(missile: Missile, trans: Transaction) {}
override fun interact(saucer: Saucer, trans: Transaction) {}
override fun interact(ship: Ship, trans: Transaction) {}
Let’s try to compile and let IDEA lead us to the other changes.
override fun interactWith(other: Collidable, trans: Transaction)
= other.collisionStrategy.interact(this, trans)
This has to change to refer to Collidable throughout. The Ship’s position
and killRadius
no longer get to be overrides. Is this going to be OK with the strategy? Yes, it knows it has a Ship.
Uh oh. I’m having an issue with position and killRadius in the Collision object. It wants to take a Collidable and they aren’t required to have position and killRadius. Can I require them in both interfaces?
That seems to work, but I have another issue, which is that I have to trek through changing a lot of things to be Collidable instead of Collider.
After a lot of renaming of parameters to Collidable, I am green. I think I’d have done better to have adjusted those interfaces in some other way. I am unfamiliar enough with using them that I don’t have good instincts about them.
But we are green. The game works. Commit: split Collider
interface into Collider
and Collidable
. position
and killRadius
in each. Temporary?
Summary
Let’s have a look around and sum up. I think one strategy will be enough to fill out the article.
Here are the two interfaces:
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 Collidable {
val position: Point
val killRadius: Double
fun interactWith(other: Collidable, trans: Transaction)
val collisionStrategy: Collider
}
In the fullness of time, space objects will be Collidable and will have a Collider (strategy). I’m mot sure if both sides will have to have position
and killRadius
or not. I think they might. Separate interface?
Here’s all Ship knows about colliding now:
class Ship(
override var position: Point,
val controls: Controls = Controls(),
override val killRadius: Double = U.SHIP_KILL_RADIUS
) : SpaceObject, Collidable {
var velocity: Velocity = Velocity.ZERO
var heading: Double = 0.0
private var dropScale = U.DROP_SCALE
var accelerating: Boolean = false
var displayAcceleration: Int = 0
override val collisionStrategy: Collider
get() = ShipCollisionStrategy(this)
override fun interactWith(other: Collidable, trans: Transaction)
= other.collisionStrategy.interact(this, trans)
And here’s its strategy:
class ShipCollisionStrategy(val ship: Ship): Collider {
override val position: Point
get() = ship.position
override val killRadius: Double
get() = ship.killRadius
override fun interact(asteroid: Asteroid, trans: Transaction) = checkCollision(asteroid, trans)
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) { }
private fun checkCollision(other: Collidable, trans: Transaction) {
Collision(other).executeOnHit(ship) {
trans.add(Splat(ship))
trans.remove(ship)
}
}
}
We have rather cleanly split Ship’s responsibility to collide off from its more ship-like behavior, like flying about, shooting missiles, entering hyperspace, and so on. Ship is more cohesive, and certainly the ShipCollisionStrategy is very cohesive, dealing only with colliding behavior.
This is good, I think.
There was one ragged bit in changing various objects and methods to agree on whether they were talking about Collidable or Collider. There may have been a better way to do that, and in a larger program I think I’d have had to roll back part way and do it more carefully. My skill with interfaces, especially the refactoring of them, is what you could2 call “limited”.
Even if all we do is extract four unique collision strategies from our four Collidable objects, that will increase the cohesion of our design and will nicely isolate collision behavior from other more asteroidal or missiley (sic) or saucerial (sic) behavior. And it’s possible, just possible, that we’ll find some duplication among the strategies that we can exploit. We’ll see about that.
Join me next time, please!