Nacalculatie van oude code deel 1 - De gouden meester

Oude code. Lelijke code. Ingewikkelde code. Spaghetti code. Jibberistische onzin. In twee woorden, Oude code. Dit is een serie die u zal helpen om ermee te werken en ermee om te gaan.

In een ideale wereld zou je alleen nieuwe code schrijven. Je zou het mooi en perfect schrijven. Je zou nooit je code opnieuw hoeven te bekijken en je zult nooit projecten van tien jaar hoeven te onderhouden. In een ideale wereld…

Helaas leven we in een realiteit die niet ideaal is. We moeten eeuwenoude code begrijpen, wijzigen en verbeteren. We moeten werken met oude code. Dus waar wacht je op? Laten we onze kop opsteken in deze eerste tutorial, de code krijgen, het een beetje begrijpen en een vangnet creëren voor onze toekomstige wijzigingen.

Definitie van oude code

Legacy-code is op zoveel manieren gedefinieerd dat het onmogelijk is om er één algemene definitie voor te vinden. De paar voorbeelden aan het begin van deze tutorial zijn slechts het topje van de ijsberg. Dus ik zal je geen officiële definitie geven. In plaats daarvan citeer ik je mijn favoriete exemplaar.

Naar mij, oude code is gewoon code zonder tests. ~ Michael Feathers

Welnu, dat is de eerste formele definitie van de uitdrukking oude code, gepubliceerd door Michael Feathers in zijn boek Effectief werken met oude code. Natuurlijk, de industrie gebruikte de uitdrukking al eeuwen, eigenlijk voor elke code die moeilijk te veranderen is. Deze definitie heeft echter iets anders te vertellen. Het verklaart het probleem heel duidelijk, zodat de oplossing duidelijk wordt. "Moeilijk om te veranderen" is zo vaag. Wat moeten we doen om het gemakkelijk te veranderen? We hebben geen idee! "Code zonder tests" is daarentegen heel concreet. En het antwoord op onze vorige vraag is eenvoudig, maak code testbaar en test het. Dus laten we beginnen.

Onze oude code verkrijgen

Deze serie is gebaseerd op het uitzonderlijke Trivia-spel van J.B. Rainsberger, ontworpen voor Legacy Code Retreat-evenementen. Het is gemaakt om echte legacy code te zijn en om ook kansen te bieden voor een breed scala aan refactoring, met een behoorlijke moeilijkheidsgraad..

Bekijk de broncode

Het Trivia-spel wordt gehost op GitHub en het is GPLv3-gelicentieerd, zodat je er vrij mee kunt spelen. We beginnen deze serie door de officiële repository te bekijken. De code is ook bijgevoegd bij deze tutorial met alle wijzigingen die we zullen aanbrengen, dus als je op een gegeven moment in de war raakt, kun je een voorproefje nemen van het eindresultaat.

 $ git clone https://github.com/jbrains/trivia.git Klonen in 'trivia' ... remote: Objecten tellen: 429, klaar. afstandsbediening: objecten comprimeren: 100% (262/262), gereed. remote: Total 429 (delta 100), hergebruikt 419 (delta 93) Ontvangende objecten: 100% (429/429), 848.33 KiB | 305.00 KiB / s, klaar. Het oplossen van delta's: 100% (100/100), klaar. Verbinding controleren ... klaar.

Wanneer u de trivia map ziet u onze code in verschillende programmeertalen. We werken in PHP, maar je bent vrij om je favoriete te kiezen en de hier gepresenteerde technieken toe te passen.

De code begrijpen

Per definitie is legacy code moeilijk te begrijpen, vooral als we niet eens weten wat het moet doen. Dus de eerste stap is om de code uit te voeren en een redenering te maken, waar het om gaat.

We hebben twee bestanden in onze directory.

$ cd php / $ ls -al totaal 20 drwxr-xr-x 2 csaba csaba 4096 10 maart 21:05. drwxr-xr-x 26 csaba csaba 4096 10 maart: 05 ... -rw-r - r - 1 csaba csaba 5568 10 maart 21:05 Game.php -rw-r - r - 1 csaba csaba 410 mrt 10 21:05 GameRunner.php

GameRunner.php lijkt een goede kandidaat te zijn voor onze poging om de code uit te voeren.

$ php ./GameRunner.php Chet is toegevoegd. Ze zijn speler nummer 1 Pat is toegevoegd. Zij zijn speler nummer 2 Sue is toegevoegd. Ze zijn speler nummer 3 Chet is de huidige speler Ze hebben een 4 chet gerold. De nieuwe locatie is 4 De categorie is Pop Pop vraag 0 Antwoord was corrent !!!! Chet heeft nu 1 gouden munten. Pat is de huidige speler Ze hebben een 2 Pat gerold. De nieuwe locatie is 2 De categorie is Sport Sportvraag 0 Antwoord was corrent !!!! Pat heeft nu 1 gouden munten. Sue is de huidige speler Ze hebben een 1 Sue's nieuwe locatie gerold is 1 De categorie is Science Science Vraag 0 Antwoord was corrent !!!! Sue heeft nu 1 gouden munten. Chet is de huidige speler Ze hebben een 4 gerold ## Sommige regels zijn verwijderd om ## de tutorial op een redelijk formaat te houden Antwoord was corrent !!!! Sue heeft nu 5 gouden munten. Chet is de huidige speler Ze hebben een 3 chet gerold komt uit de strafschopkast. De nieuwe locatie van Chet is 11 De categorie is Rock Rock Vraag 5 Antwoord was correct !!!! Chet heeft nu 5 gouden munten. Pat is de huidige speler Ze hebben een nieuwe locatie van een Pat gerold. De categorie is Sport Sport Vraag 1 Antwoord was corrent !!!! Pat heeft nu 6 gouden munten.

OK. Onze gissing was correct. Onze code liep en produceerde wat output. Als we deze uitvoer analyseren, kunnen we een basisidee afleiden over wat de code doet.

  1. We weten dat het een Trivia-spel is. We wisten het toen we de broncode controleerden.
  2. Ons voorbeeld heeft drie spelers: Chet, Pat en Sue.
  3. Er is een soort van rollen van een dobbelsteen of een soortgelijk concept.
  4. Er is een huidige locatie voor een speler. Mogelijk op een soort van bord?
  5. Er zijn verschillende categorieën waaruit vragen worden gesteld.
  6. Gebruikers beantwoorden vragen.
  7. Juiste antwoorden geven spelers goud.
  8. Verkeerde antwoorden sturen spelers naar de strafschopsteen.
  9. Spelers kunnen uit de strafschopkast komen, gebaseerd op een niet geheel duidelijke logica.
  10. Het lijkt erop dat de gebruiker die als eerste zes gouden munten bereikt, wint.

Dat is veel kennis. We konden het grootste deel van het basisgedrag van de toepassing achterhalen door gewoon naar de uitvoer te kijken. In real-life-toepassingen is de uitvoer mogelijk geen tekst op het scherm, maar het kan een webpagina, een foutenlogboek, een database, een netwerkcommunicatie, een dumpbestand, enzovoort zijn. In andere gevallen kan de module die u moet wijzigen niet op zichzelf worden uitgevoerd. Als dit het geval is, moet je hem door andere modules van de grotere applicatie leiden. Probeer alleen het minimum toe te voegen om een ​​redelijke output te krijgen van uw oude code.

De code scannen

Nu we een idee hebben over wat de code uitvoert, kunnen we ernaar kijken. We beginnen met de hardloper.

The Game Runner

Ik begin graag met het uitvoeren van alle code via de formatter van mijn IDE. Dit verbetert de leesbaarheid aanzienlijk door de vorm van de code bekend te maken met wat ik gewend ben. Dus dit:

... zal dit worden:

... wat iets beter is. Het is misschien geen groot verschil met deze kleine hoeveelheid code, maar het zal in ons volgende bestand staan.

Kijkend naar onze GameRunner.php bestand, kunnen we eenvoudig een aantal belangrijke aspecten identificeren die we in de uitvoer hebben waargenomen. We kunnen de regels zien die de gebruikers toevoegen (9-11), dat een methode roll () wordt aangeroepen en een winnaar wordt geselecteerd. Natuurlijk zijn dit verre van de innerlijke geheimen van de logica van het spel, maar we zouden in ieder geval kunnen beginnen met het identificeren van de belangrijkste methoden die ons helpen de rest van de code te ontdekken.

Het spelbestand

We zouden dezelfde opmaak op de Game.php bestand ook.

Dit bestand is veel groter; Ongeveer 200 regels code. De meeste methoden hebben de juiste afmetingen, maar sommige zijn vrij groot en na het formatteren kunnen we zien dat op twee plaatsen de code-inspringing verder gaat dan vier niveaus. Hoge inspringniveaus zijn meestal veel complexe beslissingen, dus voorlopig kunnen we aannemen dat die punten in onze code complexer en verstandiger zijn om te veranderen.

De gouden meester

En de gedachte aan verandering leidt ons naar ons gebrek aan tests. De methoden die we hebben gezien Game.php zijn vrij complex. Maak je geen zorgen als je ze niet begrijpt. Op dit punt zijn ze ook een mysterie voor mij. Oude code is een mysterie dat we moeten oplossen en begrijpen. We hebben onze eerste stap gezet om het te begrijpen en het is nu tijd voor onze tweede.

Dus wat is deze gouden meester?

Wanneer u met oude code werkt, is het bijna onmogelijk om het te begrijpen en om code te schrijven die zeker alle logische paden door de code zal gebruiken. Voor dat soort testen zouden we de code moeten begrijpen, maar dat doen we nog niet. We moeten dus een andere aanpak kiezen.

In plaats van te proberen te achterhalen wat we moeten testen, kunnen we alles vaak testen, zodat we uiteindelijk een enorme hoeveelheid output krijgen, waarvan we bijna zeker kunnen aannemen dat het is geproduceerd door alle delen van ons erfgoed uit te oefenen code. Het wordt aanbevolen om de code ten minste 10.000 (tienduizend) keer uit te voeren. We zullen een test schrijven om het tweemaal uit te voeren en de output te bewaren.

De Golden Master Generator schrijven

We kunnen vooruit denken en beginnen met het maken van een generator en een test als afzonderlijke bestanden voor toekomstige tests, maar is dit echt nodig? Dat weten we nog niet zeker. Dus waarom niet beginnen met een eenvoudig testbestand dat onze code één keer zal uitvoeren en onze logica van daaruit opbouwen.

Je vindt het in het bijgevoegde codearchief in de bron map maar buiten de trivia map onze Test map. In deze map maken we een bestand: GoldenMasterTest.php.

class GoldenMasterTest breidt PHPUnit_Framework_TestCase uit function testGenerateOutput () ob_start (); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

We kunnen dit op verschillende manieren doen. We kunnen bijvoorbeeld onze code van de console halen en de uitvoer omleiden naar een bestand. Het hebben van een test die gemakkelijk in onze IDE wordt uitgevoerd, is echter een voordeel dat we niet moeten negeren.

De code is vrij eenvoudig, het buffert de uitvoer en stopt deze in de $ uitgang variabel. De eenmalig benodigd() zal ook alle code binnen het opgenomen bestand uitvoeren. In onze var-dump zullen we een aantal al vertrouwde uitvoer zien.

Bij een tweede run kunnen we echter iets vreemds waarnemen:

... de outputs verschillen. Ook al hebben we dezelfde code uitgevoerd, de uitvoer is anders. De opgerolde nummers zijn anders, de posities van de spelers zijn anders.

Zaaien van de willekeurige generator

doe $ aGame-> roll (rand (0, 5) + 1); if (rand (0, 9) == 7) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ a Spel-> wasCorrectlyAnswered ();  while ($ notAWinner);

Door de essentiële code van de hardloper te analyseren, kunnen we zien dat deze een functie gebruikt rand() om willekeurige getallen te genereren. Onze volgende stop is de officiële PHP-documentatie om dit te onderzoeken rand() functie.

De generator voor willekeurige getallen wordt automatisch geplaatst.

De documentatie vertelt ons dat seeding automatisch gebeurt. Nu hebben we een andere taak. We moeten een manier vinden om het zaad te beheersen. De srand () functie kan daarbij helpen. Hier is de definitie uit de documentatie.

Zaad de generator van willekeurige getallen met zaad of met een willekeurige waarde als er geen zaad wordt gegeven.

Het vertelt ons dat als we dit uitvoeren vóór een oproep naar rand(), we moeten altijd eindigen met dezelfde resultaten.

function testGenerateOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

We zetten srand (1) voor onze eenmalig benodigd(). Nu is de uitvoer altijd hetzelfde.

Zet de uitvoer in een bestand

klasse GoldenMasterTest breidt PHPUnit_Framework_TestCase uit function testGenerateOutput () file_put_contents ('/ tmp / gm.txt', $ this-> generateOutput ()); $ file_content = file_get_contents ('/ tmp / gm.txt'); $ this-> assertEquals ($ file_content, $ this-> generateOutput ());  private function generateOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output; 

Deze verandering ziet er redelijk uit. Rechts? We hebben de codegeneratie in een methode geëxtraheerd, twee keer uitgevoerd en verwacht dat de uitvoer gelijk is. Maar dat zullen ze niet zijn.

De reden is dat eenmalig benodigd() zal niet hetzelfde bestand tweemaal nodig hebben. De tweede oproep aan de generateOutput () methode zal een lege string produceren. Wat kunnen we doen? Wat als we simpelweg vereisen()? Dat zou elke keer moeten gebeuren.

Welnu, dat leidt tot een ander probleem: "Can not redeclare echoln ()". Maar waar komt dat vandaan? Het is juist aan het begin van de Game.php het dossier. De reden waarom deze fout optreedt, is omdat in GameRunner.php wij hebben include __DIR__. '/Game.php';, die probeert het spelbestand twee keer op te nemen, telkens wanneer we het generateOutput () methode.

include_once __DIR__. '/Game.php';

Gebruik makend van include_once in GameRunner.php zal ons probleem oplossen. Ja, we moesten wijzigen GameRunner.php zonder er nog tests voor te hebben! We kunnen echter 99% zeker zijn dat onze wijziging de code zelf niet zal overtreden. Het is een kleine en eenvoudige verandering om ons niet erg bang te maken. En het belangrijkste is dat de tests slagen.

Draai het meerdere keren

Nu we code hebben die we vele keren kunnen uitvoeren, is het tijd om wat output te genereren.

function testGenerateOutput () $ this-> generateMany (20, '/tmp/gm.txt'); $ this-> generMany (20, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2);  private function generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ()); $ first = false;  else file_put_contents ($ fileName, $ this-> generateOutput (), FILE_APPEND);  $ keer--; 

We hebben hier een andere methode geëxtraheerd: generateMany (). Het heeft twee parameters. Eén voor het aantal keren dat we onze generator willen gebruiken, de andere is een doelbestand. Het zal de gegenereerde uitvoer in de bestanden plaatsen. Bij de eerste run worden de bestanden leeggemaakt en voor de rest van de iteraties worden de gegevens toegevoegd. U kunt in het bestand kijken om de gegenereerde uitvoer 20 keer te bekijken.

Maar wacht! Dezelfde speler wint elke keer? Is dat mogelijk?

cat /tmp/gm.txt | grep "heeft 6 gouden munten." Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten.

Ja! Het is mogelijk! Het is meer dan mogelijk. Het is zeker. We hebben hetzelfde zaadje voor onze willekeurige functie. We spelen hetzelfde spel steeds opnieuw.

Elke keer anders lopen

We moeten verschillende spellen spelen, anders is het vrijwel zeker dat slechts een klein deel van onze oude code steeds opnieuw wordt uitgeoefend. De omvang van de gouden meester is om zoveel mogelijk te oefenen. We moeten de willekeurige generator elke keer opnieuw zaaien, maar op een gecontroleerde manier. Een optie is om onze teller als de seed-waarde te gebruiken.

private function generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ($ times)); $ first = false;  else file_put_contents ($ fileName, $ this-> generateOutput ($ times), FILE_APPEND);  $ keer--;  private function generateOutput ($ seed) ob_start (); srand ($ zaad); vereisen __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output; 

Hierdoor blijft onze test passeren, dus we zijn er zeker van dat we elke keer dezelfde volledige uitvoer genereren, terwijl de uitvoer een ander spel speelt voor elke iteratie.

cat /tmp/gm.txt | grep "heeft 6 gouden munten." Sue heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Pat heeft nu 6 gouden munten. Pat heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Sue heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Sue heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Sue heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Pat heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten. Chet heeft nu 6 gouden munten.

Er zijn verschillende winnaars voor het spel op een willekeurige manier. Dit ziet er goed uit.

Krijg tot 20.000

Het eerste dat u kunt proberen is om onze code uit te voeren voor 20.000 game-iteraties.

function testGenerateOutput () $ times = 20000; $ this-> produceMany ($ times, '/tmp/gm.txt'); $ this-> produceMany ($ times, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2); 

Dit zal bijna werken. Er worden twee 55MB-bestanden gegenereerd.

ls-alh / tmp / gm * -rw-r - r-- 1 csaba csaba 55M 14 maart 20:38 /tmp/gm2.txt -rw-r - r - 1 csaba csaba 55M 14 mrt 20:38 /tmp/gm.txt

Aan de andere kant mislukt de test met een onvoldoende geheugenfout. Het maakt niet uit hoeveel RAM je hebt, dit zal mislukken. Ik heb 8GB plus een 4GB-swap en het mislukt. De twee snaren zijn gewoon te groot om te vergelijken in onze bewering.

Met andere woorden, we genereren goede bestanden, maar PHPUnit kan ze niet vergelijken. We hebben een work-around nodig.

$ this-> assertFileEquals ('/ tmp / gm.txt', '/tmp/gm2.txt');

Dat lijkt een goede kandidaat, maar het faalt nog steeds. Wat jammer. We moeten de situatie verder onderzoeken.

$ this-> assertTrue ($ file_content_gm == $ file_content_gm2);

Dit werkt echter.

Het kan de twee strings vergelijken en falen als ze anders zijn. Het heeft echter een kleine prijs. Het zal niet precies kunnen vertellen wat er mis is als de snaren verschillen. Het zal gewoon zeggen "Mislukt dat valse beweringen waar zijn.". Maar we zullen dit in een komende tutorial behandelen.

Laatste gedachten

We zijn klaar voor deze tutorial. We hebben heel veel geleerd van onze eerste les en we zijn goed begonnen aan ons toekomstige werk. We hebben de code gehaald, we hebben deze op verschillende manieren geanalyseerd en we begrepen vooral de essentiële logica ervan. Vervolgens hebben we een reeks tests gemaakt om ervoor te zorgen dat deze zo veel mogelijk wordt uitgeoefend. Ja. De tests zijn erg traag. Het kost ze 24 seconden op mijn Core i7 CPU om de output twee keer te genereren. Gelukkig zullen we in onze toekomstige ontwikkeling de gm.txt bestand onaangeroerd en genereer slechts één keer per run een andere. Maar 12 seconden is nog steeds een enorme hoeveelheid tijd voor zo'n kleine codebasis.

Tegen de tijd dat we deze serie voltooien, zouden onze tests in minder dan een seconde moeten worden uitgevoerd en alle code correct moeten testen. Dus blijf op de hoogte voor onze volgende tutorial wanneer we problemen zoals magische constanten, magische snaren en complexe conditionals zullen aanpakken. Bedankt voor het lezen.