Überarbeitung des alten Codes - Teil 10 Analyse langer Methoden mit Extraktionen

Im sechsten Teil unserer Serie sprachen wir darüber, wie lange Methoden angegriffen werden sollten, indem Paarbildung und das Anzeigen von Code aus verschiedenen Ebenen genutzt werden. Wir zoomten kontinuierlich ein und aus und beobachteten sowohl Kleinigkeiten wie die Namensgebung als auch Form und Einzug.

Heute gehen wir einen anderen Weg: Wir gehen davon aus, dass wir alleine sind, kein Kollege oder Paar, das uns helfen kann. Wir werden eine Technik namens "Extrahieren bis zum Ablegen" verwenden, bei der Code in sehr kleine Teile zerbrochen wird. Wir werden alles tun, um diese Stücke so verständlich wie möglich zu machen, damit die Zukunft uns oder jeder andere Programmierer sie leicht verstehen kann.


Bis zum Umfallen extrahieren

Ich habe dieses Konzept zum ersten Mal von Robert C. Martin gehört. Er präsentierte die Idee in einem seiner Videos als eine einfache Möglichkeit, schwer verständlichen Code umzuwandeln.

Die Grundidee ist, kleine, verständliche Codestücke zu entnehmen und zu extrahieren. Es spielt keine Rolle, ob Sie vier Zeilen oder vier Zeichen auswählen, die extrahiert werden können. Wenn Sie etwas identifizieren, das in einem klareren Konzept verkapselt werden kann, extrahieren Sie es. Sie setzen diesen Vorgang sowohl bei der Originalmethode als auch bei den neu extrahierten Stücken fort, bis Sie keinen Code finden, der als Konzept gekapselt werden kann.

Diese Technik ist besonders nützlich, wenn Sie alleine arbeiten. Sie zwingen Sie, über kleine und größere Codeabschnitte nachzudenken. Es hat einen weiteren schönen Effekt: Es lässt Sie über den Code nachdenken - sehr viel! Neben der oben genannten Extraktionsmethode oder dem Variablen-Refactoring können Sie Variablen, Funktionen, Klassen usw. umbenennen.

Sehen wir uns ein Beispiel für einen zufälligen Code aus dem Internet an. Stackoverflow ist ein guter Ort, um kleine Code-Teile zu finden. Hier ist einer, der bestimmt, ob eine Zahl eine Primzahl ist:

// Prüfen Sie, ob eine Zahl eine Primzahl ist. IsPrime ($ num, $ pf = null) if (! Is_array ($ pf)) für ($ i = 2; $ i

Zu diesem Zeitpunkt habe ich keine Ahnung, wie dieser Code funktioniert. Ich habe es gerade im Internet gefunden, als ich diesen Artikel geschrieben habe, und ich werde es zusammen mit Ihnen entdecken. Der folgende Prozess ist möglicherweise nicht der sauberste. Stattdessen wird es meine Überlegungen und das Refactoring ohne vorherige Planung widerspiegeln.

Refactoring des Prime Number Checker

Laut Wikipedia:

Eine Primzahl (oder eine Primzahl) ist eine natürliche Zahl größer als 1, die keine positiven Teiler außer 1 und sich selbst hat. 

Wie Sie sehen, ist dies eine einfache Methode für ein einfaches mathematisches Problem. Es kehrt zurück wahr oder falsch, so sollte es auch leicht zu testen sein.

class IsPrimeTest erweitert PHPUnit_Framework_TestCase function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1));  // Prüfen Sie, ob eine Zahl die Primzahl ist isPrime ($ num, $ pf = null) //… der Inhalt der Methode wie oben

Wenn wir nur mit Beispielcode spielen, ist der einfachste Weg, alles in eine Testdatei zu packen. Auf diese Weise müssen wir nicht darüber nachdenken, welche Dateien erstellt werden sollen, in welche Verzeichnisse sie gehören oder wie sie in die anderen aufgenommen werden sollen. Dies ist nur ein einfaches Beispiel, um uns mit der Technik vertraut zu machen, bevor wir sie auf eine der Trivia-Spielmethoden anwenden. So geht alles in eine Testdatei, Sie können nach Belieben benennen. Ich habe gewählt IsPrimeTest.php.

Dieser Test ist bestanden. Mein nächster Instinkt ist, ein paar mehr Primzahlen hinzuzufügen und dann einen weiteren Test ohne Primzahlen zu schreiben.

function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); $ this-> assertTrue (isPrime (2)); $ this-> assertTrue (isPrime (3)); $ this-> assertTrue (isPrime (5)); $ this-> assertTrue (isPrime (7)); $ this-> assertTrue (isPrime (11)); 

Das geht vorbei. Aber was ist damit??

function testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6)); 

Dies schlägt unerwartet fehl: 6 ist keine Primzahl. Ich hatte erwartet, dass die Methode zurückkehrt falsch. Ich weiß nicht, wie die Methode funktioniert, oder den Zweck der $ pf Parameter - ich hatte einfach erwartet, dass es zurückkehrt falsch basierend auf seinem Namen und seiner Beschreibung. Ich habe keine Ahnung, warum es nicht funktioniert oder wie ich es reparieren kann.

Dies ist ein verwirrendes Dilemma. Was sollen wir machen? Die beste Antwort ist, Tests zu schreiben, die für eine anständige Anzahl von Zahlen bestehen. Wir müssen vielleicht versuchen und raten, aber zumindest haben wir eine Vorstellung davon, was die Methode macht. Dann können wir mit dem Refactoring beginnen.

Funktion testFirst20NaturalNumbers () für ($ i = 1; $ i<20;$i++)  echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";  

Das gibt etwas Interessantes aus:

1 - wahr 2 - wahr 3 - wahr 4 - wahr 5 - wahr 6 - wahr 7 - wahr 8 - wahr 9 - wahr 10 - falsch 11 - wahr 12 - falsch 13 - wahr 14 - falsch 15 - wahr 16 - falsch 17 - wahr 18 - falsch 19 - wahr

Hier entsteht ein Muster. Alles wahr bis 9, dann abwechselnd bis 19. Aber wiederholt sich dieses Muster? Versuchen Sie, es für 100 Nummern auszuführen, und Sie werden sofort feststellen, dass dies nicht der Fall ist. Es scheint tatsächlich für Zahlen zwischen 40 und 99 zu arbeiten. Es versagt einmal zwischen 30-39, indem es 35 als Primzahl nominiert. Gleiches gilt für den Bereich 20-29. 25 gilt als Primzahl.

Diese Übung, die als einfacher Code zur Demonstration einer Technik begann, erweist sich als viel schwieriger als erwartet. Ich entschied mich jedoch, es beizubehalten, weil es das typische Leben auf typische Weise widerspiegelt.

Wie oft haben Sie begonnen, an einer Aufgabe zu arbeiten, die einfach aussah, nur um herauszufinden, dass es äußerst schwierig ist?

Wir möchten den Code nicht korrigieren. Was auch immer die Methode tut, sie sollte es weiterhin tun. Wir wollen es umgestalten, damit andere es besser verstehen.

Da Primzahlen nicht korrekt angegeben werden, verwenden wir denselben Golden Master-Ansatz, den wir in Lektion 1 gelernt haben.

function testGenerateGoldenMaster () für ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND);  

Führen Sie dies einmal aus, um den Goldenen Meister zu generieren. Es sollte schnell gehen. Wenn Sie es erneut ausführen müssen, vergessen Sie nicht, die Datei zu löschen, bevor Sie den Test ausführen. Andernfalls wird die Ausgabe an den vorherigen Inhalt angehängt.

function testMatchesGoldenMaster () $ goldenMaster = Datei (__ DIR__. '/IsPrimeGoldenMaster.txt'); für ($ i = 1; $ i<10000;$i++)  $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'Der Wert'. $ actualResult. 'ist nicht im goldenen Meister.'); 

Schreibe jetzt den Test für den goldenen Meister. Diese Lösung ist vielleicht nicht die schnellste, aber sie ist leicht zu verstehen und sagt uns genau, welche Zahl nicht passt, wenn etwas kaputt geht. Bei den beiden Testmethoden, die wir in eine Methode extrahieren könnten, gibt es jedoch eine kleine Doppelung Privatgelände Methode.

class IsPrimeTest erweitert PHPUnit_Framework_TestCase function testGenerateGoldenMaster () $ this-> markTestSkipped (); für ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND);  Funktion testMatchesGoldenMaster () $ goldenMaster = Datei (__ DIR__. '/IsPrimeGoldenMaster.txt'); für ($ i = 1; $ i<10000;$i++)  $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'The value'. $ actualResult. 'ist nicht im Golden Master.');  private Funktion getPrimeResultAsString ($ i) return $ i. '-'. (isPrime ($ i)? 'true': 'false'). "\ n"; 

Jetzt können wir un zu unserem Produktionscode wechseln. Der Test läuft auf meinem Computer in etwa zwei Sekunden ab und ist damit überschaubar.

Alles extrahieren, was wir können

Zuerst können wir eine extrahieren isDivisible () Methode im ersten Teil des Codes.

if (! is_array ($ pf)) für ($ i = 2; $ i

Dadurch können wir den Code im zweiten Teil wie folgt wiederverwenden:

 else $ pfCount = Anzahl ($ pf); für ($ i = 0; $ i<$pfCount;$i++)  if(isDivisible($num, $pf[$i]))  return false;   return true; 

Und als wir anfingen, mit diesem Code zu arbeiten, stellten wir fest, dass er unvorsichtig ausgerichtet ist. Zahnspangen stehen manchmal am Anfang der Linie, andere am Ende. 

Manchmal werden Tabulatoren zum Einrücken verwendet, manchmal auch Leerzeichen. Manchmal gibt es Leerzeichen zwischen Operanden und Operatoren, manchmal nicht. Und nein, das ist kein speziell erstellter Code. Das ist das wahre Leben. Echter Code, keine künstliche Übung.

// Prüfen Sie, ob eine Zahl eine Primzahl ist. IsPrime ($ num, $ pf = null) if (! Is_array ($ pf)) für ($ i = 2; $ i < intval(sqrt($num)); $i++)  if (isDivisible($num, $i))  return false;   return true;  else  $pfCount = count($pf); for ($i = 0; $i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;  

Das sieht besser aus Gleich die beiden ob Aussagen sehen sehr ähnlich aus. Aber wir können sie nicht wegen der entziehen Rückkehr Aussagen. Wenn wir nicht zurückkehren, brechen wir die Logik. 

Wenn die extrahierte Methode einen booleschen Wert zurückgeben würde, vergleichen wir sie, um zu entscheiden, ob wir zurückkehren sollen oder nicht isPrime (), das würde überhaupt nicht helfen. Es gibt möglicherweise eine Möglichkeit, es mithilfe einiger funktionaler Programmierungskonzepte in PHP zu extrahieren, möglicherweise jedoch später. Wir können zuerst etwas einfacher machen.

function isPrime ($ num, $ pf = null) if (! is_array ($ pf)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else $ pfCount = Anzahl ($ pf); für ($ i = 0; $ i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;   function checkDivisorsBetween($start, $end, $num)  for ($i = $start; $i < $end; $i++)  if (isDivisible($num, $i))  return false;   return true; 

Extrahieren der zum Die Schleife als Ganzes ist etwas einfacher, aber wenn wir versuchen, unsere extrahierte Methode im zweiten Teil der ob Wir können sehen, dass es nicht funktioniert. Da ist das geheimnisvoll $ pf Variable, über die wir fast nichts wissen. 

Es scheint, als würde es prüfen, ob die Anzahl durch einen Satz von bestimmten Teilern teilbar ist, anstatt alle Zahlen auf den anderen magischen Wert zu bringen, der durch bestimmt wird intval (sqrt ($ num)). Vielleicht könnten wir umbenennen $ pf in $ Divisoren.

Funktion isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else return checkDivisorsBetween (0, Anzahl ($ Divisoren), $ Num, $ Divisoren);  Funktion checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) für ($ i = $ start; $ i.) < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Dies ist eine Möglichkeit, dies zu tun. Wir haben unserer Prüfmethode einen vierten optionalen Parameter hinzugefügt. Wenn es einen Wert hat, verwenden wir ihn, ansonsten verwenden wir ihn $ i.

Können wir noch etwas extrahieren? Was ist mit diesem Code: intval (sqrt ($ num))?

Funktion isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, integerRootOf ($ num), $ num);  else return checkDivisorsBetween (0, Anzahl ($ Divisoren), $ Num, $ Divisoren);  function integerRootOf ($ num) return intval (sqrt ($ num)); 

Ist das nicht besser? Etwas. Es ist besser, wenn die Person, die nach uns kommt, nicht weiß, was intval () und sqrt () tun, aber es hilft nicht, die Logik verständlicher zu machen. Warum beenden wir unsere zum Schleife bei dieser bestimmten Nummer? Vielleicht ist dies die Frage, die unser Funktionsname beantworten sollte.

[PHP] // Prüfen Sie, ob eine Zahl die Primzahl ist. IsPrime ($ num, $ divisors = null) if (! Is_array ($ divisors)) return checkDivisorsBetween (2, highestPossibleFactor ($ num), $ num);  else return checkDivisorsBetween (0, Anzahl ($ Divisoren), $ Num, $ Divisoren);  function highestPossibleFactor ($ num) return intval (sqrt ($ num));  [PHP]

Das ist besser, weil es erklärt, warum wir dort aufhören. Vielleicht können wir in Zukunft eine andere Formel entwickeln, um diese Zahl zu bestimmen. Die Benennung führte auch zu einer kleinen Inkonsistenz. Wir haben die Zahlenfaktoren genannt, was ein Synonym für Teiler ist. Vielleicht sollten wir uns eins aussuchen und nur das verwenden. Ich lasse Sie die Umbenennung als Übung durchführen.

Die Frage ist, können wir noch etwas herausholen? Nun, wir müssen versuchen, bis wir fallen. Ich habe die funktionale Programmierseite von PHP ein paar Absätze erwähnt. Es gibt zwei Hauptmerkmale der funktionalen Programmierung, die wir leicht in PHP anwenden können: erstklassige Funktionen und Rekursion. Wann immer ich einen sehe ob Aussage mit einem Rückkehr in einem zum Schleife, wie in unserem checkDivisorsBetween () Methode, denke ich darüber nach, eine oder beide Techniken anzuwenden.

Funktion checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) für ($ i = $ start; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Aber warum sollten wir einen so komplexen Denkprozess durchlaufen? Der ärgerlichste Grund ist, dass diese Methode zwei verschiedene Dinge tut: Sie läuft ab und entscheidet. Ich möchte, dass es nur einen Zyklus darstellt und die Entscheidung einer anderen Methode überlässt. Eine Methode sollte immer etwas tun und es gut machen.

Funktion checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) $ numberIsNotPrime = function ($ num, $ divisor) if (isDivisible ($ num, $ divisor)) return false; ; für ($ i = $ start; $ i < $end; $i++)  $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i);  return true; 

Unser erster Versuch bestand darin, die Bedingung und die return-Anweisung in eine Variable zu extrahieren. Dies ist für den Moment lokal. Aber der Code funktioniert nicht. Eigentlich die zum Schleife macht die Sache ziemlich kompliziert. Ich habe das Gefühl, dass eine kleine Rekursion helfen wird.

Funktion checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true; 

Wenn wir über Rekursivität nachdenken, müssen wir immer mit den Ausnahmefällen beginnen. Unsere erste Ausnahme ist, wenn wir das Ende unserer Rekursion erreichen.

Funktion checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true;  if (isDivisible ($ num, $ divisor)) return false; 

Unser zweiter Ausnahmefall, der die Rekursion durchbricht, ist, dass die Anzahl teilbar ist. Wir wollen nicht weiter machen. Und hier geht es um alle Ausnahmefälle.

ini_set ('xdebug.max_nesting_level', 10000); Funktion checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) return checkRecursiveDivisibility ($ start, $ end, $ num, $ divisors);  Funktion checkRecursiveDivisibility ($ current, $ end, $ num, $ divisors) if ($ current == $ end) return true;  if (isDivisible ($ num, $ divisors? $ divisors [$ current]: $ current)) return false;  checkRecursiveDivisibility ($ current ++, $ end, $ num, $ divisors); 

Dies ist ein weiterer Versuch, die Rekursion für unser Problem zu verwenden, aber leider führt das 10.000-malige Wiederholen in PHP zu einem Absturz von PHP oder PHPUnit auf meinem System. Dies scheint also eine weitere Sackgasse zu sein. Aber wenn es funktioniert hätte, wäre es ein guter Ersatz für die ursprüngliche Logik gewesen.


Herausforderung

Als ich den Goldenen Meister schrieb, habe ich absichtlich etwas übersehen. Sagen wir einfach, die Tests decken nicht so viel Code ab, wie sie sollten. Kannst du das Problem erkennen? Wenn ja, wie würden Sie es angehen??


Abschließende Gedanken

"Extrahieren bis zum Umfallen" ist ein guter Weg, lange Methoden zu analysieren. Es zwingt Sie, über kleine Codeteile nachzudenken und den Stücken einen Zweck zu geben, indem Sie sie in Methoden extrahieren. Ich finde es erstaunlich, wie ich mit dieser einfachen Prozedur und häufigem Umbenennen feststellen kann, dass ein Code Dinge tut, die ich nie für möglich gehalten habe.

In unserem nächsten und letzten Tutorial zum Refactoring werden wir diese Technik auf das Trivia-Spiel anwenden. Ich hoffe, Ihnen hat dieses Tutorial gefallen, das sich als etwas anders herausstellte. Anstatt über Lehrbuchbeispiele zu sprechen, haben wir echten Code genommen und mussten mit den wirklichen Problemen kämpfen, mit denen wir täglich konfrontiert sind.