Kotlin 233 - More Strategy
GitHub Decentralized Repo
GitHub Centralized Repo
We’ll try another one of our CollisionStrategy things. I was hoping for some reuse but am not clear yet how to get it, nor whether its really worth it. I think we’ll complete all four Strategies and then look around.
Let’s just pick another class to move. Which one looks easiest?
Looks like Asteroid should be pretty straightforward:
override val collisionStrategy: Collider
get() = this
override fun interactWith(other: Collidable, trans: Transaction)
= other.collisionStrategy.interact(this, trans)
override fun interact(asteroid: Asteroid, trans: Transaction) {}
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: Collidable, trans: Transaction) {
Collision(this).executeOnHit(other) {
dieDueToCollision(trans)
}
}
fun dieDueToCollision(trans: Transaction) {
trans.remove(this)
trans.add(Splat(this))
splitIfPossible(trans)
}
Seems we can just make another Strategy and refer to it. It looks like this:
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) {}
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: Collidable, trans: Transaction) {
Collision(asteroid).executeOnHit(other) {
dieDueToCollision(trans)
}
}
fun dieDueToCollision(trans: Transaction) {
trans.remove(asteroid)
trans.add(Splat(asteroid))
splitIfPossible(trans)
}
private fun splitIfPossible(trans: Transaction) {
if (asteroid.splitCount >= 1) {
trans.add(asSplit(asteroid))
trans.add(asSplit(asteroid))
}
}
private fun asSplit(asteroid: Asteroid): Asteroid =
Asteroid(
position = asteroid.position,
killRadius = asteroid.killRadius / 2.0,
splitCount = asteroid.splitCount - 1
)
}
I have a raft of tests that call dieDueToCollision
, which can’t be called on the strategy because it isn’t in the interface. nor do I want it there. I comment those out and add a few indirections through collisionStrategy
and am green, with three removed tests. I want to see if the game works … and it works just fine.
Now how to revive those tests? Let’s inspect one.
@Test
fun `asteroid dieOnCollision`() {
val asteroid = Asteroid(Point.ZERO)
val trans = Transaction()
asteroid.dieDueToCollision(trans)
val splits = trans.asteroids()
assertThat(splits.size).isEqualTo(2)
}
I guess we could create something for the asteroid to hit and call that. I’ll try it. Ah, easy:
@Test
fun `asteroid dieOnCollision`() {
val asteroid = Asteroid(Point.ZERO)
val ship = Ship(Point.ZERO)
val trans = Transaction()
asteroid.collisionStrategy.interact(ship, trans)
val splits = trans.asteroids()
assertThat(splits.size).isEqualTo(2)
}
That runs green. Easy enough to repair the others similarly. Yes and we are green. Commit: Asteroid now collides using AsteroidCollisionStrategy.
Summary
I think we’ll stop here, this is just an afternoon dalliance, but let’s compare the two strategies and speculate about how to deal with any duplication.
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)
}
}
}
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) {}
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: Collidable, trans: Transaction) {
Collision(asteroid).executeOnHit(other) {
dieDueToCollision(trans)
}
}
fun dieDueToCollision(trans: Transaction) {
trans.remove(asteroid)
trans.add(Splat(asteroid))
splitIfPossible(trans)
}
private fun splitIfPossible(trans: Transaction) {
if (asteroid.splitCount >= 1) {
trans.add(asSplit(asteroid))
trans.add(asSplit(asteroid))
}
}
private fun asSplit(asteroid: Asteroid): Asteroid =
Asteroid(
position = asteroid.position,
killRadius = asteroid.killRadius / 2.0,
splitCount = asteroid.splitCount - 1
)
}
Obviously they’re the same down to checkCollision
.
We could imagine, perhaps, a standard method dieDuetoCOllision
as part of the Collidable interface, and each object implements it uniquely. Or, perhaps, we could provide a code block to the strategy, to be executed inside the checkCollision
. Or, I suppose there could be yet another Strategy, a What To Do When Colliding Strategy.
However, it’s easy to miss that each of the four classes may want to fill in a particular one of the interact
methods differently. Here, asteroid v asteroid is empty in Asteroid, and ship v ship is empty in Ship. I’m not sure if we’ll be able to roll these together or not. Possibly not. Even so, we are gaining cohesion but it would be nice to save some code as well.
We’ll wait to see what it looks like with all four of them. So far it has been pretty easy and is at least improving cohesion. Certainly an interesting kind of program transformation.
See you next time!