Refactoring des Legacy-Codes Teil 1 - Der goldene Meister

Alter Code. Hässlicher Code. Komplizierter Code Spaghetti-Code. Jibberish Blödsinn. In zwei Worten, Legacy Code. Diese Serie hilft Ihnen bei der Arbeit und beim Umgang damit.

In einer idealen Welt würden Sie nur neuen Code schreiben. Sie würden es schön und perfekt schreiben. Sie müssen Ihren Code niemals erneut aufrufen und Projekte, die zehn Jahre alt sind, müssen nicht gepflegt werden. In einer idealen Welt…

Leider leben wir in einer Realität, die nicht ideal ist. Wir müssen den Code der Jahrhunderte verstehen, modifizieren und verbessern. Wir müssen mit altem Code arbeiten. Also, worauf wartest Du? Lassen Sie uns in dieses erste Tutorial einsteigen, den Code erhalten, ein wenig verstehen und ein Sicherheitsnetz für unsere zukünftigen Modifikationen erstellen.

Definition des alten Codes

Legacy-Code wurde auf so viele Arten definiert, dass es unmöglich ist, eine einzige, allgemein akzeptierte Definition dafür zu finden. Die wenigen Beispiele zu Beginn dieses Tutorials sind nur die Spitze des Eisbergs. Ich gebe Ihnen also keine offizielle Definition. Stattdessen zitiere ich Sie zu meinen Favoriten.

Mir, Legacy-Code ist einfach Code ohne Tests. ~ Michael Federn

Nun, das ist die erste formale Definition des Ausdrucks Legacy-Code, von Michael Feathers in seinem Buch Working Effectively with Legacy Code veröffentlicht. Natürlich hat die Industrie den Ausdruck schon seit Ewigkeiten verwendet, im Wesentlichen für jeden Code, der schwer zu ändern ist. Diese Definition hat jedoch etwas anderes zu sagen. Es erklärt das Problem sehr klar, so dass die Lösung offensichtlich wird. "Schwer zu ändern" ist so vage. Was müssen wir tun, um die Änderung zu erleichtern? Wir haben keine Ahnung! "Code ohne Tests" ist dagegen sehr konkret. Und die Antwort auf unsere vorherige Frage ist einfach: Code überprüfbar machen und testen. Also lasst uns anfangen.

Bekommen Sie unseren Legacy Code

Diese Serie basiert auf dem außergewöhnlichen Trivia Game von J. B. Rainsberger, das für Legacy Code Retreat-Events entwickelt wurde. Es ist wie ein echter Legacy-Code und bietet auch Möglichkeiten für eine Vielzahl von Refactorings auf einem anständigen Schwierigkeitsgrad.

Check out den Quellcode

Das Trivia-Spiel wird auf GitHub gehostet und ist GPLv3-lizenziert, sodass Sie frei damit herumspielen können. Wir werden diese Serie mit dem offiziellen Repository beginnen. Der Code ist auch an dieses Tutorial mit all den Modifikationen angehängt, die wir vornehmen werden. Wenn Sie also einmal verwirrt werden, können Sie einen kurzen Blick auf das Endergebnis werfen.

 $ git clone https://github.com/jbrains/trivia.git In 'Kleinigkeiten' klonen ... remote: Objekte zählen: 429, fertig. Remote: Komprimieren von Objekten: 100% (262/262), fertig. Remote: Gesamt 429 (Delta 100), wiederverwendet 419 (Delta 93) Empfangsobjekte: 100% (429/429), 848,33 KiB | 305,00 KiB / s, fertig. Auflösen von Deltas: 100% (100/100), fertig. Konnektivität prüfen… fertig.

Wenn Sie das öffnen Kleinigkeiten In unserem Verzeichnis sehen Sie unseren Code in mehreren Programmiersprachen. Wir werden in PHP arbeiten, aber Sie können Ihren Liebling auswählen und die hier vorgestellten Techniken anwenden.

Den Code verstehen

Legacy-Code ist per Definition schwer zu verstehen, insbesondere wenn wir nicht einmal wissen, was er tun soll. Der erste Schritt ist also, den Code auszuführen und eine Art Argumentation zu machen, worum es geht.

Wir haben zwei Dateien in unserem Verzeichnis.

$ cd php / $ ls - insgesamt 20 drwxr-xr-x 2 csaba csaba 4096 Mar 10 21:05. drwxr-xr-x 26 csaba csaba 4096 Mar 10 21: 05… -rw-r - r-- 1 csaba csaba 5568 Mar 10 21:05 Spiel.php -rw-r - r-- 1 csaba csaba 410 Mar 10 21:05 GameRunner.php

GameRunner.php scheint ein guter Kandidat für unseren Versuch zu sein, den Code auszuführen.

$ php ./GameRunner.php Chet wurde hinzugefügt Sie sind Spielernummer 1 Pat wurde hinzugefügt. Sie sind Spielernummer 2. Sue wurde hinzugefügt. Sie sind Spielernummer 3. Chet ist der derzeitige Spieler. Sie haben eine 4 gewürfelt. Chets neuer Standort ist 4 Pop-Frage 0 Antwort war korrent !!!! Chet hat jetzt 1 Goldmünzen. Pat ist der aktuelle Spieler. Sie haben eine 2 gewürfelt. Pat's neuer Standort ist 2. Die Kategorie ist Sport. Sport Frage 0 Die Antwort war korrent !!!! Pat hat jetzt 1 Goldmünzen. Sue ist der aktuelle Spieler. Sie haben eine 1 gewürfelt. Der neue Standort von Sue ist 1. Die Kategorie ist Wissenschaft Wissenschaft Frage 0 Antwort war korrent !!!! Sue hat jetzt 1 Goldmünzen. Chet ist der aktuelle Spieler. Sie haben eine 4 gewürfelt. Einige Zeilen entfernt, um das Tutorial auf einer angemessenen Größe zu halten. Antwort war korrent !!!! Sue hat jetzt 5 Goldmünzen. Chet ist der aktuelle Spieler. Sie haben eine 3 gewürfelt. Chet kommt aus der Strafbox. Chets neuer Standort ist 11. Die Kategorie ist Rock Rock Frage 5 Die Antwort war richtig !!!! Chet hat jetzt 5 Goldmünzen. Pat ist der aktuelle Spieler Sie haben eine 1 gewürfelt Pat ist der neue Standort 10 Die Kategorie ist Sport Sport Frage 1 Die Antwort war korrent !!!! Pat hat jetzt 6 Goldmünzen.

OK. Unsere Vermutung war richtig. Unser Code lief und lieferte einige Ausgaben. Durch die Analyse dieser Ausgabe können wir einige grundlegende Ideen über die Funktionsweise des Codes ableiten.

  1. Wir wissen, dass es ein Trivia-Spiel ist. Wir wussten es, als wir den Quellcode auscheckten.
  2. Unser Beispiel hat drei Spieler: Chet, Pat und Sue.
  3. Es gibt eine Art Würfeln oder ein ähnliches Konzept.
  4. Es gibt einen aktuellen Standort für einen Spieler. Möglicherweise auf einer Art Board?
  5. Es gibt verschiedene Kategorien, aus denen Fragen gestellt werden.
  6. Benutzer beantworten Fragen.
  7. Richtige Antworten geben den Spielern Gold.
  8. Falsche Antworten schicken die Spieler zur Strafbox.
  9. Spieler können die Strafbox verlassen, basierend auf einer nicht ganz klaren Logik.
  10. Es scheint, als ob der Benutzer, der zuerst sechs Goldmünzen erreicht, gewinnt.

Das ist viel Wissen. Das grundlegende Verhalten der Anwendung können Sie durch einen Blick auf die Ausgabe ermitteln. In realen Anwendungen kann es sich bei der Ausgabe nicht um Text auf dem Bildschirm handeln, sondern es kann sich um eine Webseite, ein Fehlerprotokoll, eine Datenbank, eine Netzwerkkommunikation, eine Speicherauszugsdatei usw. handeln. In anderen Fällen kann das zu ändernde Modul nicht isoliert ausgeführt werden. Wenn ja, müssen Sie es durch andere Module der größeren Anwendung laufen lassen. Versuchen Sie einfach, das Minimum hinzuzufügen, um eine vernünftige Ausgabe Ihres alten Codes zu erhalten.

Code scannen

Nun, da wir eine Vorstellung davon haben, was der Code ausgibt, können wir uns damit beschäftigen. Wir werden mit dem Läufer beginnen.

Der Game Runner

Ich beginne gern damit, den gesamten Code durch den Formatierer meiner IDE auszuführen. Dies verbessert die Lesbarkeit erheblich, indem die Form des Codes mit dem vertraut gemacht wird, was ich gewohnt bin. Also das:

… Wird so werden:

… Was etwas besser ist. Es ist kein großer Unterschied mit dieser kleinen Menge Code, aber es wird in unserer nächsten Datei sein.

Blick auf unsere GameRunner.php Datei können wir einige wichtige Aspekte, die wir in der Ausgabe beobachtet haben, leicht identifizieren. Wir können die Zeilen sehen, die die Benutzer hinzufügen (9-11), dass eine roll () -Methode aufgerufen wird und ein Gewinner ausgewählt wird. Natürlich sind dies weit von den inneren Geheimnissen der Logik des Spiels entfernt, aber zumindest könnten wir zunächst Schlüsselmethoden identifizieren, die uns helfen, den Rest des Codes zu entdecken.

Die Spieldatei

Wir sollten die gleiche Formatierung auf dem durchführen Game.php Datei auch.

Diese Datei ist viel größer; Ungefähr 200 Zeilen Code. Die meisten Methoden haben eine angemessene Größe, aber einige sind ziemlich groß und nach der Formatierung können wir feststellen, dass der Codeeinzug an zwei Stellen über vier Ebenen hinausgeht. Hohe Einrückungen bedeuten in der Regel viele komplexe Entscheidungen. Daher können wir im Moment davon ausgehen, dass diese Punkte in unserem Code komplexer und für Änderungen sinnvoller sind.

Der goldene Meister

Und der Gedanke an Veränderung führt uns zu fehlenden Tests. Die Methoden, die wir gesehen haben Game.php sind ziemlich komplex. Mach dir keine Sorgen, wenn du sie nicht verstehst. An diesem Punkt sind sie auch für mich ein Rätsel. Legacy-Code ist ein Rätsel, das wir lösen und verstehen müssen. Wir haben unseren ersten Schritt unternommen, um es zu verstehen, und jetzt ist es Zeit für unseren zweiten.

Was ist dieser goldene Meister??

Wenn Sie mit altem Code arbeiten, ist es fast unmöglich, ihn zu verstehen und Code zu schreiben, der sicher alle logischen Pfade durch den Code führt. Für diese Art von Tests müssten wir den Code verstehen, aber noch nicht. Wir müssen also einen anderen Ansatz wählen.

Anstatt zu versuchen herauszufinden, was getestet werden soll, können wir alles oftmals testen, sodass wir eine riesige Menge an Output erhalten, bei der wir fast sicher davon ausgehen können, dass er durch das Üben aller Teile unseres Erbes erzeugt wurde Code. Es wird empfohlen, den Code mindestens 10.000 (zehntausend) Mal auszuführen. Wir werden einen Test schreiben, um das Doppelte auszuführen und die Ausgabe zu speichern.

Den Golden Master Generator schreiben

Wir können vorausdenken und anfangen, einen Generator und einen Test als separate Dateien für zukünftige Tests zu erstellen, aber ist dies wirklich notwendig? Wir wissen das noch nicht mit Sicherheit. Warum also nicht einfach mit einer einfachen Testdatei beginnen, die unseren Code einmal ausführt und von dort aus unsere Logik aufbaut.

Sie finden das beigefügte Code-Archiv im Quelle Ordner aber außerhalb der Kleinigkeiten Ordner unser Prüfung Mappe. In diesem Ordner erstellen wir eine Datei: GoldenMasterTest.php.

class GoldenMasterTest erweitert PHPUnit_Framework_TestCase function testGenerateOutput () ob_start (); required_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Wir könnten dies auf viele Arten tun. Wir könnten beispielsweise unseren Code von der Konsole aus ausführen und die Ausgabe in eine Datei umleiten. Es ist jedoch ein Vorteil, den Test in einem Test durchzuführen, der problemlos in unserer IDE ausgeführt werden kann.

Der Code ist ziemlich einfach, er puffert die Ausgabe und gibt sie in die $ ausgabe Variable. Das einmalig benötigt() führt auch den gesamten Code in der enthaltenen Datei aus. In unserem Var-Dump sehen wir einige bereits bekannte Ausgaben.

Bei einem zweiten Durchlauf können wir jedoch etwas Ungewöhnliches beobachten:

… Die Ausgänge sind unterschiedlich. Obwohl wir denselben Code ausgeführt haben, ist die Ausgabe unterschiedlich. Die gerollten Zahlen sind unterschiedlich, die Positionen der Spieler sind unterschiedlich.

Zufallsgenerator aussäen

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

Durch die Analyse des wesentlichen Codes des Läufers können wir feststellen, dass er die Funktion verwendet rand () Zufallszahlen erzeugen. Unser nächster Halt ist die offizielle PHP-Dokumentation, um dies zu erforschen rand () Funktion.

Der Zufallszahlengenerator wird automatisch ausgesät.

Die Dokumentation sagt uns, dass das Seeding automatisch erfolgt. Jetzt haben wir eine andere Aufgabe. Wir müssen einen Weg finden, den Samen zu kontrollieren. Das srand () Funktion kann dabei helfen. Hier ist seine Definition aus der Dokumentation.

Setzt den Zufallszahlengenerator mit Seed oder mit einem Zufallswert, wenn kein Seed angegeben ist.

Es sagt uns, dass, wenn wir dies vor jedem Aufruf ausführen rand (), wir sollten immer mit den gleichen Ergebnissen enden.

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

Wir stellen srand (1) vor unserem einmalig benötigt(). Jetzt ist die Ausgabe immer gleich.

Legen Sie die Ausgabe in eine Datei

class GoldenMasterTest erweitert PHPUnit_Framework_TestCase 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 Funktion generateOutput () ob_start (); srand (1); required_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output; 

Diese Änderung sieht vernünftig aus. Recht? Wir haben die Codegenerierung in eine Methode extrahiert, zweimal ausgeführt und erwartet, dass die Ausgabe gleich ist. Sie werden jedoch nicht sein.

Der Grund ist, dass einmalig benötigt() wird nicht zweimal dieselbe Datei benötigen. Der zweite Anruf beim generateOutput () Die Methode erzeugt einen leeren String. Was können wir also tun? Was ist, wenn wir einfach benötigen()? Das sollte jedes Mal ausgeführt werden.

Nun, das führt zu einem anderen Problem: "Echoln () kann nicht neu deklariert werden". Aber woher kommt das? Es ist gleich am Anfang des Game.php Datei. Der Grund, warum dieser Fehler auftritt, ist in GameRunner.php wir haben Include __DIR__. '/Game.php';, die versucht, die Spieldatei zweimal aufzunehmen, jedes Mal, wenn wir die aufrufen generateOutput () Methode.

include_once __DIR__. '/Game.php';

Verwenden include_once im GameRunner.php wird unser Problem lösen. Ja, wir mussten modifizieren GameRunner.php noch ohne Tests dafür! Wir können jedoch zu 99% sicher sein, dass unsere Änderung den Code selbst nicht beschädigt. Es ist eine kleine und einfache Änderung, um uns nicht sehr zu erschrecken. Und vor allem macht es die Tests erfolgreich.

Führen Sie es mehrmals aus

Nun, da wir Code haben, den wir viele Male ausführen können, ist es an der Zeit, etwas Ausgabe zu generieren.

function testGenerateOutput () $ this-> generateMany (20, '/tmp/gm.txt'); $ this-> generateMany (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 Funktion generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ Dateiname, $ this-> generateOutput ()); $ first = false;  else file_put_contents ($ fileName, $ this-> generateOutput (), FILE_APPEND);  $ times--; 

Wir haben hier eine andere Methode extrahiert: generateMany (). Es hat zwei Parameter. Einer für die Anzahl, die wir unseren Generator ausführen möchten, der andere ist eine Zieldatei. Die erzeugte Ausgabe wird in die Dateien eingefügt. Beim ersten Durchlauf werden die Dateien geleert, und für den Rest der Iterationen werden die Daten angefügt. Sie können die erzeugte Ausgabe 20-mal in die Datei sehen.

Aber warte! Der gleiche Spieler gewinnt jedes Mal? Ist das möglich?

cat /tmp/gm.txt | grep "hat 6 Goldmünzen." Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen.

Ja! Es ist möglich! Es ist mehr als möglich. Es ist eine sichere Sache. Wir haben den gleichen Samen für unsere Zufallsfunktion. Wir spielen immer wieder dasselbe Spiel.

Führen Sie es jedes Mal anders aus

Wir müssen verschiedene Spiele spielen, ansonsten ist es fast sicher, dass nur ein kleiner Teil unseres alten Codes tatsächlich immer wieder ausgeführt wird. Die Aufgabe des Goldenen Meisters ist es, so viel wie möglich auszuüben. Wir müssen den Zufallsgenerator jedes Mal neu säen, jedoch auf kontrollierte Weise. Eine Möglichkeit ist, unseren Zähler als Startwert zu verwenden.

private Funktion generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ Dateiname, $ this-> generateOutput ($ times)); $ first = false;  else file_put_contents ($ fileName, $ this-> generateOutput ($ times), FILE_APPEND);  $ times--;  private Funktion generateOutput ($ seed) ob_start (); srand ($ seed); erfordern __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output; 

Damit bleibt unser Test bestehen, sodass wir sicher sind, dass wir jedes Mal dieselbe vollständige Ausgabe generieren, während die Ausgabe bei jeder Wiederholung ein anderes Spiel spielt.

cat /tmp/gm.txt | grep "hat 6 Goldmünzen." Sue hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Pat hat jetzt 6 Goldmünzen. Pat hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Sue hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Sue hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Sue hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Pat hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen. Chet hat jetzt 6 Goldmünzen.

Es gibt verschiedene Gewinner des Spiels in zufälliger Weise. Das sieht gut aus.

20.000 erreichen

Als Erstes können Sie versuchen, unseren Code für 20.000 Spieldurchläufe auszuführen.

function testGenerateOutput () $ times = 20000; $ this-> generateMany ($ times, '/tmp/gm.txt'); $ this-> generateMany ($ 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); 

Das wird fast funktionieren. Es werden zwei 55MB-Dateien generiert.

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

Auf der anderen Seite schlägt der Test mit einem unzureichenden Speicherfehler fehl. Es spielt keine Rolle, wie viel RAM Sie haben, dies wird fehlschlagen. Ich habe 8 GB plus einen 4 GB-Swap und es schlägt fehl. Die beiden Saiten sind einfach zu groß, um in unserer Behauptung verglichen zu werden.

Mit anderen Worten, wir generieren gute Dateien, aber PHPUnit kann sie nicht vergleichen. Wir brauchen eine Umgehung.

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

Das scheint ein guter Kandidat zu sein, scheitert aber immer noch. Schade. Wir müssen die Situation weiter erforschen.

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

Das funktioniert aber.

Es kann die beiden Zeichenfolgen vergleichen und fehlschlagen, wenn sie sich unterscheiden. Es hat jedoch einen geringen Preis. Es kann nicht genau sagen, was falsch ist, wenn sich die Saiten unterscheiden. Es wird einfach sagen Msgstr "Falsch behauptet, dass falsch wahr ist.". Aber das werden wir in einem kommenden Tutorial behandeln.

Abschließende Gedanken

Wir sind für dieses Tutorial fertig. Wir haben für unsere erste Lektion viel gelernt und sind für unsere zukünftige Arbeit gut gestartet. Wir haben den Code kennengelernt, ihn auf verschiedene Weise analysiert und seine wesentliche Logik zumeist verstanden. Dann haben wir eine Reihe von Tests erstellt, um sicherzustellen, dass dies so gut wie möglich ausgeübt wird. Ja. Die Tests sind sehr langsam. Sie benötigen 24 Sekunden auf meiner Core i7-CPU, um die Ausgabe zweimal zu erzeugen. Zum Glück in unserer zukünftigen Entwicklung werden wir das behalten gm.txt Datei unberührt und generieren Sie nur einmal pro Lauf eine weitere. Aber 12 Sekunden sind immer noch sehr viel Zeit für eine so kleine Codebasis.

Wenn wir diese Serie abschließen, sollten unsere Tests in weniger als einer Sekunde ablaufen und den gesamten Code ordnungsgemäß testen. Bleiben Sie also beim nächsten Tutorial dran, wenn wir Probleme wie magische Konstanten, magische Zeichenketten und komplexe Bedingungen angehen. Danke fürs Lesen.