Kotlin 229 - Strangler Review
GitHub Decentralized Repo
GitHub Centralized Repo
Let’s see how we like the look of the new GameCycler, and review how it went. There are things to do. Could we get more objects to be immutable? That would be interesting.
Let’s start in Game to review what it knows and what it does about it. We start a new game with this code:
private fun initializeGame(controls: Controls, shipCount: Int) {
numberOfAsteroidsToCreate = U.ASTEROID_STARTING_COUNT
knownObjects.performWithTransaction { trans ->
createInitialObjects(trans,shipCount, controls)
}
}
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
scoreKeeper = ScoreKeeper(shipCount)
knownObjects.scoreKeeper = scoreKeeper
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
saucer = Saucer()
cycler = makeCycler()
cycler.cancelAllOneShots()
trans.clear()
}
fun makeCycler() = GameCycler(this, knownObjects, numberOfAsteroidsToCreate, ship, saucer)
The makeCycler()
is broken out for a mostly bad reason. Because most everything we want to test is now inside GameCycler, a lot of tests need to get a cycler to send messages to. And that seems to imply that Game needs to hold on to all those details just to pass them on. So Game has member variables that, I suspect, it doesn’t really need. Let’s check. We’ll start with scoreKeeper
:
ScoreKeeper
class Game ...
private var scoreKeeper: ScoreKeeper = ScoreKeeper(-1)
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
scoreKeeper = ScoreKeeper(shipCount)
knownObjects.scoreKeeper = scoreKeeper
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
saucer = Saucer()
cycler = makeCycler()
cycler.cancelAllOneShots()
trans.clear()
}
That seems pretty straightforward. Remove the member and inline:
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
saucer = Saucer()
cycler = makeCycler()
cycler.cancelAllOneShots()
trans.clear()
}
Test. Green. Commit: Game no longer holds on to scoreKeeper.
That was easy. What about the ship and saucer? Here we’ll have a problem, because making the cycler requires them and we have been wanting to make the cycler from elsewhere.
Shift from Ship to Controls
We have considered changing things so that we don’t have to save the ship and saucer, creating new ones as needed. Ship need Controls. Here’s its creation:
class Ship(
override var position: Point,
val controls: Controls = Controls(),
override val killRadius: Double = U.SHIP_KILL_RADIUS
) : SpaceObject, Collider {
var velocity: Velocity = Velocity.ZERO
var heading: Double = 0.0
private var dropScale = U.DROP_SCALE
var accelerating: Boolean = false
var displayAcceleration: Int = 0
...
I think the controls are all we need. Let’s see what GameCycler does with ship.
class GameCycler(
private val game: Game,
private val knownObjects: SpaceObjectCollection,
initialNumberOfAsteroidsToCreate: Int,
val ship: Ship,
val saucer: Saucer
) {
...
private fun startShipAtHome(trans: Transaction) {
ship.setToHome()
trans.add(ship)
}
That’s it. Let’s change the signature on GameCycler and makeCycler
.
class GameCycler(
private val game: Game,
private val knownObjects: SpaceObjectCollection,
initialNumberOfAsteroidsToCreate: Int,
val controls: Controls,
val saucer: Saucer
) {
While we’re here, fix this. Kotlin guess wrong:
private fun startShipAtHome(trans: Transaction) {
controls.setToHome()
trans.add(controls)
}
It had no way of knowing:
private fun startShipAtHome(trans: Transaction) {
val ship = Ship(U.CENTER_OF_UNIVERSE,controls)
trans.add(ship)
}
And over in game:
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
saucer = Saucer()
cycler = makeCycler(controls)
cycler.cancelAllOneShots()
trans.clear()
}
fun makeCycler(controls: Controls = Controls()) = GameCycler(this, knownObjects, numberOfAsteroidsToCreate, controls, saucer)
This isn’t as nice as I’d like but we’re trying to get to green. We won’t stop there. I think this works. I’m almost right, by which I mean “I’m wrong”. We had to change the default cycler. Which I hate.
private var cycler: GameCycler = GameCycler(this, knownObjects, 0, Controls(), saucer)
With that in place we are good. Why do we save the cycler? Oh, right, we have to tell it to cycle:
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
cycler.cycle(deltaTime, drawer)
}
We’ll try to improve that. Can we get rid of ship now?
The good news is that Game doesn’t use ship for anything. The bad news is that several tests do. I’ll remove the member and then we’ll check the tests.
@Test
fun `asteroid too close makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
val game = Game(mix)
game.ship = Ship(U.CENTER_OF_UNIVERSE)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(true)
val asteroid = Asteroid(Point(100.0, 100.0))
mix.add(asteroid)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(true)
val dangerousAsteroid = Asteroid(U.CENTER_OF_UNIVERSE + Point(50.0, 50.0))
mix.add(dangerousAsteroid)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(false)
}
Harder than I expected
We’re trying to test the canShipEmerge
method, which is now on GameCycler:
fun canShipEmerge(): Boolean {
if (knownObjects.saucerIsPresent()) return false
if (knownObjects.missiles().isNotEmpty()) return false
for ( asteroid in knownObjects.asteroids() ) {
val distance = asteroid.position.distanceTo(U.CENTER_OF_UNIVERSE)
if ( distance < U.SAFE_SHIP_DISTANCE ) return false
}
return true
}
You could make a case for feature envy here. All the messages are being sent to knownObjects
or to things inside knownObjects
. The case against that idea is that SpaceObjectCollection
isn’t supposed to know that much about the semantics of the things it holds. Let’s extract a method here, that takes a SpaceObjectCollection as a parameter. Then we’ll pass the objects in from the test.
private fun canShipEmerge(): Boolean {
return canShipEmerge(knownObjects)
}
fun canShipEmerge(spaceObjects: SpaceObjectCollection): Boolean {
if (spaceObjects.saucerIsPresent()) return false
if (spaceObjects.missiles().isNotEmpty()) return false
for ( asteroid in spaceObjects.asteroids() ) {
val distance = asteroid.position.distanceTo(U.CENTER_OF_UNIVERSE)
if ( distance < U.SAFE_SHIP_DISTANCE ) return false
}
return true
}
I made the top one private, which will conveniently find all the tests that need fixing.
Let’s tick through some of the tests:
@Test
fun `saucer makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
val game = Game(mix)
game.ship = Ship(U.CENTER_OF_UNIVERSE)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(true)
val saucer = Saucer()
mix.add(saucer)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(false)
}
We just need the mix
now, nothing else. I want to say this but I can’t:
@Test
fun `saucer makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
mix.add(Ship(U.CENTER_OF_UNIVERSE))
val cycler = GameCycler()
assertThat(cycler.canShipEmerge(mix)).isEqualTo(true)
val saucer = Saucer()
mix.add(saucer)
assertThat(cycler.canShipEmerge(mix)).isEqualTo(false)
}
Feature Envy Fix
I can’t make that naked a GameCycler, even though the method I want to call doesn’t care.
I think this is a clear indication that I need to move that method over to SpaceObjectCollection. Let’s revert for a clean starting point.
OK, let’s move this method over to SpaceObjectCollection where it seems to want to reside:
fun canShipEmerge(): Boolean {
if (knownObjects.saucerIsPresent()) return false
if (knownObjects.missiles().isNotEmpty()) return false
for ( asteroid in knownObjects.asteroids() ) {
val distance = asteroid.position.distanceTo(U.CENTER_OF_UNIVERSE)
if ( distance < U.SAFE_SHIP_DISTANCE ) return false
}
return true
}
I think I can just copy and paste it over and hammer it a bit:
class SpaceObjectCollection ...
fun canShipEmerge(): Boolean {
if (saucerIsPresent()) return false
if (missiles().isNotEmpty()) return false
for ( asteroid in asteroids() ) {
val distance = asteroid.position.distanceTo(U.CENTER_OF_UNIVERSE)
if ( distance < U.SAFE_SHIP_DISTANCE ) return false
}
return true
}
class GameCycler ...
fun canShipEmerge(): Boolean {
return knownObjects.canShipEmerge()
}
Now let’s find all the senders of this and convert them to talk to the mix:
Here’s this again:
@Test
fun `asteroid too close makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
val game = Game(mix)
game.ship = Ship(U.CENTER_OF_UNIVERSE)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(true)
val asteroid = Asteroid(Point(100.0, 100.0))
mix.add(asteroid)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(true)
val dangerousAsteroid = Asteroid(U.CENTER_OF_UNIVERSE + Point(50.0, 50.0))
mix.add(dangerousAsteroid)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(false)
}
We can simplify this nicely:
fun `asteroid too close makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
mix.add(Ship(U.CENTER_OF_UNIVERSE))
assertThat(mix.canShipEmerge()).isEqualTo(true)
val asteroid = Asteroid(Point(100.0, 100.0))
mix.add(asteroid)
assertThat(mix.canShipEmerge()).isEqualTo(true)
val dangerousAsteroid = Asteroid(U.CENTER_OF_UNIVERSE + Point(50.0, 50.0))
mix.add(dangerousAsteroid)
assertThat(mix.canShipEmerge()).isEqualTo(false)
}
I rather expect this test to run green. I’ll just run them one at a time as I clean them up. That one’s green. Who’s next?
@Test
fun `missile makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
val game = Game(mix)
game.ship = Ship(U.CENTER_OF_UNIVERSE)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(true)
val missile = Missile(Point(100.0, 100.0))
mix.add(missile)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(false)
}
Same deal:
fun `missile makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
mix.add(Ship(U.CENTER_OF_UNIVERSE))
assertThat(mix.canShipEmerge()).isEqualTo(true)
val missile = Missile(Point(100.0, 100.0))
mix.add(missile)
assertThat(mix.canShipEmerge()).isEqualTo(false)
}
Green. Next?
@Test
fun `saucer makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
val game = Game(mix)
game.ship = Ship(U.CENTER_OF_UNIVERSE)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(true)
val saucer = Saucer()
mix.add(saucer)
assertThat(game.makeCycler().canShipEmerge()).isEqualTo(false)
}
Again:
fun `saucer makes it unsafe for ship`() {
val mix = SpaceObjectCollection()
mix.add(Ship(U.CENTER_OF_UNIVERSE))
assertThat(mix.canShipEmerge()).isEqualTo(true)
val saucer = Saucer()
mix.add(saucer)
assertThat(mix.canShipEmerge()).isEqualTo(false)
}
Green. Test all. All good. Commit: canShipEmerge defers to knownObjects.
Remove Game Instance
The warnings remind me that GameCycler no longer needs game. Let’s change the signature:
class GameCycler(
private val knownObjects: SpaceObjectCollection,
initialNumberOfAsteroidsToCreate: Int,
private val controls: Controls,
val saucer: Saucer
) {
class Game ...
private var cycler: GameCycler = GameCycler(knownObjects, 0, Controls(), saucer)
fun makeCycler(controls: Controls = Controls()) = GameCycler(knownObjects, numberOfAsteroidsToCreate, controls, saucer)
Test. Green. Commit: GameCycler no longer receives game instance.
Where were we? Time to reflect, I’ve kind of pushed my memory stack too far.
Reflection
Our purpose, as I understand it now, is to reduce the number of strange objects that Game has to hold on to just to pass them to GameCycler. This will simplify Game and GameCycler as well. We’ve managed to remove the ship, changing to pass a Controls instance instead, and we’ve moved from anyone caching a single ship to creating new ones every time we need one.
It would be nice not to have to pass Saucer, and not to have to hold on to it. The saucer has a tiny bit of necessary memory, direction
, which remembers whether to enter from left or right, because it alternates. We could change it to be random. But do we need to pass it to GameCycler at all? Why can’t it just create the Saucer?
Don’t Pass Saucer
Let’s just change GameCycler not to expect a saucer, but to create one:
class GameCycler(
private val knownObjects: SpaceObjectCollection,
initialNumberOfAsteroidsToCreate: Int,
private val controls: Controls,
) {
val saucer = Saucer()
class Game
private var cycler: GameCycler = GameCycler(knownObjects, 0, Controls())
fun makeCycler(controls: Controls = Controls()) = GameCycler(knownObjects, numberOfAsteroidsToCreate, controls)
This should run green, I reckon. It does. Commit: Game no longer passes saucer to GameCycler.
Now what other use have we for the saucer in Game? One would think none. It just creates it to be ignored. Remove the definition and the setting.
Test. Green. Commit: remove saucer member.
Commentary
Clearly I “wasted” some commits here. I could have combined the last two. Why didn’t I? Because it takes no time to do a commit and when I make a mistake it takes a while to fix it. So I try to make the smallest changes I can and to commit every time everything is green. This gives me a sense of rhythm as I work, helps me avoid creating hard-to-debug problems, and ensures that most of my progress is forward.
I like it when I do this.
Back To It - Ship?
We haven’t removed the ship member from Game yet, and there are some tests that refer to it. Weird, I know. Some of my tests, well, they’re useful but not pretty.
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
val shipPosition = U.CENTER_OF_UNIVERSE
ship = Ship(shipPosition, controls)
cycler = makeCycler(controls)
cycler.cancelAllOneShots()
trans.clear()
}
We don’t even use the ship here. We can remove that line. In fact, it seems to me that no one could possibly be using the value for anything. Remove this ship line. Tests are green. Commit: Game does not create a ship during createInitialObjects. Remove the ship position line and commit again. Thanks for the warning, IDEA.
Now let’s see who complains when we remove the member.
@Test
fun `colliding ship and asteroid splits asteroid, loses ship`() {
val game = Game()
val asteroid = Asteroid(Vector2(1000.0, 1000.0))
val ship = Ship(
position = Vector2(1000.0, 1000.0)
)
game.ship = ship // crock
game.knownObjects.add(asteroid)
game.knownObjects.add(ship)
assertThat(game.knownObjects.spaceObjects().size).isEqualTo(2)
assertThat(ship).isIn(game.knownObjects.spaceObjects())
game.makeCycler().processInteractions()
assertThat(ship).isNotIn(game.knownObjects.spaceObjects())
assertThat(game.knownObjects.asteroidCount()).isEqualTo(2)
}
I think we can delete that line marked “crock”. I’d like to get past that makeCycler()
as well, but one thing at a time. I find and remove all the tests that do that crock trick and they are now all green.
Commit: Remove ship member var from Game. Byebye lateinit.
What else are we saving in Game just to pass it to makeCycle? And can we get rid of that method from the tests?
class GameCycler(
private val knownObjects: SpaceObjectCollection,
initialNumberOfAsteroidsToCreate: Int,
private val controls: Controls,
)
We clearly need all of those when we start a real game.
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
cycler = makeCycler(controls)
cycler.cancelAllOneShots()
trans.clear()
}
fun makeCycler(controls: Controls = Controls()) = GameCycler(knownObjects, numberOfAsteroidsToCreate, controls)
We should pass all those in from the call, rather than pick them up from the members. Then we’ll default them here.
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
cycler = makeCycler(knownObjects, numberOfAsteroidsToCreate, controls)
cycler.cancelAllOneShots()
trans.clear()
}
fun makeCycler(
knownObjects: SpaceObjectCollection,
numberOfAsteroidsToCreate: Int = -1,
controls: Controls = Controls()
) = GameCycler(knownObjects, numberOfAsteroidsToCreate, controls)
Create Cyclers Directly
That will break all my calls to makeCycler
from the tests. I think we can tick through and change them readily, to directly create the darn thing.
@Test
fun `colliding ship and asteroid splits asteroid, loses ship`() {
val game = Game()
val asteroid = Asteroid(Vector2(1000.0, 1000.0))
val ship = Ship(
position = Vector2(1000.0, 1000.0)
)
game.knownObjects.add(asteroid)
game.knownObjects.add(ship)
assertThat(game.knownObjects.spaceObjects().size).isEqualTo(2)
assertThat(ship).isIn(game.knownObjects.spaceObjects())
game.makeCycler().processInteractions()
assertThat(ship).isNotIn(game.knownObjects.spaceObjects())
assertThat(game.knownObjects.asteroidCount()).isEqualTo(2)
}
This is a good time to recast this test in terms that make more sense:
@Test
fun `colliding ship and asteroid splits asteroid, loses ship`() {
val asteroid = Asteroid(Vector2(1000.0, 1000.0))
val ship = Ship(
position = Vector2(1000.0, 1000.0)
)
val knownObjects = SpaceObjectCollection()
knownObjects.add(asteroid)
knownObjects.add(ship)
assertThat(knownObjects.spaceObjects().size).isEqualTo(2)
assertThat(ship).isIn(knownObjects.spaceObjects())
GameCycler(knownObjects).processInteractions()
assertThat(ship).isNotIn(knownObjects.spaceObjects())
assertThat(knownObjects.asteroidCount()).isEqualTo(2)
}
This tells me that I really want those defaults in GameCycler itself:
class GameCycler(
private val knownObjects: SpaceObjectCollection,
initialNumberOfAsteroidsToCreate: Int = -1,
private val controls: Controls = Controls(),
) {
The test above should be happy now. Unfortunately the file won’t compile until I fix the other tests. Grr.
Hm, after making the obvious changes a couple of tests are actually failing. I think I probably did the tests wrong. Let’s see.
expected: 4
but was: 0
@Test
fun `game can create asteroids directly`() {
val game = Game()
game.createInitialContents(Controls())
val transForFour = Transaction()
val cycler = game.makeCycler(game.knownObjects)
cycler.makeWave(transForFour)
assertThat(transForFour.asteroids().size).isEqualTo(4)
val transForSix = Transaction()
cycler.makeWave(transForSix)
assertThat(transForSix.asteroids().size).isEqualTo(6)
}
Ah. We have to specify how many asteroids we want in this case:
@Test
fun `game can create asteroids directly`() {
val game = Game()
game.createInitialContents(Controls())
val transForFour = Transaction()
val cycler = game.makeCycler(game.knownObjects, 4)
cycler.makeWave(transForFour)
assertThat(transForFour.asteroids().size).isEqualTo(4)
val transForSix = Transaction()
cycler.makeWave(transForSix)
assertThat(transForSix.asteroids().size).isEqualTo(6)
}
That one runs.
@Test
fun `how many asteroids per wave`() {
val game = Game()
game.insertQuarter(Controls())
val cycler = game.makeCycler(game.knownObjects)
assertThat(cycler.howMany()).isEqualTo(4)
assertThat(cycler.howMany()).isEqualTo(6)
assertThat(cycler.howMany()).isEqualTo(8)
assertThat(cycler.howMany()).isEqualTo(10)
assertThat(cycler.howMany()).isEqualTo(11)
assertThat(cycler.howMany()).isEqualTo(11)
}
Same issue. We are green. Commit: set up default parameters for GameCycler and makeCycler, for test convenience.
The article’s getting long but I don’t feel done yet.
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
cycler = makeCycler(knownObjects, numberOfAsteroidsToCreate, controls)
cycler.cancelAllOneShots()
trans.clear()
}
A bit of thinking …
Hm, I think there’s an issue here. Is it necessary to cancel the OneShots? They’ll be brand new, as they are now created in GameCycler. I think that can be removed.
What about the trans.clear()
? Does that make sense? Ah, right. That tells the transaction to clear the known objects before doing any of its adds or removes. It would be better, methinks, if we created a whole new SpaceObjectCollection with each new GameCycler.
Let’s look a bit further up the chain …
Game can be created with a given collection:
class Game(val knownObjects:SpaceObjectCollection = SpaceObjectCollection()) {
I suspect some tests do that and that they should stop. We’ll let it ride for a moment: we’re exploring here.
We create two different kinds of Game contents, the initial ones that form an attract screen, and the real one when you insert a quarter:
fun createInitialContents(controls: Controls) {
initializeGame(controls, -1)
}
fun insertQuarter(controls: Controls) {
initializeGame(controls, U.SHIPS_PER_QUARTER)
}
private fun initializeGame(controls: Controls, shipCount: Int) {
numberOfAsteroidsToCreate = U.ASTEROID_STARTING_COUNT
knownObjects.performWithTransaction { trans ->
createInitialObjects(trans,shipCount, controls)
}
}
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
cycler = makeCycler(knownObjects, numberOfAsteroidsToCreate, controls)
trans.clear()
}
Both of these paths could create a new knownObjects
, and would have no reason to hold on to it that I can see.
Too big for this time of day! Nope!
However, we have 25 (!) tests that are creating a game and providing it with a collection. I think we’ll save that trek for another day. Jira yellow sticky note: “Change tests not to use game(mix), remove, make temp”.
Inline makeCycler
I think we might do one more thing. Let’s change the calls to makeCycler
in the tests to create the Cycler directly. Change a few lines saying game.makeCycler...
to say GameCycler...
and inline the no longer needed makeCycler
:
private fun createInitialObjects(
trans: Transaction,
shipCount: Int,
controls: Controls
) {
knownObjects.scoreKeeper = ScoreKeeper(shipCount)
cycler = GameCycler(knownObjects, numberOfAsteroidsToCreate, controls)
trans.clear()
}
Test. Green. Commit: remove makeCycler helper method.
Let’s sum up.
Summary
We’ve removed most of the member variables from Game.
class Game(val knownObjects:SpaceObjectCollection = SpaceObjectCollection()) {
private var lastTime = 0.0
private var numberOfAsteroidsToCreate = 0
private var cycler: GameCycler = GameCycler(knownObjects, 0, Controls())
I think it actually needs all of those. We could get rid of lastTime
by doing the subtraction in GameCycler, which would be OK. But we do need to know the number of asteroids wanted? Or do we? Aren’t they passed in when we create things?
OK, maybe we could get rid of 2/3 of them, but I’m sure we need the cycler, so that we can forward the cycle
method to GameCycler. Unless …
We’ll leave all that for another day, but I’m getting the impression that we might have found it interesting to extract the creation instead of the running of the game. Interesting. Let me expand on that.
I find myself kind of wanting to have the main program talk directly to the GameCycler, which is now subordinate to the object that the main creates, the game. If we had extracted creation instead of cycling, might we have had a factory to use and might we be able to rig the main program to use the factory to build the cycler? Might we be able to do that now? I should think we could.
Maybe we will. But we may, possibly, have gone around three sides of a square to get to the adjacent corner. Do we care? Not really: we weren’t smart enough to do it then and we’re here now, in a good place.
Fascinating
What is fascinating to me are at least two things:
- Small Transformations, Big Changes
-
First, we have transformed the program’s design, again and again, each time making it a bit different and (I’d claim) a bit better (or at least a bit more like what we wanted). And throughout all those design changes, some of which were quite fundamental, we’ve taken steps that were just a few minutes each, and after each step, again had a perfectly good running program.
-
To the extent that it generalizes, this is a very strong result. It suggests something that, based on decades of experience I firmly believe: We can almost always improve a program’s design as we need to, without breaking it, and without long delays before delivering value. And by “almost”, I mean “it is always the way to bet”.
- Always Room For More
-
Second, I am amazed that in a program whose design I really like, and that is quite easy to change, we continue to find valuable and interesting improvements to make to its design. Of course, with a real product, we couldn’t just spend a period of a few weeks improving things, as I’ve done in this article, but the first fascinating thing says we could do things as we found them.
-
I suppose it could be depressing to think that there’s nearly always room for improvement, but I find it to be a good thing. What it’s telling us is that our sense of what’s good can always be improving, and that we’ll always have the opportunity to do a little something we can be proud of.
I hope that you find opportunities to be proud of your work. If, sadly, you do not find enough of them, I have two ideas for you:
- Look For and Take Small Opportunities
-
We might refine your vision and your skill at small steps, and make tiny improvements. It’s surprising how good it feels just to do something well, when we pay attention to it.
- Look For and Consider Large Opportunities
-
If the job isn’t providing joy, and we can’t find joy in it, it may be time to open our ears to new opportunities. Yes, the market is probably bad just now, but that doesn’t mean that it’s bad for us. We can use our contacts, and just keep ears open. Something better may be out there. Programming can be great fun and we all deserve fun.
See you next time. We’re not done yet!