F___ Around and Find Out
Fool1 around and find out, they say, and that’s exactly what happened yesterday. I want to emphasize that that was a good thing.
One of my concerns with the design of the Dungeon program is that there are objects that hold on to instances of “higher-level” objects. I have come to believe that that’s usually a problem in the design, and that it can lead to defects where a low-level object sends a message to an otherwise obsolete higher one. So I was considering ways to avoid those up-pointers.
All of the action in the Dungeon game takes place on tiles, and the dungeon objects all know their current tile, and often want to know important things about their neighboring tiles or the dungeon at large. So they send a message to a higher-level object that has the big picture, and it replies with whatever info was needed. That makes sense to me, but the up pointers trouble me.
I had this idea that seemed tasty. Instead of the lower objects having up pointers, they could just publish an event to the EventBus, and somewhere that event would get fielded. And, if the event included a writeable object, a table or array, the fielder could just put the answer into it, and when the publish call returned, the answer would magically be in the table.
I was quite sure that this would work, though I hadn’t done it. And I could see that it didn’t change much for the caller, and it seemed it wouldn’t be bad at the other end, so I set out to do it, in Dungeon 280: Cleverness?
I want to summarize what happened, and why it was a good thing.
I Reverted the Work I Had Done
Right. I did it and then reverted it. Waste of time, right? Well, no. It was well worth doing. Here’s what happened:
I started with this code:
function Monster:manhattanDistanceFromPlayer()
return self.runner:playerManhattanDistance(self:getTile())
end
The reference to runner
is what I wanted to get rid of. The replacement code was this:
function Monster:manhattanDistanceFromPlayer()
local result = {}
Bus:publish("playerManhattanDistance", self, self:getTile(), result)
return result[1]
end
Simple change. We create a result array, trigger a new event “playerManhattanDistance”, and pass in the necessary info, including the result. When the publish
returns, the result is in the array and we return it.
So far that seemed OK to me, so I made it work. That wa simple as well. The runner needed a new subscription call in its init:
...
Bus:subscribe(self, self.playerDistance, "playerManhattanDistance")
...
And then I just implemented the new method, playerDistance
, calling the same method as the Monster had originally called:
function GameRunner:playerDistance(event, sender, tile, result)
local dist = self:playerManhattanDistance(tile)
table.insert(result,dist)
end
It all worked. Just a bit of boilerplate, making the call a bit more indirect. But here’s a look at the whole thing:
Old:
function Monster:manhattanDistanceFromPlayer()
return self.runner:playerManhattanDistance(self:getTile())
end
New:
function Monster:manhattanDistanceFromPlayer()
local result = {}
Bus:publish("playerManhattanDistance", self, self:getTile(), result)
return result[1]
end
...
Bus:subscribe(self, self.playerDistance, "playerManhattanDistance")
...
function GameRunner:playerDistance(event, sender, tile, result)
local dist = self:playerManhattanDistance(tile)
table.insert(result,dist)
end
Looking at the code in place, my decision was easy. The question wasn’t whether this was too clever, the question was whether this was worth it at all. I had replaced one simple call with a more complex call, and, to field the call, had to add a line of code in the Runner init
, and a new method to tuck the answer into the array.
The indirection isn’t obvious, but I’m used to indirection. But to replace one line of code with seven or eight, in three places, while obscuring who does what … I just didn’t like it. Seeing it in place, my clever idea was, well, ugly2.
So I reverted the code and filed the idea away. Maybe I’ll use it someday. Maybe not. It’ll certainly be filed fairly deeply in the bag of tricks.
Like a Spike?
Was this just a spike? Not really. I had actually done a spike of the idea, a separate little program, to prove to myself that the idea would work. This was an actual change to the real program, not a throw-away experiment.
Oh, I did throw it away, but I didn’t expect to. I actually expected to accept the change and push forward with it. Until I saw my baby and realized it was ugly. I had to see it in place to see that.
Waste of Time?
I suppose it took me an hour to find a place for the change, to make and test the change, and write it up. Was it a waste of time?
No, because “Fool around and find out” is quite often the best way to make a decision and feel comfortable with it. A bit of time trying something to see how it looks and feels relieves a lot of tension in our thinking.
I’d been mulling this idea for days. It’s really a nifty idea, if perhaps a bit too clever. I kept wanting to do it, looking for places to do it, plotting about it, speculating about it.
Until I just did it, right there in a real program. Then I could see it. I could see the changes that had to be made, I could read the code and imagine what it would be like to run across it. And I could make the decision comfortably: don’t do this, it’s not worth it. It’s a good decision, I’m comfortable with it. Evil desires reduced. It’s a good thing.
Quite often, speculation won’t give us a good answer. We have tests, and we have the ability to revert. We don’t have to speculate. We can
Fool around and find out!