Refactoring von Legacy Code Teil 8 - Umkehren von Abhängigkeiten für eine saubere Architektur

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.

Es ist jetzt an der Zeit, über Architektur zu sprechen und wie wir unsere neu gefundenen Codeschichten organisieren. Es ist an der Zeit, unsere Anwendung zu nutzen und zu versuchen, sie dem theoretischen architektonischen Entwurf zuzuordnen.

Saubere Architektur

Dies ist etwas, was wir in unseren Artikeln und Tutorials gesehen haben. Saubere Architektur.

Auf hoher Ebene sieht es aus wie das Schema oben und ich bin sicher, dass Sie es bereits kennen. Es ist eine vorgeschlagene architektonische Lösung von Robert C. Martin.

Im Zentrum unserer Architektur steht unsere Geschäftslogik. Dies sind die Klassen, die die Geschäftsprozesse darstellen, die unsere Anwendung zu lösen versucht. Dies sind die Entitäten und Interaktionen, die die Domäne unseres Problems darstellen.

Dann gibt es einige andere Arten von Modulen oder Klassen rund um unsere Geschäftslogik. Diese können als einfache Hilfsmodule angesehen werden. Sie haben verschiedene Zwecke und die meisten davon sind unverzichtbar. Sie stellen die Verbindung zwischen dem Benutzer und unserer Anwendung über einen Zustellmechanismus bereit. In unserem Fall ist dies eine Befehlszeilenschnittstelle. Es gibt eine Reihe weiterer Hilfsklassen, die unsere Geschäftslogik mit unserer Persistenzschicht und allen Daten in dieser Schicht verbinden, aber wir haben keine solche Schicht in unserer Anwendung. Dann gibt es helfende Klassen wie Fabriken und Bauherren, die neue Objekte für unsere Geschäftslogik erstellen und bereitstellen. Schließlich gibt es die Klassen, die den Einstiegspunkt in unser System darstellen. In unserem Fall, GameRunner kann als eine solche Klasse betrachtet werden, oder alle unsere Tests sind auf ihre Art auch Einstiegspunkte.

Was im Diagramm am wichtigsten ist, ist die Abhängigkeitsrichtung. Alle Hilfsklassen hängen von der Geschäftslogik ab. Die Geschäftslogik hängt von nichts anderem ab. Wenn alle Objekte in unserer Geschäftslogik mit allen Daten magisch auftauchen könnten und wir sehen könnten, was direkt in unserem Computer passiert, sollten sie funktionieren können. Unsere Geschäftslogik muss in der Lage sein, ohne Benutzeroberfläche oder ohne Persistenzschicht zu funktionieren. Unsere Geschäftslogik muss isoliert in einer Blase eines logischen Universums existieren.

Das Prinzip der Abhängigkeitsinversion

A. High-Level-Module sollten nicht von Low-Level-Modulen abhängen. Beides sollte von Abstraktionen abhängen.
B. Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.

Dies ist es, das letzte SOLID-Prinzip und wahrscheinlich das mit dem größten Einfluss auf Ihren Code. Es ist sowohl ziemlich einfach zu verstehen als auch ziemlich einfach zu implementieren.

In einfachen Worten heißt es, dass konkrete Dinge immer von abstrakten Dingen abhängen sollten. Ihre Datenbank ist sehr konkret, daher sollte es auf etwas Abstrakteres ankommen. Ihre Benutzeroberfläche ist sehr konkret, daher sollte es auf etwas Abstrakteres ankommen. Ihre Fabriken sind wieder sehr konkret. Aber wie sieht es mit Ihrer Geschäftslogik aus? In Ihrer Geschäftslogik sollten Sie diese Ideen weiter anwenden, sodass die Klassen, die sich näher an den Grenzen befinden, von Klassen abhängen, die abstrakter sind und eher in der Geschäftslogik liegen.

Eine reine Geschäftslogik repräsentiert auf abstrakte Weise die Prozesse und Verhaltensweisen einer definierten Domäne oder eines Geschäftsmodells. Eine solche Geschäftslogik enthält keine Details (konkrete Dinge) wie Werte, Geld, Kontonamen, Kennwörter, die Größe einer Schaltfläche oder die Anzahl der Felder in einem Formular. Die Geschäftslogik sollte sich nicht für konkrete Dinge interessieren. Es sollte sich nur um Ihre Geschäftsprozesse kümmern.

Der technische Trick

Das Prinzip der Inversion der Abhängigkeit (DIP) bedeutet, dass wir unsere Abhängigkeiten immer dann umkehren sollten, wenn Code vorhanden ist, der von etwas Konkretem abhängt. Im Moment sieht unsere Abhängigkeitsstruktur so aus.

GameRunner, Verwenden der Funktionen in RunnerFunctions.php schafft ein Spiel Klasse und verwendet es dann. Auf der anderen Seite unsere Spiel Klasse, die unsere Geschäftslogik darstellt, erstellt und verwendet a Anzeige Objekt.

Der Läufer hängt also von unserer Geschäftslogik ab. Das ist richtig. Auf der anderen Seite unsere Spiel kommt drauf an Anzeige, das ist nicht gut. Unsere Geschäftslogik sollte niemals von unserer Präsentation abhängen.

Der einfachste technische Trick, den wir machen können, ist die Verwendung der abstrakten Konstrukte in unserer Programmiersprache. Eine traditionelle Klasse ist konkreter als eine abstrakte Klasse, die konkreter ist als eine Schnittstelle.

Ein Abstrakte Klasse ist ein spezieller Typ, der nicht initialisiert werden kann. Es enthält nur Definitionen und Teilimplementierungen. Eine abstrakte Basisklasse hat normalerweise mehrere untergeordnete Klassen. Diese untergeordneten Klassen erben die allgemeine Teilfunktionalität vom abstrakten übergeordneten Element. Sie fügen ihr eigenes erweitertes Verhalten hinzu und müssen alle im abstrakten übergeordneten übergeordneten Element definierten Methoden implementieren, jedoch nicht darin implementiert.

Ein Schnittstelle ist ein spezieller Typ, der nur die Definition von Methoden und Variablen erlaubt. Es ist das abstrakteste Konstrukt in der objektorientierten Programmierung. Jede Implementierung muss immer alle Methoden der übergeordneten Schnittstelle implementieren. Eine konkrete Klasse kann mehrere Schnittstellen implementieren.

Mit Ausnahme der objektorientierten Sprachen der C-Familie lassen andere Sprachen wie Java oder PHP keine Mehrfachvererbung zu. Eine konkrete Klasse kann also eine einzige abstrakte Klasse erweitern, kann jedoch, falls erforderlich, mehrere Schnittstellen implementieren. Oder aus einer anderen Perspektive kann eine einzelne abstrakte Klasse viele Implementierungen haben, während viele Schnittstellen viele Implementierungen haben können.

Für eine ausführlichere Erläuterung des DIP lesen Sie bitte das diesem SOLID-Prinzip gewidmete Tutorial.

Abhängigkeit über eine Schnittstelle umkehren

PHP unterstützt Schnittstellen vollständig. Ausgehend vom Anzeige Klasse als unser Modell könnten wir eine Schnittstelle mit den öffentlichen Methoden definieren, die alle Klassen, die für die Anzeige von Daten verantwortlich sind, implementieren müssen.

Anschauen AnzeigeIn der Liste der Methoden gibt es 12 öffentliche Methoden, einschließlich des Konstruktors. Dies ist eine ziemlich große Schnittstelle. Sie sollten diese Anzahl so niedrig wie möglich halten und Schnittstellen so offen legen, wie sie von den Clients benötigt werden. Das Interface-Segregationsprinzip hat dazu einige gute Ideen. Vielleicht werden wir versuchen, dieses Problem in einem zukünftigen Tutorial zu behandeln.

Was wir jetzt erreichen wollen, ist eine Architektur wie die unten.

Auf diese Weise statt Spiel je nach konkreter Anzeige, Beide hängen von der sehr abstrakten Schnittstelle ab. Spiel verwendet die Schnittstelle, während Anzeige implementiert es.

Schnittstellen benennen

Phil Karlton sagte: "In der Informatik gibt es nur zwei schwierige Dinge: Cache-Ungültigkeitserklärung und Namensgebung."

Obwohl wir uns nicht für Caches interessieren, müssen wir unsere Klassen, Variablen und Methoden benennen. Schnittstellen zu benennen kann eine Herausforderung sein.

In den alten Tagen der ungarischen Notation hätten wir es so gemacht.

Für dieses Diagramm haben wir die tatsächlichen Klassen- / Dateinamen und die tatsächliche Großschreibung verwendet. Die Schnittstelle heißt "IDisplay" mit einem großen "I" vor "Display". Es gab tatsächlich Programmiersprachen, die eine solche Benennung für Schnittstellen forderten. Ich bin sicher, es gibt ein paar Leser, die sie noch benutzen und gerade lächeln.

Das Problem bei diesem Benennungsschema ist das falsch platzierte Anliegen. Schnittstellen gehören zu ihren Kunden. Unsere Schnittstelle gehört zu Spiel. Somit Spiel muss nicht wissen, dass es eine Schnittstelle oder ein reales Objekt verwendet. Spiel muss sich nicht um die Implementierung kümmern, die es tatsächlich bekommt. Von Spiel's Sichtweise, es verwendet nur ein "Display", das ist alles.

Das löst das Spiel zu Anzeige Namensproblem. Die Verwendung des Suffix "Impl" für die Implementierung ist etwas besser. Es hilft, die Besorgnis zu beseitigen Spiel.

Es ist auch viel effektiver für uns. Denk an Spiel wie es gerade aussieht. Es verwendet eine Anzeige Objekt und weiß, wie man es benutzt. Wenn wir unser Interface "Display" nennen, reduzieren wir die Anzahl der erforderlichen Änderungen Spiel.

Dennoch ist diese Benennung nur unwesentlich besser als die vorherige. Es erlaubt nur eine Implementierung für Anzeige und der Name der Implementierung sagt uns nicht, über welche Art von Anzeige wir sprechen.

Das ist jetzt wesentlich besser. Unsere Implementierung wurde "CLIDisplay" genannt, da sie an die CLI ausgegeben wird. Wenn wir eine HTML-Ausgabe oder eine Windows-Desktop-Benutzeroberfläche wünschen, können wir all dies einfach zu unserer Architektur hinzufügen.

Zeigen Sie mir den Code

Da wir zwei Arten von Tests haben, den langsamen goldenen Master und die schnellen Unit-Tests, möchten wir uns so weit wie möglich auf Unit-Tests und so wenig wie möglich auf den goldenen Master verlassen. Lassen Sie uns unsere goldenen Meistertests als überspringen und auf unsere Unit-Tests verlassen. Sie gehen gerade vorbei und wir möchten eine Änderung vornehmen, die sie weiter bestehen lässt. Aber wie können wir so etwas tun, ohne alle oben vorgeschlagenen Änderungen vorzunehmen?

Gibt es eine Testmethode, mit der wir einen kleineren Schritt machen könnten??

Spott rettet den Tag

Es gibt einen solchen Weg. Beim Testen gibt es ein Konzept namens "Mocking".

Wikipedia definiert Mocking als solches: "In der objektorientierten Programmierung sind Scheinobjekte simulierte Objekte, die das Verhalten realer Objekte auf kontrollierte Weise nachahmen."

Ein solches Objekt wäre für uns eine große Hilfe. Tatsächlich brauchen wir nicht einmal etwas so Komplexes wie die Simulation des gesamten Verhaltens. Alles, was wir brauchen, ist ein falsches, dummes Objekt, an das wir schicken können Spiel anstelle der realen Anzeigelogik.

Die Schnittstelle erstellen

Erstellen wir eine Schnittstelle namens Anzeige mit allen öffentlichen Methoden der aktuellen konkreten Klasse.

Wie Sie sehen können, das Alte Display.php wurde in umbenannt DisplayOld.php. Dies ist nur ein vorübergehender Schritt, der es uns ermöglicht, ihn aus dem Weg zu räumen und uns auf das Interface zu konzentrieren.

Schnittstelle Anzeige  

Mehr brauchen Sie nicht, um eine Schnittstelle zu erstellen. Sie können sehen, dass es als "Schnittstelle" und nicht als "Klasse" definiert ist. Fügen wir die Methoden hinzu.

interface Anzeige function statusAfterRoll ($ rollsNumber, $ currentPlayer); function playerSentToPenaltyBox ($ currentPlayer); function playerStaysInPenaltyBox ($ currentPlayer); function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); function statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); Funktion playerAdded ($ playerName, $ numberOfPlayers); Funktion askQuestion ($ currentCategory); Funktion correctAnswer (); function correctAnswerWithTypo (); Funktion wrongAnswer (); Funktion SpielerCoins ($ currentPlayer, $ PlayerCoins);  

Ja. Eine Schnittstelle ist nur eine Reihe von Funktionsdeklarationen. Stellen Sie sich das als eine C-Header-Datei vor. Keine Implementierungen, nur Deklarationen. Es kann überhaupt keine Implementierung enthalten. Wenn Sie versuchen, eine der Methoden zu implementieren, führt dies zu einem Fehler.

Aber diese sehr abstrakten Definitionen erlauben uns etwas Wunderbares. Unsere Spiel Die Klasse hängt jetzt von ihnen ab und nicht von einer konkreten Implementierung. Wenn wir jedoch versuchen, unsere Tests auszuführen, schlagen diese fehl.

Schwerwiegender Fehler: Die Anzeige der Schnittstelle kann nicht instanziiert werden

Das ist, weil Spiel versucht, in der Zeile 25 im Konstruktor eine neue Anzeige selbst zu erstellen.

Wir wissen, dass wir das nicht können. Eine Schnittstelle oder eine abstrakte Klasse kann nicht instanziiert werden. Wir brauchen ein echtes Objekt.

Abhängigkeitsspritze

Wir brauchen ein Dummy-Objekt, um in unseren Tests verwendet zu werden. Eine einfache Klasse, die alle Methoden des implementiert Anzeige Schnittstelle, aber nichts tun. Schreiben wir es direkt in unseren Unit-Test. Wenn Ihre Programmiersprache nicht mehrere Klassen in derselben Datei zulässt, können Sie eine neue Datei für Ihre Dummy-Klasse erstellen.

Die Klasse DummyDisplay implementiert die Funktion Display function statusAfterRoll ($rollNumber, $ currentPlayer) // TODO: Die Methode statusAfterRoll ().  function playerSentToPenaltyBox ($ currentPlayer) // TODO: Implementiert die playerSentToPenaltyBox () -Methode.  function playerStaysInPenaltyBox ($ currentPlayer) // TODO: Implementiert die playerStaysInPenaltyBox () -Methode.  function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementiert die statusAfterNonPenalizedPlayerMove () - Methode.  function statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementiert die Methode statusAfterPlayerGettingOutOfPenaltyBox ().  Funktion playerAdded ($ playerName, $ numberOfPlayers) // TODO: Implementiere die playerAdded () - Methode.  Funktion askQuestion ($ currentCategory) // TODO: Implementiert die askQuestion () -Methode.  function correctAnswer () // TODO: Implementiere die correctAnswer () -Methode.  function correctAnswerWithTypo () // TODO: Implementiere die correctAnswerWithTypo () - Methode.  function wrongAnswer () // TODO: Implementieren Sie die wrongAnswer () -Methode.  function playerCoins ($ currentPlayer, $ playerCoins) // TODO: Implementiert die playerCoins () -Methode). 

Sobald Sie sagen, dass Ihre Klasse eine Schnittstelle implementiert, können Sie in der IDE die fehlenden Methoden automatisch eingeben. Dadurch ist das Erstellen solcher Objekte in wenigen Sekunden sehr schnell.

Jetzt lass es uns benutzen Spiel indem Sie es in seinem Konstruktor initialisieren.

function __construct () $ this-> players = array (); $ this-> places = Array (0); $ this-> Geldbörsen = Array (0); $ this-> inPenaltyBox = Array (0); $ this-> display = new DummyDisplay (); 

Dies führt den Test durch, führt jedoch zu einem großen Problem. Spiel muss über seinen Test Bescheid wissen. Wir wollen das wirklich nicht. Ein Test ist nur ein weiterer Einstiegspunkt. Das DummyDisplay ist nur eine andere Benutzeroberfläche. Unsere Geschäftslogik, die Spiel Klasse, sollte nicht von der Benutzeroberfläche abhängen. Lassen Sie uns es also nur von der Schnittstelle abhängig machen.

Funktion __construct (Anzeige $ display) $ this-> players = array (); $ this-> places = Array (0); $ this-> Geldbörsen = Array (0); $ this-> inPenaltyBox = Array (0); $ this-> display = $ display; 

Aber um zu testen Spiel, Wir müssen die Dummy-Anzeige aus unseren Tests einsenden.

function setUp () $ this-> game = new Game (neues DummyDisplay ()); 

Das ist es. In unseren Unit-Tests mussten wir eine einzige Zeile ändern. Im Setup senden wir als Parameter eine neue Instanz von DummyDisplay. Das ist eine Abhängigkeitsinjektion. Die Verwendung von Schnittstellen und das Injizieren von Abhängigkeiten hilft insbesondere, wenn Sie in einem Team arbeiten. Wir bei Syneto haben festgestellt, dass die Angabe eines Schnittstellentyps für eine Klasse und das Einfügen dieser Klasse uns helfen wird, die Absichten des Client-Codes viel besser zu kommunizieren. Jeder, der sich den Client anschaut, weiß, welcher Objekttyp in den Parametern verwendet wird. Und ein cooler Bonus ist, dass Ihre IDE die Methoden für diese Parameter automatisch vervollständigt, da sie ihre Typen bestimmen können.

Eine echte Implementierung für Golden Master

Der goldene Master-Test führt unseren Code wie in der realen Welt aus. Damit dies funktioniert, müssen wir unsere alte Anzeigeklasse in eine reale Implementierung der Schnittstelle umwandeln und sie in unsere Geschäftslogik einschicken. Hier ist eine Möglichkeit, dies zu tun.

Klasse CLIDisplay implementiert Anzeige //… //

Benennen Sie es in um CLIDisplay und machen es umzusetzen Anzeige.

Funktion 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 ())); 

Im RunnerFunctions.php, in dem Lauf() erstellen Sie eine neue Anzeige für CLI und übergeben Sie diese an Spiel wenn es erstellt ist.

Kommentieren Sie und führen Sie Ihre goldenen Meistertests durch. Sie werden passieren.

Abschließende Gedanken

Diese Lösung führt effektiv zu einer Architektur wie in der folgenden Abbildung.

Nun erstellt unser Game Runner, der den Einstiegspunkt in unsere Anwendung darstellt, einen konkreten Aspekt CLIDisplay und hängt daher davon ab. CLIDisplay hängt nur von der Schnittstelle ab, die an der Grenze zwischen Präsentation und Geschäftslogik liegt. Unser Läufer hängt auch direkt von der Geschäftslogik ab. So sieht unsere Anwendung aus, wenn Sie auf die saubere Architektur projizieren, mit der wir diesen Artikel begonnen haben.

Vielen Dank für das Lesen und verpassen Sie nicht das nächste Tutorial, wenn wir ausführlicher über Spott und Klasseninteraktion sprechen werden.