Exercise based on the Trivia Legacy Code Retreat code.
- Capture the program's output and check whether it's deterministic or not;
- Repeat the execution a reasonable number of times, until the code coverage gives you enough confidence for your subsequent refactoring activities;
- Collect the output in a file, and include it in the test project, using an error-prone manual operation (Poka-yoke).
- Identify the source of non-determinism;
- Make it parametric and make the parameter controllable via args;
- Use parameters in tests to make them deterministic.
- Identify the source of non-determinism;
- Extract it as a method;
- Make the method virtual, so it can be overridden in the tests; using inheritance make a test version of the productive code, which removes the non-deterministic behaviors;
- When needed, introduce non-static classes and replace the static program entry point with a non-static method.
- Identify the source of non-determinism;
- Isolate it from the code, moving it to the top;
- Extract the rest of the code in a separate method, which takes the non-deterministic value as a parameter;
- Use the new method in the test, with a deterministic parameter.
- Extract the business logic present in the constructor of
Game
to a separate methodFillQuestions()
- Move
FillQuestions()
in a separateQuestionDeck
class;Game
's constructor will create an instance ofQuestionDeck
and will store it as an instance field; - Pass
FillQuestions()
the current instance ofGame
so it can stil access the needed fields. We will move them later; - To achieve the previous step, encapsulate all the fields
FillQuestions()
needs to access, creating Bottlenecks. FillQuestions()
also accesses anotherGame
's method,createRockQuestion()
: move it toQuestionDeck
as well;
- We aim to move
private String currentCategory()
{
if (places[currentPlayer] == 0) return "Pop";
if (places[currentPlayer] == 4) return "Pop";
if (places[currentPlayer] == 8) return "Pop";
if (places[currentPlayer] == 1) return "Science";
if (places[currentPlayer] == 5) return "Science";
if (places[currentPlayer] == 9) return "Science";
if (places[currentPlayer] == 2) return "Sports";
if (places[currentPlayer] == 6) return "Sports";
if (places[currentPlayer] == 10) return "Sports";
return "Rock";
}
to QuestionDeck
;
- First, make
places[currentPlayer]
a parameter to remove the duplication. To do so, create a Bottleneck forcurrentCategory
extracting its body in a separate method; - Then refactor
places[currentPlayer]
as a parameter; - The resulting method won't contain any reference to
places[currentPlayer]
, and won't have but 1 single usage (the originalcurrentCategory()
), since it's a bottleneck; move it toQuestionDeck
- We aim to move
private void askQuestion()
{
if (currentCategory() == "Pop")
{
Console.WriteLine(PopQuestions.First());
PopQuestions.RemoveFirst();
}
if (currentCategory() == "Science")
{
Console.WriteLine(ScienceQuestions.First());
ScienceQuestions.RemoveFirst();
}
if (currentCategory() == "Sports")
{
Console.WriteLine(SportsQuestions.First());
SportsQuestions.RemoveFirst();
}
if (currentCategory() == "Rock")
{
Console.WriteLine(RockQuestions.First());
RockQuestions.RemoveFirst();
}
}
to QuestionDeck
- Since there are multiple usages of
QuestionDeck
, first create a bottleneck using Extract Method, generating a separateAskQuestionCategory()
method; - Make
currentCategory()
one of its parameters with Extract Parameter; - Use Move to move
AskQuestionCategory()
toQuestionDeck
.
- We aim to move
Game
's fields toQuestionDeck
- First inject the
Game
instance intoQuestionDeck
through constructor instead of through methods; that's another way to create a bottleneck, since we end up with one single reference toGame
; - Now that there's one single reference to
Game
, moveGame
's fields toQuestionDeck
, using Extract Field; - Using Inline we can now remove all the references from
Game
. - Remove the injection of the unused
Game
instance intoQuestioDeck
- We aim to remove the duplication of present in
AskCategoryQuestion
: there are multipleConsole.WriteLine()
; - Using
ExtractMethod
,Split declaration and assignment
,Move to outer scope
,Merge variables
,Rename
andInline
it's possible to move all theConsole.WriteLine()
s up in the caller methodGame.askQuestion()
- Once
AskCategoryQuestion()
returns a string rather than writing to the console, it's easy to cover its behavior with unit tests.
- Looking at
FillQuestions()
's code it's apparent that there is a missing reuse of the code implemented byCreateRockQuestion()
public void FillQuestions()
{
for (var i = 0; i < 50; i++)
{
_popQuestions.AddLast("Pop Question " + i);
_scienceQuestions.AddLast(("Science Question " + i));
_sportsQuestions.AddLast(("Sports Question " + i));
_rockQuestions.AddLast(CreateRockQuestion(i));
}
}
public string CreateRockQuestion(int index)
{
return "Rock Question " + index;
}
- Using
Split string
,Introduce parameter
and "Rename" convert to the code
public void FillQuestions()
{
for (var i = 0; i < 50; i++)
{
_popQuestions.AddLast(CreateQuestion(i, "Pop"));
_scienceQuestions.AddLast(CreateQuestion(i, "Science"));
_sportsQuestions.AddLast(CreateQuestion(i, "Sports"));
_rockQuestions.AddLast(CreateQuestion(i, "Rock"));
}
}
public string CreateQuestion(int index, string category)
{
return category + " Question " + index;
}