Refactoring Legacy Code: Teil 8 - Die Abhängigkeiten für eine saubere Architektur invertieren
German (Deutsch) translation by Katharina Grigorovich-Nevolina (you can also view the original English article)
Alter Code. Hässlicher Code. Komplizierter Code. Spaghetti-Code. Gibberischer Unsinn. In zwei Worten, Legacy Code. Das ist eine Serie, die Ihnen hilft, damit zu arbeiten und umzugehen.
Jetzt ist es Zeit, über Architektur zu sprechen und darüber, wie wir unsere neu gefundenen Codelayers organisieren. Es ist Zeit, unsere Anwendung zu nehmen und zu versuchen, sie dem theoretischen Architekturdesign zuzuordnen.
Saubere Architektur
Das haben wir in unseren Artikeln und Tutorials gesehen. Saubere Architektur.



Auf hoher Ebene sieht es wie das obige Schema aus und ich bin sicher, dass Sie bereits damit vertraut sind. Es ist eine vorgeschlagene architektonische Lösung von Robert C. Martin.
Im Zentrum unserer Architektur steht unsere Geschäftslogik. Das sind die Klassen, die die Geschäftsprozesse darstellen, die unsere Anwendung zu lösen versucht. Das sind die Entitäten und Interaktionen, die die Domäne unseres Problems darstellen.
Darüber hinaus gibt es verschiedene 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 von ihnen sind unverzichtbar. Sie stellen die Verbindung zwischen dem Benutzer und unserer Anwendung über einen Übermittlungsmechanismus her. In unserem Fall ist dies eine Befehlszeilenschnittstelle. Es gibt einen weiteren Satz von Hilfsklassen, die unsere Geschäftslogik mit unserer Persistenzschicht und allen Daten in diesem Layer verbinden, aber wir haben kein solches Layer in unserer Anwendung. Dann gibt es helfende Klassen wie Fabriken und Bauherren, die neue Objekte für unsere Geschäftslogik konstruieren und bereitstellen. Schließlich gibt es die Klassen, die den Einstiegspunkt in unser System darstellen. In unserem Fall kann GameRunner als solche Klasse angesehen werden, oder alle unsere Tests sind auf ihre Weise 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 auf magische Weise erscheinen könnten und wir sehen könnten, was direkt in unserem Computer passiert, sollten sie funktionsfähig sein. Unsere Geschäftslogik muss ohne Benutzeroberfläche oder ohne Persistenzschicht funktionieren können. Unsere Geschäftslogik muss isoliert in einer Blase eines logischen Universums existieren.
Das Prinzip der Abhängigkeitsinversion
A. Hoch-Ebene-Module sollten nicht von Niedrig-Ebene-Modulen abhängen. Beides sollte von Abstraktionen abhängen.
B. Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.
Das ist das letzte SOLID-Prinzip und wahrscheinlich das mit der größten Auswirkung 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 sie von etwas Abstrakterem abhängen. Ihre Benutzeroberfläche ist sehr konkret, daher sollte sie von etwas Abstrakterem abhängen. Ihre Fabriken sind wieder sehr konkret. Aber was ist mit Ihrer Geschäftslogik? Innerhalb Ihrer Geschäftslogik sollten Sie diese Ideen weiterhin anwenden, damit die Klassen, die näher an den Grenzen liegen, von Klassen abhängen, die abstrakter sind und mehr im Zentrum Ihrer Geschäftslogik stehen.
Eine reine Geschäftslogik repräsentiert auf abstrakte Weise die Prozesse und Verhaltensweisen einer definierten Domäne oder eines Geschäftsmodells. Solche Geschäftslogik enthält keine Besonderheiten (konkrete Dinge) wie Werte, Geld, Kontonamen, Passwörter, die Größe einer Schaltfläche oder die Anzahl der Felder in einem Formular. Die Geschäftslogik sollte sich nicht um konkrete Dinge kümmern. Es sollte sich nur um Ihre Geschäftsprozesse kümmern.
Der technische Trick
Das Dependency Inversion Principle (DIP) besagt also, dass wir unsere Abhängigkeiten invertieren sollten, wenn es Code gibt, der von etwas Konkretem abhängt. Im Moment sieht unsere Abhängigkeitsstruktur so aus.



GameRunner erstellt mithilfe der Funktionen in RunnerFunctions.php eine Game-Klasse und verwendet sie dann. Andererseits erstellt und verwendet unsere Game-Klasse, die unsere Geschäftslogik darstellt, ein Display-Objekt.
Der Läufer hängt also von unserer Geschäftslogik ab. Das ist richtig. Andererseits hängt unser Game von Display ab, was nicht gut ist. Unsere Geschäftslogik sollte niemals von unserer Präsentation abhängen.
Der einfachste technische Trick, den wir machen können, besteht darin, die abstrakten Konstrukte in unserer Programmiersprache zu verwenden. Eine traditionelle Klasse ist konkreter als eine abstrakte Klasse, die konkreter ist als eine Schnittstelle.
Eine 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, fügen ihr eigenes erweitertes Verhalten hinzu und müssen alle im abstrakten übergeordneten Element definierten, aber nicht darin implementierten Methoden implementieren.
Eine 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 erlauben andere wie Java oder PHP keine Mehrfachvererbung. Eine konkrete Klasse kann also eine einzelne abstrakte Klasse erweitern, aber bei Bedarf auch mehrere Schnittstellen gleichzeitig implementieren. Oder aus einer anderen Perspektive ausgedrückt, eine einzelne abstrakte Klasse kann viele Implementierungen haben, während viele Schnittstellen viele Implementierungen haben können.
Eine ausführlichere Erläuterung des DIP finden Sie im Tutorial, das diesem SOLID Prinzip gewidmet ist.
Invertieren der Abhängigkeit mit Hilfe einer Schnittstelle
PHP unterstützt Schnittstellen vollständig. Ausgehend von der Display-Klasse als Modell könnten wir eine Schnittstelle zu den öffentlichen Methoden definieren, die alle Klassen, die für die Anzeige von Daten verantwortlich sind, implementieren müssen.



In der Methodenliste von Display 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 die Schnittstellen nach Bedarf der Clients verfügbar machen. Das Prinzip der Schnittstellentrennung enthält einige gute Ideen dazu. Vielleicht werden wir versuchen, dieses Problem in einem zukünftigen Tutorial zu lösen.
Was wir jetzt erreichen wollen, ist eine Architektur wie die folgende.



Auf diese Weise hängen beide anstelle von Game abhängig von dem konkreteren Display von der sehr abstrakten Oberfläche ab. Das Game verwendet die Benutzeroberfläche, während Display sie implementiert.
Schnittstellen benennen
Phil Karlton sagte: "In der Informatik gibt es nur zwei schwierige Dinge: die Ungültigmachung des Caches und das Benennen von Dingen."
Während wir uns nicht um Caches kümmern, müssen wir unsere Klassen, Variablen und Methoden benennen. Das Benennen von Schnittstellen kann eine ziemliche 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ßbuchstaben "I" vor "Display". Es gab tatsächlich Programmiersprachen, die eine solche Benennung für Schnittstellen erforderten. Ich bin mir sicher, dass es noch einige Leser gibt, die sie benutzen und gerade lächeln.
Das Problem bei diesem Namensschema ist das fehlgeleitete Problem. Schnittstellen gehören ihren Kunden. Unsere Oberfläche gehört zum Game. Daher darf Game nicht wissen, dass es eine Schnittstelle oder ein reales Objekt verwendet. Game darf sich nicht um die Implementierung kümmern, die es tatsächlich erhält. Aus Sicht des Game wird nur ein "Display" verwendet, das ist alles.



Dies löst das Benennungsproblem von Game to Display. Die Verwendung des Suffixes "Impl" für die Implementierung ist etwas besser. Es hilft, die Bedenken aus Game zu beseitigen.
Es ist auch viel effektiver für uns. Denken Sie an Game, wie es gerade aussieht. Es verwendet ein Display-Objekt und weiß, wie es verwendet wird. Wenn wir unsere Benutzeroberfläche "Anzeige" nennen, reduzieren wir die Anzahl der im Game erforderlichen Änderungen.
Trotzdem ist diese Benennung nur unwesentlich besser als die vorherige. Es ist nur eine Implementierung für Display zulässig, und der Name der Implementierung sagt nichts darüber aus, um welche Art von Anzeige es sich handelt.



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 problemlos zu unserer Architektur hinzufügen.



Zeigen Sie mir den Code
Da wir zwei Arten von Tests haben, den langsamen Golden Master und den schnellen Unit-Test, möchten wir uns so weit wie möglich auf Unit-Tests und so wenig wie möglich auf Golden Master verlassen. Markieren wir also unsere Golden Master-Tests als übersprungen und versuchen Sie, sich auf unsere Unit-Tests zu verlassen. Sie gehen gerade vorbei und wir wollen eine Änderung vornehmen, die sie weitergibt. 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önnen?
Spott rettet den Tag
Es gibt so einen 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."
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, das wir anstelle der eigentlichen Anzeigelogik an Game senden können.
Schnittstelle erstellen
Erstellen wir eine Schnittstelle namens Display mit allen öffentlichen Methoden der aktuellen konkreten Klasse.



Wie Sie sehen können, wurde die alte Display.php in DisplayOld.php umbenannt. Dies ist nur ein vorübergehender Schritt, der es uns ermöglicht, ihn aus dem Weg zu räumen und uns auf die Benutzeroberfläche zu konzentrieren.
1 |
interface Display { |
2 |
|
3 |
}
|
Das ist alles, 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.
1 |
interface Display { |
2 |
function statusAfterRoll($rolledNumber, $currentPlayer); |
3 |
function playerSentToPenaltyBox($currentPlayer); |
4 |
function playerStaysInPenaltyBox($currentPlayer); |
5 |
function statusAfterNonPenalizedPlayerMove($currentPlayer, $currentPlace, $currentCategory); |
6 |
function statusAfterPlayerGettingOutOfPenaltyBox($currentPlayer, $currentPlace, $currentCategory); |
7 |
function playerAdded($playerName, $numberOfPlayers); |
8 |
function askQuestion($currentCategory); |
9 |
function correctAnswer(); |
10 |
function correctAnswerWithTypo(); |
11 |
function incorrectAnswer(); |
12 |
function playerCoins($currentPlayer, $playerCoins); |
13 |
}
|
Ja. Eine Schnittstelle ist nur eine Reihe von Funktionsdeklarationen. Stellen Sie es sich als C-Header-Datei vor. Keine Implementierungen, nur Erklärungen. Es kann überhaupt keine Implementierung halten. Wenn Sie versuchen, eine der Methoden zu implementieren, führt das zu einem Fehler.
Aber diese sehr abstrakten Definitionen erlauben uns etwas Wunderbares. Unsere Game-Klasse hängt jetzt von ihnen ab, anstatt von einer konkreten Implementierung. Wenn wir jedoch versuchen, unsere Tests auszuführen, schlagen sie fehl.
1 |
Fatal error: Cannot instantiate interface Display |
Das liegt daran, dass Game versucht, in Zeile 25 im Konstruktor selbst eine neue Anzeige 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
Für unsere Tests benötigen wir ein Dummy-Objekt. Eine einfache Klasse, die alle Methoden der Display-Oberfläche implementiert, aber nichts tut. 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.
1 |
class DummyDisplay implements Display { |
2 |
|
3 |
function statusAfterRoll($rolledNumber, $currentPlayer) { |
4 |
// TODO: Implement statusAfterRoll() method.
|
5 |
}
|
6 |
|
7 |
function playerSentToPenaltyBox($currentPlayer) { |
8 |
// TODO: Implement playerSentToPenaltyBox() method.
|
9 |
}
|
10 |
|
11 |
function playerStaysInPenaltyBox($currentPlayer) { |
12 |
// TODO: Implement playerStaysInPenaltyBox() method.
|
13 |
}
|
14 |
|
15 |
function statusAfterNonPenalizedPlayerMove($currentPlayer, $currentPlace, $currentCategory) { |
16 |
// TODO: Implement statusAfterNonPenalizedPlayerMove() method.
|
17 |
}
|
18 |
|
19 |
function statusAfterPlayerGettingOutOfPenaltyBox($currentPlayer, $currentPlace, $currentCategory) { |
20 |
// TODO: Implement statusAfterPlayerGettingOutOfPenaltyBox() method.
|
21 |
}
|
22 |
|
23 |
function playerAdded($playerName, $numberOfPlayers) { |
24 |
// TODO: Implement playerAdded() method.
|
25 |
}
|
26 |
|
27 |
function askQuestion($currentCategory) { |
28 |
// TODO: Implement askQuestion() method.
|
29 |
}
|
30 |
|
31 |
function correctAnswer() { |
32 |
// TODO: Implement correctAnswer() method.
|
33 |
}
|
34 |
|
35 |
function correctAnswerWithTypo() { |
36 |
// TODO: Implement correctAnswerWithTypo() method.
|
37 |
}
|
38 |
|
39 |
function incorrectAnswer() { |
40 |
// TODO: Implement incorrectAnswer() method.
|
41 |
}
|
42 |
|
43 |
function playerCoins($currentPlayer, $playerCoins) { |
44 |
// TODO: Implement playerCoins() method.
|
45 |
}
|
46 |
}
|
Sobald Sie sagen, dass Ihre Klasse eine Schnittstelle implementiert, können Sie mit der IDE die fehlenden Methoden automatisch ausfüllen. Dadurch können solche Objekte in nur wenigen Sekunden sehr schnell erstellt werden.
Verwenden wir es jetzt im Game, indem wir es in seinem Konstruktor initialisieren.
1 |
function __construct() { |
2 |
|
3 |
$this->players = array(); |
4 |
$this->places = array(0); |
5 |
$this->purses = array(0); |
6 |
$this->inPenaltyBox = array(0); |
7 |
|
8 |
$this->display = new DummyDisplay(); |
9 |
}
|
Das führt dazu, dass der Test bestanden wird, führt jedoch zu einem großen Problem. Game muss über seinen Test Bescheid wissen. Das wollen wir wirklich nicht. Ein Test ist nur ein weiterer Einstiegspunkt. Das DummyDisplay ist nur eine weitere Benutzeroberfläche. Unsere Geschäftslogik, die Game-Klasse, sollte nicht von der Benutzeroberfläche abhängen. Lassen Sie es also nur von der Schnittstelle abhängen.
1 |
function __construct(Display $display) { |
2 |
|
3 |
$this->players = array(); |
4 |
$this->places = array(0); |
5 |
$this->purses = array(0); |
6 |
$this->inPenaltyBox = array(0); |
7 |
|
8 |
$this->display = $display; |
9 |
}
|
Um das Game zu testen, müssen wir jedoch die Dummy-Anzeige aus unseren Tests einsenden.
1 |
function setUp() { |
2 |
$this->game = new Game(new DummyDisplay()); |
3 |
}
|
Das ist es. Wir mussten in unseren Unit-Tests eine einzelne Zeile ändern. Im Setup senden wir als Parameter eine neue Instanz von DummyDisplay. Das ist eine Abhängigkeitsinjektion. Die Verwendung von Schnittstellen und die Abhängigkeitsinjektion hilft insbesondere, wenn Sie in einem Team arbeiten. Wir bei Syneto haben festgestellt, dass die Angabe eines Schnittstellentyps für eine Klasse und deren Einfügung uns dabei helfen wird, die Absichten des Client-Codes viel besser zu kommunizieren. Jeder, der den Client betrachtet, weiß, welcher Objekttyp in den Parametern verwendet wird. Ein cooler Bonus ist, dass Ihre IDE die Methoden für diese Parameter automatisch vervollständigt, da sie deren Typen bestimmen können.
Eine echte Implementierung für Golden Master
Der Golden Master Test führt unseren Code wie in der realen Welt aus. Damit dies erfolgreich ist, müssen wir unsere alte Anzeigeklasse in eine echte Implementierung der Schnittstelle umwandeln und in unsere Geschäftslogik einbinden. Hier ist eine Möglichkeit, dies zu tun.
1 |
class CLIDisplay implements Display { |
2 |
// ... //
|
3 |
}
|
Benennen Sie es in CLIDisplay um und lassen Sie es Display implementieren.
1 |
function run() { |
2 |
$display = new CLIDisplay(); |
3 |
$aGame = new Game($display); |
4 |
$aGame->add("Chet"); |
5 |
$aGame->add("Pat"); |
6 |
$aGame->add("Sue"); |
7 |
|
8 |
do { |
9 |
$dice = rand(0, 5) + 1; |
10 |
$aGame->roll($dice); |
11 |
} while (!didSomebodyWin($aGame, isCurrentAnswerCorrect())); |
12 |
}
|
Erstellen Sie in RunnerFunctions.php in der Funktion run() eine neue Anzeige für die CLI und übergeben Sie sie beim Erstellen an Game.
Kommentieren Sie aus und führen Sie Ihre Golden Master-Tests durch. Sie werden vergehen.
Abschließende Gedanken
Diese Lösung führt effektiv zu einer Architektur wie in der folgenden Abbildung.



Jetzt erstellt unser Game Runner, der der Einstiegspunkt für unsere Anwendung ist, ein konkretes CLIDDisplay und hängt daher davon ab. CLIDisplay hängt nur von der Schnittstelle ab, die sich an der Grenze zwischen Präsentation und Geschäftslogik befindet. Unser Läufer hängt auch direkt von der Geschäftslogik ab. So sieht unsere Anwendung aus, wenn sie auf die saubere Architektur projiziert wird, mit der wir diesen Artikel begonnen haben.
Vielen Dank für das Lesen und verpassen Sie nicht das nächste Tutorial, in dem wir ausführlicher über Spott und Klasseninteraktion sprechen werden.



