Space Invaders 76
I have an idea and I can’t wait to try it out.
This is the Button class just now:
-- Button
-- RJ 20201023
Button = class()
function Button:init(message, size, boxX, boxY, boxW,boxH, textX, textY)
self.message = message
self.size = size
self.boxX = boxX
self.boxY = boxY
self.boxW = boxW
self.boxH = boxH
self.textX = textX
self.textY = textY
end
function Button:rightButton(s2, size, boxRightX, boxY)
local margin = 8
fontSize(size)
sx,sy = textSize(s2)
return self:leftButton(s2, size, boxRightX-margin-sx, boxY)
end
function Button:leftButton(s1, size, boxX, boxY)
local margin = 8
local spacing = 4
local textX = boxX + margin
local textY = boxY + spacing
fontSize(size)
local sx,sy = textSize(s1)
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
function Button:draw()
pushStyle()
fontSize(self.size)
fill(0,0,0,0)
rect(self.boxX,self.boxY, self.boxW,self.boxH)
fill(255)
text(self.message, self.textX, self.textY)
popStyle()
end
I’m not sure quite what I don’t like about it. The two separate calls to textSize make me think there’s something odd, plus the duplication of the margin value.
What I propose to do is to try to make the right and left calls as close to the same as possible, then see if I can remove the duplication differently from how it’s done here, to better result. I base this on something I saw over two decades ago, when Kent Beck took two pieces of code that didn’t seem similar at all, made them look the same, and then reduced the code to a simpler form that it had been in. I’m not that good, but I want to try.
I propose to call the button constructor from each of the two right and left class methods, instead of right calling left. We’ll see how that goes.
First I move the marked stuff up, so they look more similar:
function Button:rightButton(s2, size, boxRightX, boxY)
local margin = 8
local spacing = 4 -- <---
local textX = boxX + margin --< ---
local textY = boxY + spacing -- <---
fontSize(size)
sx,sy = textSize(s2)
return self:leftButton(s2, size, boxRightX-margin-sx, boxY)
end
function Button:leftButton(s1, size, boxX, boxY)
local margin = 8
local spacing = 4
local textX = boxX + margin
local textY = boxY + spacing
fontSize(size)
local sx,sy = textSize(s1)
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
This of course still works, since all the new lines are unused. Now it seems that boxW and boxH are surely the same, so, with renaming the string parm:
function Button:rightButton(s1, size, boxRightX, boxY)
local margin = 8
local spacing = 4
local textX = boxX + margin
local textY = boxY + spacing
fontSize(size)
sx,sy = textSize(s1)
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return self:leftButton(s1, size, boxRightX-margin-sx, boxY)
end
function Button:leftButton(s1, size, boxX, boxY)
local margin = 8
local spacing = 4
local textX = boxX + margin
local textY = boxY + spacing
fontSize(size)
local sx,sy = textSize(s1)
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
Now if I were to call the Button()
creator, I’d be missing boxX:
function Button:rightButton(s1, size, boxRightX, boxY)
local margin = 8
local spacing = 4
local textX = boxX + margin
local textY = boxY + spacing
fontSize(size)
sx,sy = textSize(s1)
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
So with this addition and a little reordering, this works:
function Button:rightButton(s1, size, boxRightX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
sx,sy = textSize(s1)
local boxX = boxRightX - margin - sx
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
function Button:leftButton(s1, size, boxX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
local sx,sy = textSize(s1)
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
Now we’re SOOO close! Just that pesky boxX
calculation.
I should mention that I could probably just think hard to figure this out but the idea is not to think hard, just kind of make the two methods more and more similar by rote.
What if we did this:
function Button:rightButton(s1, size, boxRightX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
sx,sy = textSize(s1)
local boxX = self:leftMargin(boxRightX, "right", margin, sx)
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
function Button:leftButton(s1, size, boxLeftX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
local sx,sy = textSize(s1)
local boxX = self:leftMargin(boxLeftX, "left", margin, sx)
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
function Button:leftMargin(boxX, direction, margin, sx)
if direction == "left" then return boxX end
return boxX - margin - sx
end
That’s interesting. Now to rename the input parms:
function Button:rightButton(s1, size, boxInputX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
sx,sy = textSize(s1)
local boxX = self:leftMargin(boxInputX, "right", margin, sx)
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
function Button:leftButton(s1, size, boxInputX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
local sx,sy = textSize(s1)
local boxX = self:leftMargin(boxInputX, "left", margin, sx)
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
return Button(s1,size, boxX,boxY, boxW,boxH, textX,textY)
end
The two versions are now identical. We can begin to move code down to Button:init()
. It seems to me that I can move the whole shebang into init
:
Meh. Did something wrong. Revert. And commit so I can revert more easily next time. Commit: refactoring Button.
Now I can get back to a stable point more easily. Should have been doing this every time it worked. Now it seems to me that I can paste this code into init
but that didn’t seem to work. One more try, in case I don’t make the same mistake again.
I’ll need to rename the input boxX parm, and pass in direction. So I’ve done this:
function Button:init(message, size, direction, boxInputX, boxY, boxW,boxH, textX, textY)
local margin = 8
local spacing = 4
fontSize(size)
sx,sy = textSize(message)
local boxX = self:leftMargin(boxInputX, direction, margin, sx)
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
self.message = message
self.size = size
self.boxX = boxX
self.boxY = boxY
self.boxW = boxW
self.boxH = boxH
self.textX = textX
self.textY = textY
end
That’s a bit messy but if it works, it was all by rote. If it works. If not, I’ll take a smaller bite.
The calls now:
function Button:rightButton(s1, size, boxInputX, boxY)
return Button(s1,size, "right", boxInputX,boxY, boxW,boxH, textX,textY)
end
function Button:leftButton(s1, size, boxInputX, boxY)
return Button(s1,size, "left", boxInputX,boxY, boxW,boxH, textX,textY)
end
Yes! It’s still working. Commit: more refactoring Button.
Now most of those parameters are no longer used. We should be able to remove textX
and textY
. Yes. And boxW and boxH.
This now works:
function Button:init(message, size, direction, boxInputX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
sx,sy = textSize(message)
local boxX = self:leftMargin(boxInputX, direction, margin, sx)
local textX = boxX + margin
local textY = boxY + spacing
local boxW = 2*margin+sx
local boxH = 2*spacing+sy
self.message = message
self.size = size
self.boxX = boxX
self.boxY = boxY
self.boxW = boxW
self.boxH = boxH
self.textX = textX
self.textY = textY
end
Commit: still more refactoring Button.
Now most of those locals turn into member variables:
And here we are:
function Button:init(message, size, direction, boxInputX, boxY)
local margin = 8
local spacing = 4
fontSize(size)
local sx,sy = textSize(message)
self.boxY = boxY
self.boxX = self:leftMargin(boxInputX, direction, margin, sx)
self.textX = self.boxX + margin
self.textY = self.boxY + spacing
self.boxW = 2*margin+sx
self.boxH = 2*spacing+sy
self.message = message
self.size = size
end
function Button:rightButton(s1, size, boxInputX, boxY)
return Button(s1,size, "right", boxInputX,boxY)
end
function Button:leftButton(s1, size, boxInputX, boxY)
return Button(s1,size, "left", boxInputX,boxY)
end
function Button:leftMargin(boxX, direction, margin, sx)
if direction == "left" then return boxX end
return boxX - margin - sx
end
I think I’d rename the parameters to the factory calls:
function Button:rightButton(message, fontSize, boxRightX, boxY)
return Button(message,fontSize, "right", boxRightX,boxY)
end
function Button:leftButton(message, fontSize, boxLeftX, boxY)
return Button(message,fontSize, "left", boxLeftX,boxY)
end
The X’s were named that way before, but I changed them both to “Input” to make the code identical.
I’m a bit bothered by calling that parameter fontSize
since there is a system function of the same name, but as long as we don’t name it that in the init
it’s OK. I like that when you read these two methods, you see better what you need to pass in.
This seems to me to be much more betterer. The creation calls are symmetric and the code is simpler if only because it’s a bit more conventional in style.
Commit: right and left Button work same way.
Staring at this, I think moving a couple of lines makes it better:
function Button:init(message, size, direction, boxInputX, boxY)
local margin = 8
local spacing = 4
self.message = message
self.size = size
fontSize(self.size)
local sx,sy = textSize(message)
self.boxX = self:leftMargin(boxInputX, direction, margin, sx)
self.boxY = boxY
self.textX = self.boxX + margin
self.textY = self.boxY + spacing
self.boxW = 2*margin+sx
self.boxH = 2*spacing+sy
end
The order makes a bit more sense to me now. I’m concerned about those locals, margin and spacing. First commit this reordering: reorder Button init.
Now what if we made these values either member variables or elements in Constants, or class functions on Button. When this Button class grows up, probably margin and spacing will be parameters, but that’s more than we need here. I think the wise thing is to leave them alone.
Let’s Sum Up
I was troubled by the shape of the new Button class. It successfully drew buttons but it seemed asymmetric to me. Rather than apply actual thought to the problem, I instead made the two factory creation methods, rightButton
and leftButton
look more and more the same, by duplicating more and more code into each. Finally there was only one difference, the code that sets the box left position, which is as provided for a left button and needs to be calculated for a right button.
I abstracted that into a trivial method:
function Button:leftMargin(boxX, direction, margin, sx)
if direction == "left" then return boxX end
return boxX - margin - sx
end
That made the only difference between the two creation methods the occurrence of “left” or “right”. Then I moved all the duplicated code into init
, which required a small change to the init
method to pass in the direction. Then a series of small changes reduced the init
parameters, as everything was computed inside.
A bit of tidying, and now we have something I can feel good about.
Could I have done this some other way, by thinking and deciding and drawing some pictures? Probably. But this way involved only very small local decisions. At first, the local decisions made things worse, increasing duplication, but that was my plan. Then we reduced duplication by drawing everything into init
, and then step by step, inch by inch, slowly we turned the init
into a rather straightforward sequential method.
I can assure you that I hardly thought at all.
See you next time!