Überarbeitung des alten Codes Teil 9 - Analyse der Bedenken

In diesem Lernprogramm konzentrieren wir uns weiterhin auf unsere Geschäftslogik. Wir werden bewerten, ob RunnerFunctions.php gehört zu einer Klasse und wenn ja zu welcher Klasse? Wir werden über Bedenken und Methoden nachdenken. Zum Schluss werden wir etwas mehr über das Konzept des Spottes erfahren. Also, worauf wartest Du? Weiter lesen.


RunnerFunctions - Vom prozeduralen zum objektorientierten

Obwohl wir den Großteil unseres Codes in objektorientierter Form haben, schön in Klassen organisiert, sitzen einige Funktionen einfach in einer Datei. Wir müssen einige nehmen, um die Funktionen zu geben RunnerFunctions.php in einem mehr objektorientierten Aspekt.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; Funktion isCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  function run () $ display = new CLIDisplay (); $ aGame = neues Spiel ($ display); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); do $ dice = rand (0, 5) + 1; $ aGame-> würfeln ($ würfeln);  while (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ()));  function didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) if ($ isCurrentAnswerCorrect) return! $ aGame-> wasCorrectlyAnswered ();  else Rückkehr! $ aGame-> wrongAnswer (); 

Mein erster Instinkt ist, sie einfach in eine Klasse einzuwickeln. Das ist nichts Geniales, aber es ist etwas, was uns dazu bringt, Dinge zu ändern. Mal sehen, ob die Idee tatsächlich funktionieren kann.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; Klasse RunnerFunctions function isCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) Rückgabe rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  Funktion run () //… // Funktion didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) //… //

Wenn wir das tun, müssen wir unsere Tests und unsere Tests ändern GameRunner.php die neue Klasse verwenden. Wir haben die Klasse vorläufig als etwas Generisches bezeichnet. Eine Umbenennung ist einfach, wenn sie benötigt wird. Wir wissen nicht einmal, ob diese Klasse alleine existiert oder in sie aufgenommen wird Spiel. Machen Sie sich also noch keine Sorgen um die Namensgebung.

private Funktion generateOutput ($ seed) ob_start (); srand ($ seed); (neue RunnerFunctions ()) -> run (); $ output = ob_get_contents (); ob_end_clean (); return $ output; 

In unserer GoldenMasterTest.php Datei, müssen wir die Art und Weise ändern, wie wir unseren Code ausführen. Die Funktion ist generateOutput () Die dritte Zeile muss geändert werden, um ein neues Objekt und einen neuen Aufruf zu erstellen Lauf() darauf Aber das schlägt fehl.

PHP Schwerwiegender Fehler: Aufruf an undefinierte Funktion didSomebodyWin () in… 

Wir müssen jetzt unsere neue Klasse weiter modifizieren.

do $ dice = rand (0, 5) + 1; $ aGame-> würfeln ($ würfeln);  while (! $ this-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

Wir mussten nur den Zustand der ändern während Aussage in der Lauf() Methode. Der neue Code ruft an didSomebodyWin () und isCurrentAnswerCorrect () aus der aktuellen Klasse durch Voranstellen $ this-> zu ihnen.

Damit ist der goldene Meister bestanden, aber die Läufertests werden abgebremst.

PHP Fataler Fehler: Aufruf der undefined-Funktion isCurrentAnswerCorrect () in /… /RunnerFunctionsTest.php in Zeile 25

Das Problem ist in assertAnswersAreCorrectFor (), aber leicht zu beheben, indem zuerst ein Läuferobjekt erstellt wird.

private Funktion assertAnswersAreCorrectFor ($ correctAnserIDs) $ runner = new RunnerFunctions (); foreach ($ correctAnserIDs als $ id) $ this-> assertTrue ($ runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Das gleiche Problem muss auch in drei anderen Funktionen behandelt werden.

function testItCanFindWrongAnswer () $ runner = new RunnerFunctions (); $ this-> assertFalse ($ runner-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  Funktion testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ runner = new RunnerFunctions (); $ this-> assertTrue ($ runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aCorrectAnswer ()));  Funktion testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ runner = new RunnerFunctions (); $ this-> assertFalse ($ runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aWrongAnswer ())); 

Während dies den Code durchläuft, führt dies zu etwas Code-Duplizierung. Da wir jetzt alle Tests auf Grün durchführen, können wir die Läufer-Erstellung in eine extrahieren Konfiguration() Methode.

privater $ Läufer; function setUp () $ this-> runner = neuer Runner ();  function testItCanFindCorrectAnswer () $ this-> assertAnswersAreCorrectFor ($ this-> getCorrectAnswerIDs ());  Funktion testItCanFindWrongAnswer () $ this-> assertFalse ($ this-> runner-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  Funktion testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ this-> assertTrue ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aCorrectAnswer ()));  Funktion testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ this-> assertFalse ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aWrongAnswer ()));  private Funktion assertAnswersAreCorrectFor ($ correctAnserIDs) foreach ($ correctAnserIDs als $ id) $ this-> assertTrue ($ this-> runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Nett. All diese neuen Kreationen und Refactorings haben mich zum Nachdenken gebracht. Wir haben unsere Variable benannt Läufer. Vielleicht könnte unsere Klasse genauso heißen. Lass es uns überarbeiten. Es sollte einfach sein.

Wenn Sie nicht geprüft haben "Suchen Sie nach Textvorkommen"Vergessen Sie nicht, Ihre Include - Dateien im obigen Feld manuell zu ändern, da die Datei ebenfalls umbenannt wird.

Jetzt haben wir eine Datei namens GameRunner.php, ein anderer namens Runner.php und ein dritter genannt Game.php. Ich weiß nicht wie es euch geht, aber das erscheint mir äußerst verwirrend. Wenn ich diese drei Dateien zum ersten Mal in meinem Leben sehen würde, hätte ich keine Ahnung, welche Datei was macht. Wir müssen mindestens einen von ihnen loswerden.

Der Grund, warum wir das geschaffen haben RunnerFunctions.php In den frühen Stadien unseres Refactorings bestand die Möglichkeit, alle Methoden und Dateien zum Testen einzubeziehen. Wir brauchten Zugang zu allem, aber liefen nicht alles, außer in einer vorbereiteten Umgebung in unserem goldenen Meister. Wir können immer noch dasselbe tun, nur nicht unseren Code ausführen GameRunner.php. Bevor wir fortfahren, müssen wir die Includes aktualisieren und eine Klasse erstellen.

required_once __DIR__. '/Display.php'; required_once __DIR__. '/Runner.php'; (neuer Runner ()) -> run ();

Das wird es schaffen. Wir müssen einschließen Display.php explizit, also wann Läufer versucht ein neues zu erstellen CLIDisplay, es wird wissen, was zu implementieren ist.


Bedenken analysieren

Ich glaube, dass das Definieren von Bedenken eine der wichtigsten Eigenschaften objektorientierter Programmierung ist. Ich stelle mir immer Fragen wie: "Macht diese Klasse, was der Name sagt?", "Ist diese Methode für dieses Objekt von Belang?", "Soll sich mein Objekt für diesen bestimmten Wert interessieren?"

Überraschenderweise haben diese Arten von Fragen eine große Bedeutung für die Klärung sowohl der Geschäftsdomäne als auch der Softwarearchitektur. Wir fragen und beantworten diese Fragen in einer Gruppe bei Syneto. Wenn ein Programmierer ein Dilemma hat, steht er oft auf und bittet das Team zwei Minuten um Aufmerksamkeit, um unsere Meinung zu einem Thema zu finden. Diejenigen, die mit der Codearchitektur vertraut sind, werden aus Software-Sicht antworten, während andere, die sich mit der Unternehmensdomäne auskennen, möglicherweise einige wichtige Erkenntnisse über kommerzielle Aspekte aufzeigen.

Versuchen wir, in unserem Fall über Bedenken nachzudenken. Wir können uns weiterhin auf das konzentrieren Läufer Klasse. Es ist sehr viel wahrscheinlicher, diese Klasse zu beseitigen oder umzuwandeln, als Spiel.

Erstens sollte sich ein Läufer darum kümmern, wie isCurrentAnswerCorrect () Arbeiten? Sollte ein Läufer über Fragen und Antworten Bescheid wissen??

Es scheint wirklich, als wäre diese Methode besser geeignet Spiel. Ich glaube fest daran, dass a Spiel über Kleinigkeiten sollte es wichtig sein, ob eine Antwort richtig ist oder nicht. Ich glaube wirklich ein Spiel muss besorgt sein, das Ergebnis der Antwort auf die aktuelle Frage anzugeben.

Es ist Zeit zu handeln. Wir werden ein tun Methode verschieben Refactoring. Wie wir das alles schon in meinen vorherigen Tutorials gesehen haben, zeige ich Ihnen nur das Endergebnis.

required_once __DIR__. '/CLIDisplay.php'; include_once __DIR__. '/Game.php'; class Runner function run () //… // Funktion didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) //… //

Es ist wichtig zu beachten, dass nicht nur die Methode wegfiel, sondern auch die Konstante, die die Grenzen der Antwort festlegt.

Aber was ist mit didSomebodyWin ()? Soll ein Läufer entscheiden, wann jemand gewonnen hat? Wenn wir den Körper der Methode betrachten, sehen wir ein Problem, das wie eine Taschenlampe im Dunkeln hervorgehoben wird.

function didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) if ($ isCurrentAnswerCorrect) return! $ aGame-> wasCorrectlyAnswered ();  else return! $ aGame-> wrongAnswer (); 

Was auch immer diese Methode tut, tut es auf einer Spiel nur Objekt Es überprüft die aktuelle Antwort, die vom Spiel zurückgegeben wird. Dann wird das zurückgegeben, was ein Spielobjekt in seinem zurückgibt wasCorrectlyAnswered () oder falsche Antwort() Methoden. Diese Methode ist effektiv nichts für sich. Alles, worum es geht, ist Spiel. Dies ist ein klassisches Beispiel für einen Codegeruch Feature Neid. Eine Klasse tut etwas, was eine andere Klasse tun sollte. Zeit es zu verschieben.

Die Klasse RunnerFunctionsTest erweitert PHPUnit_Framework_TestCase private $ runner; function setUp () $ this-> runner = neuer Runner (); 

Wie üblich haben wir die Tests zuerst verschoben. TDD? Jemand?

Dies lässt uns keine weiteren Tests mehr laufen, so dass diese Datei jetzt verwendet werden kann. Löschen ist mein Lieblingsteil der Programmierung.

Und wenn wir unsere Tests durchführen, erhalten wir einen schönen Fehler.

Schwerwiegender Fehler: Aufruf an undefined Methode Game :: didSomebodyWin ()

Es ist jetzt Zeit, auch den Code zu ändern. Kopieren und Einfügen der Methode in Spiel wird alle Tests magisch bestehen lassen. Sowohl die alten als auch die anderen zogen nach GameTest. Dies bringt die Methode zwar an den richtigen Ort, hat aber zwei Probleme: Der Läufer muss ebenfalls geändert werden, und wir senden eine Fälschung Spiel Objekt, das wir nicht mehr tun müssen, da es Teil von ist Spiel.

do $ dice = rand (0, 5) + 1; $ aGame-> würfeln ($ würfeln);  while (! $ aGame-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

Das Fixieren des Läufers ist sehr einfach. Wir ändern uns einfach $ this-> didSomebodyWin (…) in $ aGame-> didSomebodyWin (…). Wir müssen hierher zurückkommen und es nach unserem nächsten Schritt erneut ändern. Der Test Refactoring.

Funktion testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ aGame = \ Mockery :: mock ('Game [wasCorrectlyAnswered]'); $ aGame-> sollteReceive ('wasCorrectlyAnswered') -> once () -> andReturn (false); $ this-> assertTrue ($ aGame-> didSomebodyWin ($ this-> aCorrectAnswer ())); 

Es ist Zeit für etwas Spott! Anstatt unsere am Ende unserer Tests definierte falsche Klasse zu verwenden, verwenden wir Mockery. Dadurch können wir eine Methode problemlos überschreiben Spiel, Erwarten Sie, dass es aufgerufen wird, und geben Sie den gewünschten Wert zurück. Natürlich könnten wir das durch unsere falsche Klasse erweitern Spiel und überschreiben Sie die Methode selbst. Warum aber eine Arbeit, für die ein Werkzeug existiert??

Funktion testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ aGame = \ Mockery :: mock ('Game [wrongAnswer]'); $ aGame-> sollteReceive ('wrongAnswer') -> once () -> andReturn (true); $ this-> assertFalse ($ aGame-> didSomebodyWin ($ this-> aWrongAnswer ())); 

Nachdem unsere zweite Methode neu geschrieben wurde, können wir die falsche Spielklasse und alle Methoden, mit denen sie initialisiert wurden, loswerden. Probleme gelöst!

Abschließende Gedanken

Auch wenn wir nur an das gedacht haben Läufer, Wir haben heute große Fortschritte gemacht. Wir haben etwas über Verantwortlichkeiten gelernt und Methoden und Variablen identifiziert, die zu einer anderen Klasse gehören. Wir haben auf einer höheren Ebene nachgedacht und uns zu einer besseren Lösung entwickelt. Im Syneto-Team besteht die feste Überzeugung, dass es Möglichkeiten gibt, Code gut zu schreiben und niemals Änderungen vorzunehmen, es sei denn, der Code wurde ein wenig sauberer. Dies ist eine Technik, die mit der Zeit zu einer viel schöneren Codebase mit weniger Abhängigkeiten, mehr Tests und eventuell weniger Fehlern führen kann.

Vielen Dank für Ihre Zeit.