Alter Code. Hässlicher Code. Komplizierter Code Spaghetti-Code. Gibberish Blödsinn. In zwei Worten, Legacy Code. Diese Serie hilft Ihnen bei der Arbeit und beim Umgang damit.
Ich denke gerne über Code nach, genauso wie ich über Prosa denke. Lange, verschachtelte, zusammengesetzte Sätze mit exotischen Wörtern sind schwer zu verstehen. Von Zeit zu Zeit benötigen Sie eines, aber die meiste Zeit können Sie einfache Sätze in kurzen Sätzen verwenden. Dies gilt auch für den Quellcode. Komplexe Bedingungen sind schwer zu verstehen. Lange Methoden sind wie endlose Sätze.
Hier ist ein "prosaisches" Beispiel, um Sie aufzuheitern. Zunächst der All-in-One-Satz. Der Hässliche.
Wenn die Temperatur im Serverraum unter fünf Grad liegt und die Luftfeuchtigkeit über fünfzig Prozent ansteigt, der Luftdruck jedoch konstant bleibt, sollte der leitende Techniker John, der mindestens drei Jahre Berufserfahrung in der Netzwerk- und Serververwaltung hat, dies tun informiert werden, und er muss mitten in der Nacht aufwachen, sich verkleiden, ausgehen, sein Auto nehmen oder ein Taxi rufen, wenn er kein Auto hat, ins Büro fahren, das Gebäude betreten, die Klimaanlage starten und starten Warten Sie, bis die Temperatur über zehn Grad ansteigt und die Luftfeuchtigkeit unter zwanzig Prozent fällt.
Wenn Sie diesen Absatz verstehen, verstehen und sich daran erinnern können, ohne ihn erneut zu lesen, gebe ich Ihnen eine Medaille (virtuell natürlich). Lange, verschlungene Absätze, die in einem einzigen komplizierten Satz stehen, sind schwer zu verstehen. Leider kenne ich nicht genug exotische englische Wörter, um das noch schwerer zu verstehen.
Finden wir einen Weg, um es etwas zu vereinfachen. Alles von seinem ersten Teil bis zum "Dann" ist eine Bedingung. Ja, es ist kompliziert, aber wir könnten es so zusammenfassen: Wenn Umweltbedingungen ein Risiko darstellen… … Dann sollte etwas getan werden. Der komplizierte Ausdruck sagt, wir sollten jemanden benachrichtigen, der viele Bedingungen erfüllt: Benachrichtigen Sie dann den technischen Support der Stufe 3. Schließlich wird ein ganzer Prozess beschrieben, vom Aufwachen des Technikers bis zur Behebung aller Probleme: und erwarten, dass die Umgebung innerhalb normaler Parameter wiederhergestellt wird. Lassen Sie uns alles zusammenstellen.
Wenn Umgebungsbedingungen ein Risiko darstellen, benachrichtigen Sie den technischen Support der Stufe 3 und erwarten Sie, dass die Umgebung innerhalb der normalen Parameter wiederhergestellt wird.
Das sind nur etwa 20% Länge im Vergleich zum ursprünglichen Text. Wir kennen die Details nicht und in der großen Mehrheit der Fälle ist es uns egal. Dies gilt auch für den Quellcode. Wie oft haben Sie sich die Details zur Implementierung von a interessiert logInfo ("Some message");
Methode? Wahrscheinlich einmal, ob und wann Sie es implementiert haben. Dann wird die Nachricht in der Kategorie "Info" protokolliert. Oder wenn ein Benutzer eines Ihrer Produkte kauft, interessiert es Sie, wie Sie ihn in Rechnung stellen? Alles, worum Sie sich kümmern wollen, ist Wenn das Produkt gekauft wurde, entsorgen Sie es aus dem Bestand und stellen Sie es dem Käufer in Rechnung. Die Beispiele könnten endlos sein. Im Grunde schreiben wir korrekte Software.
In diesem Abschnitt werden wir versuchen, die Prosa-Philosophie auf unser Trivia-Spiel anzuwenden. Ein Schritt auf einmal. Angefangen mit komplexen Bedingungen. Beginnen wir mit einem einfachen Code. Nur zum aufwärmen.
Linie zwanzig der GameRunner.php
Datei liest sich wie folgt:
if (rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId)
Wie klingt das in Prosa?? Wenn eine Zufallszahl zwischen der minimalen Antwort-ID und der maximalen Antwort-ID der ID der falschen Antwort entspricht, dann…
Das ist nicht sehr kompliziert, aber wir können es noch einfacher machen. Was ist damit? Wenn eine falsche Antwort ausgewählt wird, dann… Besser ist es nicht?
Wir brauchen einen Weg, eine Prozedur, eine Technik, um diese bedingte Anweisung an einen anderen Ort zu verschieben. Dieses Ziel kann leicht eine Methode sein. Oder in unserem Fall, da wir uns hier nicht in einer Klasse befinden, eine Funktion. Diese Verschiebung des Verhaltens in eine neue Methode oder Funktion wird als Refactoring "Methode extrahieren" bezeichnet. Nachfolgend die Schritte, die Martin Fowler in seinem ausgezeichneten Buch Refactoring: Verbesserung des Designs von vorhandenem Code definiert hat. Wenn Sie dieses Buch nicht gelesen haben, sollten Sie es jetzt in Ihre Liste "Zum Lesen" aufnehmen. Es ist eines der wichtigsten Bücher für einen modernen Programmierer.
Für unser Tutorial habe ich die ursprünglichen Schritte unternommen und sie ein wenig vereinfacht, um unseren Bedürfnissen und unserer Art von Tutorial besser zu entsprechen.
Das ist jetzt ziemlich kompliziert. Die Extraktionsmethode ist jedoch wahrscheinlich die am häufigsten verwendete Refactoring-Methode, mit Ausnahme der Umbenennung. Sie müssen also die Mechanik verstehen.
Glücklicherweise bieten moderne IDEs wie PHPStorm hervorragende Refactoring-Tools, wie wir im Tutorial PHPStorm: When the IDE Really Matters gesehen haben. Wir werden also die Funktionen nutzen, die uns zur Verfügung stehen, anstatt alles von Hand zu tun. Dies ist weniger fehleranfällig und viel, viel schneller.
Wählen Sie einfach den gewünschten Teil des Codes und Rechtsklick es.
Die IDE erkennt automatisch, dass wir drei Parameter benötigen, um unseren Code auszuführen, und schlägt die folgende Lösung vor.
//… // $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) Rückgabewert ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; do $ dice = rand (0, 5) + 1; $ aGame-> würfeln ($ würfeln); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer (); else $ notAWinner = $ aGame-> wasCorrectlyAnswered (); while ($ notAWinner);
Obwohl dieser Code syntaktisch korrekt ist, werden unsere Tests durchbrochen. Zwischen all dem Rauschen, das uns in Rot, Blau und Schwarz angezeigt wird, können wir den Grund dafür erkennen:
Schwerwiegender Fehler: isCurrentAnswerWrong () kann nicht erneut deklariert werden (zuvor in / home / csaba / Persönlich / Programmieren / NetTuts / Refactoring von Legacy Code - Teil 3: Komplexe Bedingungen und lange Methoden /Source/trivia/php/GameRunner.php:16) in / home / csaba / Personal / Programmieren / NetTuts / Umgestaltung von Legacy Code - Teil 3: Komplexe Bedingungen und lange Methoden /Source/trivia/php/GameRunner.php in Zeile 18
Im Wesentlichen heißt es, dass wir die Funktion zweimal deklarieren wollen. Aber wie kann das passieren? Wir haben es nur einmal in unserem GameRunner.php
!
Sehen Sie sich die Tests an. Da ist ein generateOutput ()
Methode, die ein tut benötigen()
auf unserem GameRunner.php
. Es wird mindestens zweimal aufgerufen. Hier liegt die Fehlerquelle.
Jetzt haben wir ein Dilemma. Aufgrund des Seeding des Zufallsgenerators müssen wir diesen Code mit kontrollierten Werten aufrufen.
private Funktion generateOutput ($ seed) ob_start (); srand ($ seed); erfordern __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output;
Es gibt jedoch keine Möglichkeit, eine Funktion zweimal in PHP zu deklarieren. Daher benötigen wir eine andere Lösung. Wir beginnen, die Belastung unserer Goldenen Meisterprüfung zu spüren. Wenn Sie das Ganze 20000 Mal ausführen, jedes Mal, wenn wir einen Code ändern, ist dies möglicherweise keine langfristige Lösung. Abgesehen von der Tatsache, dass es Ewigkeiten braucht, um zu laufen, zwingt es uns dazu, unseren Code zu ändern, um die Art und Weise, wie wir ihn testen, anzupassen. Dies ist in der Regel ein Zeichen für schlechte Tests. Der Code sollte sich ändern und trotzdem den Test bestehen, die Änderungen sollten jedoch Gründe haben, die nur aus dem Quellcode stammen.
Aber genug reden, wir brauchen eine Lösung, auch eine vorübergehende Lösung wird es für jetzt tun. Die Migration zu Komponententests beginnt mit unserer nächsten Lektion.
Eine Möglichkeit, unser Problem zu lösen, besteht darin, den gesamten restlichen Code in zu übernehmen GameRunner.php
und lege es in eine Funktion. Sagen wir Lauf()
include_once __DIR__. '/Game.php'; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) Rückgabewert ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; function run () $ notAWinner; $ aGame = neues Spiel (); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; do $ dice = rand (0, 5) + 1; $ aGame-> würfeln ($ würfeln); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer (); else $ notAWinner = $ aGame-> wasCorrectlyAnswered (); while ($ notAWinner);
Dies ermöglicht es uns, es zu testen. Beachten Sie jedoch, dass bei Ausführung des Codes über die Konsole das Spiel nicht ausgeführt wird. Wir haben eine leichte Verhaltensänderung vorgenommen. Wir haben die Prüfbarkeit auf Kosten einer Verhaltensänderung gewonnen, die wir gar nicht erst wollten. Wenn Sie den Code von der Konsole aus ausführen möchten, benötigen Sie jetzt eine andere PHP-Datei, die den Läufer enthält oder erfordert, und ruft dann explizit die run-Methode auf. Eine nicht so große Änderung, aber ein Muss, vor allem, wenn Dritte Ihren vorhandenen Code verwenden.
Auf der anderen Seite können wir die Datei jetzt einfach in unseren Test aufnehmen.
erfordern __DIR__. '/… /Trivia/php/GameRunner.php';
Und dann anrufen Lauf()
innerhalb der generateOutput () -Methode.
private Funktion generateOutput ($ seed) ob_start (); srand ($ seed); Lauf(); $ output = ob_get_contents (); ob_end_clean (); return $ output;
Vielleicht ist dies eine gute Gelegenheit, über die Struktur unserer Verzeichnisse und Dateien nachzudenken. Es gibt keine komplexeren Bedingungen in unserem GameRunner.php
, aber bevor wir weiter zum Game.php
Datei, wir dürfen keine Unordnung hinter uns lassen. Unsere GameRunner.php
Es läuft nichts mehr und wir mussten Methoden zusammen hacken, um es testbar zu machen, was unsere öffentliche Schnittstelle brach. Der Grund dafür ist, dass wir vielleicht das Falsche testen.
Unsere Testanrufe Lauf()
in dem GameRunner.php
Datei, die enthält Game.php
, spielt das Spiel und eine neue goldene Master-Datei wird generiert. Was ist, wenn wir eine andere Datei einführen? Wir machen das GameRunner.php
um das Spiel tatsächlich auszuführen, indem Sie anrufen Lauf()
und sonst nichts. Was ist, wenn es keine Logik gibt, die schief gehen könnte und keine Tests erforderlich sind, und wir dann unseren aktuellen Code in eine andere Datei verschieben?
Das ist eine ganz andere Geschichte. Jetzt greifen unsere Tests auf den Code direkt unter dem Läufer zu. Grundsätzlich sind unsere Tests nur Läufer. Und natürlich in unserem Neuen GameRunner.php
Es wird nur ein Aufruf zum Ausführen des Spiels geben. Dies ist ein echter Läufer, der nichts anderes als den Aufruf von Lauf()
Methode. Keine Logik bedeutet, dass keine Tests erforderlich sind.
required_once __DIR__. '/RunnerFunctions.php'; Lauf();
Es gibt andere Fragen, die wir uns an diesem Punkt stellen könnten. Brauchen wir wirklich eine RunnerFunctions.php
? Könnten wir nicht einfach die Funktionen von dort nehmen und sie verschieben? Game.php
? Wir könnten wahrscheinlich, aber welche Funktion gehört nach unserem derzeitigen Verständnis wohin? Ist nicht genug. Wir werden in einer kommenden Lektion einen Platz für unsere Methode finden.
Wir haben auch versucht, unsere Dateien entsprechend dem Code in ihrem Namen zu benennen. Eine ist nur eine Reihe von Funktionen für den Läufer, Funktionen, die wir an dieser Stelle als zusammengehörig betrachten, um die Bedürfnisse des Läufers zu befriedigen. Wird dies zu einem späteren Zeitpunkt eine Klasse werden? Könnte sein. Vielleicht nicht. Im Moment ist es gut genug.
Schauen wir uns das an RunnerFunctions.php
Datei, es gibt ein bisschen Durcheinander, das wir eingeführt haben.
Wir definieren:
$ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7;
… In der Lauf()
Methode. Sie haben einen einzigen Grund zu existieren und einen einzigen Ort, an dem sie benutzt werden. Warum definieren Sie sie nicht einfach innerhalb dieser Methode und entfernen Sie die Parameter vollständig?
function isCurrentAnswerWrong () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId;
Ok, die Tests sind bestanden und der Code ist viel schöner. Aber nicht gut genug.
Es ist für den menschlichen Geist viel einfacher, positives Denken zu verstehen. Wenn Sie also negative Bedingungen vermeiden können, sollten Sie immer diesen Weg einschlagen. In unserem aktuellen Beispiel sucht die Methode nach einer falschen Antwort. Es wäre viel einfacher, eine Methode zu verstehen, die auf Gültigkeit prüft und diese bei Bedarf negiert.
function isCurrentAnswerCorrect () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId)! = $ wrongAnswerId;
Wir haben die Umbenennungsmethode überarbeitet. Dies ist wiederum ziemlich kompliziert, wenn es von Hand verwendet wird, aber in jeder IDE ist es so einfach wie das Schlagen STRG + r, oder wählen Sie die entsprechende Option im Menü aus. Damit unsere Tests erfolgreich sind, müssen wir unsere Bedingungsanweisung mit einer Negation aktualisieren.
if (! isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wrongAnswer (); else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();
Dies bringt uns unserem Verständnis der Bedingungen einen Schritt näher. Verwenden !
in einem (n ob()
Aussage hilft eigentlich. Es fällt auf und unterstreicht, dass dort tatsächlich etwas negiert wird. Aber können wir das umkehren, um die Negation vollständig zu vermeiden? ja wir können.
if (isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wasCorrectlyAnswered (); else $ notAWinner = $ aGame-> wrongAnswer ();
Jetzt haben wir keine logische Negation durch die Verwendung von !
, noch lexikalische Negation durch Benennung und Rückgabe der falschen Dinge. All diese Schritte machten unsere Bedingungen viel leichter verständlich.
Game.php
Wir haben uns extrem vereinfacht, RunnerFunctions.php
. Lasst uns unsere angreifen Game.php
Datei jetzt. Es gibt mehrere Möglichkeiten, nach Bedingungen zu suchen. Wenn Sie möchten, können Sie den Code einfach scannen, indem Sie ihn einfach ansehen. Dies ist langsamer, hat jedoch den zusätzlichen Vorteil, dass Sie dazu gezwungen werden, es sequenziell zu verstehen.
Die zweite offensichtliche Möglichkeit, nach Bedingungen zu suchen, besteht darin, einfach nach "if" oder "if (") zu suchen. Wenn Sie Ihren Code mit den integrierten Funktionen Ihrer IDE formatiert haben, können Sie sicher sein, dass alle bedingten Anweisungen die gleiche spezifische Form In meinem Fall gibt es ein Leerzeichen zwischen dem "Wenn" und der Klammer. Wenn Sie die integrierte Suche verwenden, werden die gefundenen Ergebnisse in einer strengen Farbe hervorgehoben, in meinem Fall gelb.
Jetzt, da wir alle unseren Code wie einen Weihnachtsbaum leuchten, können wir sie einzeln übernehmen. Wir kennen die Übung, wir kennen die Techniken, die wir anwenden können, es ist Zeit, sie anzuwenden.
if ($ this-> inPenaltyBox [$ this-> currentPlayer])
Das scheint ziemlich vernünftig zu sein. Wir könnten es in eine Methode extrahieren, aber es würde einen Namen für diese Methode geben, um die Bedingung klarer zu machen?
if ($ roll% 2! = 0)
Ich wette, 90% aller Programmierer können das Problem oben verstehen ob
Aussage. Wir versuchen uns auf die aktuelle Methode zu konzentrieren. Und unser Gehirn ist mit dem Problembereich verbunden. Wir möchten nicht einen anderen Thread starten, um diesen mathematischen Ausdruck zu berechnen, um zu verstehen, dass nur geprüft wird, ob eine Zahl ungerade ist. Dies ist eine dieser kleinen Ablenkungen, die eine schwierige logische Schlussfolgerung ruinieren können. Also sage ich es herausholen.
if ($ this-> isOdd ($ roll))
Das ist besser, weil es sich um die Domäne des Problems handelt und keine zusätzliche Gehirnleistung erforderlich ist.
if ($ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard)
Dies scheint ein weiterer guter Kandidat zu sein. Es ist nicht so schwer, als mathematischer Ausdruck zu verstehen, aber es ist auch ein Ausdruck, der Seitenverarbeitung erfordert. Ich frage mich, was bedeutet es, wenn die Position des aktuellen Spielers das Ende des Bretts erreicht hat? Können wir diesen Zustand nicht kürzer ausdrücken? Wir können es wahrscheinlich.
if ($ this-> playerReachedEndOfBoard ($ lastPositionOnTheBoard))
Das ist besser. Aber was passiert eigentlich im Inneren? ob
? Der Spieler wird zu Beginn der Tafel neu positioniert. Der Spieler startet eine neue "Runde" im Rennen. Was ist, wenn wir in der Zukunft einen anderen Grund haben werden, eine neue Runde zu beginnen? Sollte unser ob
Anweisungsänderung, wenn wir die zugrunde liegende Logik in der privaten Methode ändern? Absolut nicht! Also, lasst uns diese Methode in was umbenennen ob
repräsentiert, was passiert, nicht das, wonach wir suchen.
if ($ this-> playerShouldStartANewLap ($ lastPositionOnTheBoard))
Wenn Sie versuchen, Methoden und Variablen zu benennen, denken Sie immer darüber nach, was der Code tun soll und nicht, welchen Status oder welche Bedingung er darstellt. Wenn Sie dies richtig gemacht haben, wird das Umbenennen von Aktionen in Ihrem Code erheblich reduziert. Dennoch muss selbst ein erfahrener Programmierer eine Methode mindestens drei bis fünf Mal umbenennen, bevor er den richtigen Namen finden kann. Also keine Angst davor zu treffen STRG + r und häufig umbenennen. Übernehmen Sie niemals Ihre Änderungen am VCS des Projekts, wenn Sie die Namen Ihrer neu hinzugefügten Methoden nicht gescannt haben und Ihr Code nicht wie eine gut geschriebene Prosa gelesen wird. Umbenennen ist in unseren Tagen so billig, dass Sie die Dinge umbenennen können, um verschiedene Versionen auszuprobieren und mit einem einzigen Tastendruck wieder herzustellen.
Das ob
Die Aussage in Zeile 90 entspricht der vorherigen. Wir können unsere extrahierte Methode einfach wiederverwenden. Voila, Doppelarbeit beseitigt! Vergessen Sie nicht, Ihre Tests jetzt und dann auszuführen, auch wenn Sie die Magie Ihrer IDE verwenden. Was uns zu unserer nächsten Beobachtung führt. Magie versagt manchmal. Schauen Sie sich die Linie 65 an.
$ lastPositionOnTheBoard = 11;
Wir deklarieren eine Variable und verwenden sie nur an einer Stelle als Parameter für unsere neu extrahierte Methode. Dies legt stark nahe, dass sich die Variable innerhalb der Methode befinden sollte.
private Funktion playerShouldStartANewLap () $ lastPositionOnTheBoard = 11; $ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard zurückgeben;
Vergessen Sie nicht, die Methode ohne Parameter in Ihrer aufzurufen ob
Aussagen.
if ($ this-> playerShouldStartANewLap ())
Das ob
Aussagen in der Frage stellen()
Methode scheint in Ordnung zu sein, ebenso wie die in currentCategory ()
.
if ($ this-> inPenaltyBox [$ this-> currentPlayer])
Dies ist etwas komplizierter, aber in der Domäne und ausdrucksstark genug.
if ($ this-> currentPlayer == count ($ this-> Spieler))
Wir können daran arbeiten. Es ist offensichtlich, dass der Vergleich bedeutet, wenn der aktuelle Spieler außerhalb der Grenzen liegt. Aber wie wir oben gelernt haben, wollen wir nicht die Absicht sagen.
if ($ this-> sollteResetCurrentPlayer ())
Das ist viel besser, und wir werden es in Zeile 172, 189 und 203 wiederverwenden. Duplikation, ich meine die Verdreifachung, ich meine die Vierfachvervielfachung, eliminiert!
Tests sind bestanden und alle ob
Aussagen wurden auf Komplexität bewertet.
Es gibt mehrere Lektionen, die man aus Refactoring-Bedingungen lernen kann. Sie helfen vor allem, die Absicht des Codes besser zu verstehen. Wenn Sie dann die extrahierte Methode benennen, um die Absicht richtig darzustellen, vermeiden Sie zukünftige Namensänderungen. Das Auffinden von Duplizierungen in der Logik ist schwieriger als das Auffinden von duplizierten Zeilen mit einfachem Code. Sie haben vielleicht gedacht, dass wir eine bewusste Vervielfältigung machen sollten, aber ich bevorzuge die Vervielfältigung, wenn ich Unit-Tests habe, denen ich mein Leben anvertrauen kann. Der Goldene Meister ist gut, aber höchstens ein Sicherheitsnetz und kein Fallschirm.
Vielen Dank für das Lesen und bleiben Sie dran für unser nächstes Tutorial, wenn wir unsere ersten Unit-Tests vorstellen.