
Chiunque lavori nel mondo dello sviluppo software avrà avuto l’esigenza di aggiungere feature al codice legacy, ereditato magari dal precedente team e su cui fare una fix urgentissima.
Tra le tante definizioni di codice legacy che si trovano in letteratura, quella che preferisco è: “per Legacy code si intende il codice utile che ho paura di modificare”. Se ci pensate, esprime due concetti fondamentali:
- Il codice deve essere utile, avere valore. Se non ha valore ci sarà poco interesse e quindi poca volontà di modificarlo.
- Deve generare paura di modificarlo, perché potrebbero introdurre nuovi bug o rompere parti di codice con dipendenze nascoste.
La facilità di commettere errori aumenta quando:
- Il codice non è coperto da test.
- Il codice è poco pulito; non è rispettato il principio di singola responsabilità.
- Il codice è mal pensato o è diventato mal strutturato nel tempo: modificare un pezzo di codice può comportare molti side effects.
- Non si ha a disposizione il tempo necessario per avere una conoscenza approfondita di quello che si sta modificando.
Un’arma che abbiamo a disposizione come sviluppatori è quella dei test. I test ci forniscono sicurezza sul risultato e ci danno un modo per rilevare velocemente eventuali errori commessi. Ma come possiamo testare codice che non conosciamo? Realizzare una suite di Unit Test ci fornirebbe un’approfondita conoscenza del progetto, ma avrebbe anche un costo elevato. Se però non possiamo testare i dettagli, possiamo strutturare dei Characterization Test, ovvero dei test che descrivono il comportamento di un pezzo di software.
Un pattern che ci viene in aiuto in queste situazioni è chiamato Golden Master Pattern. L’idea alla base è molto semplice: non potendo entrare nei dettagli, bisogna provare a fotografare l’intera esecuzione. Catturiamo l’output (stdout, immagini, file di log, ecc.) di un’esecuzione corretta e questo sarà il nostro Golden Master, che useremo come output atteso di una esecuzione corretta del codice. Se l’output dell’esecuzione attuale corrisponde, possiamo essere fiduciosi che le modifiche non avranno introdotto errori.
Per mostrare l’applicazione del Golden Master Pattern, partiamo da un esempio (il codice completo lo trovate qui). La nostra compagnia sviluppa giochi per command line, tra cui il gioco del Tris (l’implementazione è presa da qui), di cui ci viene chiesta una modifica per ridimensionare a piacere la griglia di gioco. Diamo uno sguardo al codice:
namespace Tris
{
public class Game
{
//making array and
//by default I am providing 0-9 where no use of zero
static char[] arr = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static int player = 1; //By default player 1 is set
static int choice; //This holds the choice at which position user want to mark
// The flag variable checks who has won if its value is 1 then someone has won the match if -1 then Match has Draw if 0 then match is still running
static int flag = 0;
public static void run()
{
do
{
Console.Clear();// whenever loop will be again start then screen will be clear
Console.WriteLine("Player1:X and Player2:O");
Console.WriteLine("\n");
if (player % 2 == 0)//checking the chance of the player
{
Console.WriteLine("Player 2 Chance");
}
else
{
Console.WriteLine("Player 1 Chance");
}
Console.WriteLine("\n");
Board();// calling the board Function
choice = int.Parse(Console.ReadLine());//Taking users choice
// checking that position where user want to run is marked (with X or O) or not
if (arr[choice] != 'X' && arr[choice] != 'O')
{
if (player % 2 == 0) //if chance is of player 2 then mark O else mark X
{
arr[choice] = 'O';
player++;
}
else
{
arr[choice] = 'X';
player++;
}
}
else //If there is any position where user wants to run and that is already marked then show message and load board again
{
Console.WriteLine("Sorry the row {0} is already marked with {1}", choice, arr[choice]);
Console.WriteLine("\n");
Console.WriteLine("Please wait 2 second board is loading again.....");
Thread.Sleep(2000);
}
flag = CheckWin();// calling of check win
} while (flag != 1 && flag != -1);// This loof will be run until all cell of the grid is not marked with X and O or some player is not winner
Console.Clear();// clearing the console
Board();// getting filled board again
if (flag == 1)// if flag value is 1 then someone has win or means who played marked last time which has win
{
Console.WriteLine("Player {0} has won", (player % 2) + 1);
}
else// if flag value is -1 the match will be drawn and no one is the winner
{
Console.WriteLine("Draw");
}
Console.ReadLine();
}
// Board method which creats board
private static void Board()
{
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[1], arr[2], arr[3]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[4], arr[5], arr[6]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[7], arr[8], arr[9]);
Console.WriteLine(" | | ");
}
private static int CheckWin()
{
#region Horzontal Winning Condtion
//Winning Condition For First Row
if (arr[1] == arr[2] && arr[2] == arr[3])
{
return 1;
}
//Winning Condition For Second Row
else if (arr[4] == arr[5] && arr[5] == arr[6])
{
return 1;
}
//Winning Condition For Third Row
else if (arr[6] == arr[7] && arr[7] == arr[8])
{
return 1;
}
#endregion
#region vertical Winning Condtion
//Winning Condition For First Column
else if (arr[1] == arr[4] && arr[4] == arr[7])
{
return 1;
}
//Winning Condition For Second Column
else if (arr[2] == arr[5] && arr[5] == arr[8])
{
return 1;
}
//Winning Condition For Third Column
else if (arr[3] == arr[6] && arr[6] == arr[9])
{
return 1;
}
#endregion
#region Diagonal Winning Condition
else if (arr[1] == arr[5] && arr[5] == arr[9])
{
return 1;
}
else if (arr[3] == arr[5] && arr[5] == arr[7])
{
return 1;
}
#endregion
#region Checking For Draw
// If all the cells or values filled with X or O then any player has won the match
else if (arr[1] != '1' && arr[2] != '2' && arr[3] != '3' && arr[4] != '4' && arr[5] != '5' && arr[6] != '6' && arr[7] != '7' && arr[8] != '8' && arr[9] != '9')
{
return -1;
}
#endregion
else
{
return 0;
}
}
}
}
Ad una lettura veloce, il codice risulta confuso, le responsabilità non sono ben separate e i nomi delle variabili sono poco significativi. Dopo una lettura più approfondita, si riesce a vedere che la griglia di gioco viene memorizzata in un `static char[] arr’. Aggiungere semplicemente elementi all’array non è efficace perché nelle funzioni PrintBoard e CheckWin vi si accede con indici costanti. Capiamo quindi che, per ridimensionare la griglia, dobbiamo modificare in più parti il codice.
Creiamo un progetto e lanciamo il nostro gioco:
class Program
{
static void Main(string[] args)
{
Game.run();
}
}
Dopo aver stampato la board, il gioco resta in attesa dell’input del primo giocatore. Possiamo automatizzare l’input facendolo leggere da file.
class Program
{
private const string InputPath = "input.txt";
public static void Main(string[] args)
{
var input = new StreamReader(new FileStream(InputPath, FileMode.Open));
Console.SetIn(input);
Game.run();
input.Close();
}
}
L’insieme di tutti i possibili input è troppo ampio per un testing brute-force. Quello che possiamo fare è campionare i valori considerando i possibili risultati di una partita di Tris:
- Vittoria giocatore 1
- Vittoria giocatore 2
- Pareggio
Selezioniamo quindi il set minimo di test per coprire tutti i casi, scrivendo i tre path in file di testo nella cartella input e raccogliendo i risultati nelle cartella goldenMaster:
class Program
{
private const string InputFolderPath = "input/";
private const string OutputFolderPath = "goldenMaster/";
public static void Main(string[] args)
{
int i = 1;
foreach (var filePath in Directory.GetFiles(InputFolderPath)) {
var input = new StreamReader(new FileStream(filePath, FileMode.Open));
var output = new StreamWriter(new FileStream(OutputFolderPath + "output" + i.ToString() + ".txt" , FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);
Game.run();
input.Close();
output.Close();
i++;
}
}
}
I tre file rappresentano i nostri Golden Master, da cui possiamo sviluppare dei Characterization Test:
[Test]
public void WinPlayerOne()
{
inputPath = InputFolderPath + "input1.txt";
outputPath = OutputFolderPath + "output.txt";
var goldenMasterOutput = GoldenMasterOutput + "output1.txt";
var input = new StreamReader(new FileStream(inputPath, FileMode.Open));
var output = new StreamWriter(new FileStream(outputPath, FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);
Game.run();
input.Close();
output.Close();
Assert.True(AreFileEquals(goldenMasterOutput, outputPath));
}
private bool AreFileEquals(string expectedPath, string actualPath)
{
byte[] bytes1 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(expectedPath)));
byte[] bytes2 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(actualPath)));
return bytes1.SequenceEqual(bytes2);
}
Finché i test sono verdi possiamo fare refactoring senza paura di rompere qualcosa. Un possibile risultato potrebbe essere il seguente:
public static void run()
{
char[] board = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
int actualPlayer = 1;
while (CheckWin(board) == 0)
{
PrintPlayerChoise(actualPlayer);
PrintBoard(board);
var choice = ReadPlayerChoise();
if (isBoardCellAlreadyTaken(board[choice]))
{
PrintCellIsAlreadyMarketMessage(board[choice], choice);
continue;
}
board[choice] = GetPlayerMarker(actualPlayer);
actualPlayer = UpdatePlayer(actualPlayer);
}
PrintResult(board, actualPlayer);
}
Da questo blocco emerge più chiaramente il concetto di Board e delle sue responsabilità. Proviamo a estrarne il comportamento in una nuova classe Board. La nostra nuova Board dovrebbe essere in grado di:
- Stampare la griglia.
- Segnare la scelta del giocatore.
- Controllare che ci sia un vincitore.
Sfruttiamo un approccio TDD (maggiori dettagli in questo articolo di Adolfo) per sviluppare una Board in grado di ridimensionarsi (trovate il codice dei test qui, mentre quello della classe qui) e andiamo a inserirla nel gioco, controllando che i Golden Master Test restino verdi:
private const int Boardsize = 3;
public static void run()
{
Board board = new Board(Boardsize);
int actualPlayer = 1;
while (board.CheckWin() == -1)
{
PrintPlayerChoise(actualPlayer);
Console.WriteLine(board.Print());
var choice = ReadPlayerChoise();
if (!board.UpdateBoard(actualPlayer, choice))
{
PrintCellIsAlreadyMarketMessage(board.GetCellValue(choice), choice);
continue;
}
actualPlayer = UpdatePlayer(actualPlayer);
}
PrintResult(board, actualPlayer);
}
A questo punto possiamo ripristinare lo stdin/stdout e leggere la dimensione della griglia dall’utente:
class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Insert Diagonal dimension of Board: ");
var boardSize = int.Parse(Console.ReadLine());
Game.run(boardSize);
}
}
Come avete visto, grazie al Golden Master Pattern siamo riusciti a dominare il codice legacy e abbiamo potuto fare refactoring senza paure. Ma non è tutto oro quel che luccica: applicare il Golden Master può risultare ostico in caso di “noise output”, l’output, cioè, che è inutile ai fini dell’esecuzione e che varia nel tempo (es. timestamp, thread name, ecc.). In questi casi bisogna filtrare l’output e considerare solo la parte significativa.
Spero possa tornarvi utile la prossima volta che erediterete un progetto legacy: in fondo ci fa paura solo quello che non possiamo controllare!
Alla prossima