Ludo Refactoring — merge Game and Board
TL;DR
We merge the Game and Board classes. Although they have slightly different responsibilities, the Board is just a lower-level view of the Game. The only client of the Board is the Game.
Refactoring strategy
We can see the game as a specialization of the Board, so in a first step, we eliminate any naming conflicts and make the Game a subclass of the Board. (Actually three steps: eliminate naming conflicts, make it a subclass, and run the tests.)
Next we try to push up methods from Game to Board.
Then we remove references to Game, and remove the Game class.
Finally we rename Board to LudoGame.
We start with this version of the Ludo game from 2022-05-04.
Eliminate naming conflicts
Analysis
Here we have the common selectors.
Caveat: of course this won't work now, because in this image the classes have already been merged, and
GtLudoBoard
no longer exists. However you can browse the github snapshot above to get an insight into what the original state was.
common := GtLudoGame selectors asSet & GtLudoBoard selectors asSet
Let's review the Game methods also in the Board:
GtLudoGame methods select: [ :m | common includes: m selector ]
The gt*
view methods are just forwards, so we can ignore them. (I.e., remove them when we subclass Game from Board.)
Similarly, the feedback:
, die
, and roll:
methods just forward to the board, so we can remove them.
This leaves initialize
and players
.
The players
method is special, since Game uses the players slot to implement a queue of players to play, whereas the Board assumes the players are in a fixed order. We should rename the players slot in the Game to, e.g., playerQueue.
The two initialize
methods need to be merged. First we can compose them with a super initialize
. We should take care with the board
slot. We can initialize it to self
, and later deprecate and then remove the board
accessor.
Steps
We delete gt*
, feedback:
, die
, and roll:
from GtLudoGame
.
We rename the players
slot to playerQueue
.
We try to rename the players
accessor in GtLudoGame
, but this would also rename other players
methods in other classes, namely in GtLudoBoard
, so we must refactor manually.
We add a new playerQueue
accessor and remove the players
accessor.
We search for senders of players
within GtLudoGame
.
#players gtSenders & GtLudoGame gtMethodsInClass
We manually rename each players
send to a playerQueue
send.
This leaves the initialize
method.
As a quick solution, we set board
to self
. Later we can replace all sends to board
or self board
to self
and remove the board
slot and accessor.
We also perform a super initialize
.
We check that there are no more common methods except for initialize, and no more sends to players
except in the initialize
method.
GtLudoGame selectors asSet & GtLudoBoard selectors asSet
#players gtSenders & GtLudoGame gtMethodsInClass
Now we should be ready to make the Game a subclass of the Board
Turning the Game into a subclass of the Board
We change the GtLudoGame
definition to make it a subclass of GtLudoBoard
.
We check that the warnings in GtLudoGame>>#initialize
are now resolved, as feedback:
and die
are now inherited.
We run all the tests in GtLudoBoardExamples
and GtLudoGameExamples
.
examples := GtExplicitExampleGroup withAll: GtLudoGame package gtExamplesAllContained. examples runAll
All tests are green, so now lets'clean up the references to board
.
We change all sends to self board
into self
, and we remove the board
slot and accessors.
#board gtSenders & GtLudoGame gtMethodsInClass
The refactoring engine won't let us remove the slot, so we search for any remaining references:
#board gtReferences
We rename the slot and try to remove it, but it still thinks there are references. We'll worry about that later.
#boardNOTNEEDED gtReferences
We run all the tests again. This time the tests fail, and we find the dangling references to the slot, which is not initialized. We replace it by self
and remove the slot.
The tests are all green now. We can commit this version.
Merging the Game and the Board
Now we want to merge the two classes. We start by pushing all the slots of GtLudoGame
to the superclass.
We can review the changes first:
namespace := RBNamespace new. (GtLudoGame slots collect: [ :each | each name asSymbol ]) do: [ :each | (RBPullUpInstanceVariableRefactoring model: namespace variable: each class: GtLudoGame superclass) primitiveExecute ]. namespace
And now do it.
(GtLudoGame slots collect: [ :each | each name asSymbol ]) do: [ :each | (RBPullUpInstanceVariableRefactoring variable: each class: GtLudoGame superclass) execute ]
The tests are green!
Now let's move up all the methods except initialize
.
methodsToPushUp := GtLudoGame selectors reject: [:each | each = #initialize]
Let's inspect the changes:
namespace := RBNamespace new. (RBPullUpMethodRefactoring model: namespace pullUp: methodsToPushUp from: GtLudoGame) primitiveExecute. namespace
Looks good. Let's do it.
(RBPullUpMethodRefactoring pullUp: methodsToPushUp from: GtLudoGame) execute
(Actually this raises an error (Instance of AsyncStreamTransitionBuffer did not understand #preloadAmount:
) but it works in the Morphic browser for now.
We run the tests.
Now we just have to fix the initialize method. We copy up the Game initialization and run the tests
Finally we replace all references to GtLudoGame
to refer to the Board instaed.
GtLudoGame gtReferences
There is only one (in the examples) so we make it refer to the Board.
We remove the Game class and run the tests.
We rename Board to Game.
The tests are green, so we commit this version.