Übung »Refactoring der "Bauzeichner 2.0"-Software aus dem Video nach SOLID und Clean Code«
Das im Video zu den SOLID-Prinzipien vorgestellte Beispiel der “Bauzeichner 2.0”-Software wird in dieser Übung einmal gründlich gefactored - gemäß der Prinzipien von SOLID und Clean Code.
- Dauer
- Ca. 300 min
- Video(s) hierzu
-
Worum geht es?
Alle nachfolgenden Schritte beziehen sich auf das Beispiel-Repo
Bauzeichner2.0-CleanCode-SOLID. Das Repo ist
so aufgebaut, dass es zwei Java-Projekte enthält.
before_refactoring ist die Version, die DDD- und SOLID-Prinzipien verletzt. Alle entsprechenden Tests
sind rot (bzw. gelb, in IntelliJ). Funktional ist die Implementierung ok (wenn auch unvollständig und
sehr lieblos runtergehauen …), was man daran sieht, dass die funktionalen Unit-Tests im
Package functionaltests grün sind.
after_refactoring enthält die Variante nach dem Refactoring.
Hinweis: Wegen der zwei Projekte sollten Sie also nicht den Top-Level-Ordner bauzeichner2.0-cleancode-solid
in IntelliJ öffnen, sondern die beiden unterliegenden Ordner before_refactoring und after_refactoring in
zwei verschiedenen IntelliJ-Instanzen.
Schritt 1: Clean-Code-Prinzipien anwenden
Schauen Sie sich das Video an, und finden Sie Verstöße gegen die Clean-Code-Prinzipien.
Das PMD-Plugin für IntelliJ kann Ihnen dabei helfen.
- Gehen Sie die Refactoring-Schritte durch.
- Ich werde das ebenfalls im Live-Coding machen.
- Anschließend schauen Sie Ihren eigenen Code aus Meilenstein M0 an. Welche Clean-Code-Prinzipien verletzen Sie dort?
Lösung anzeigen
(Die Lösung finden Sie - genau wie auch bei den nachfolgenden Schritten 2 … 3c - in dem Projekt after_refactoring.)
Schritt 2: Package-Struktur und Namenskonventionen entsprechend DDD-Schichtenarchitektur
Machen Sie gemäß unserer Konventionen für die DDD-Schichtenarchitektur
ein Refactoring des Übungs-Repos, so dass die DDD-Konventionen eingehalten werden. Denken Sie auch an die Namenskonventionen.
Schritt 3a: SOLID-Prinzipien anwenden - Single Responsibility Principle
Finden Sie den hauptsächlichen Verstoß gegen das Single Responsibility Principle (SRP) im Code. Beheben Sie ihn.
Schritt 3b: SOLID-Prinzipien anwenden - Open-Closed Principle
Finden Sie den hauptsächlichen Verstoß gegen das Open-Closed Principle (OCP) im Code. Beheben Sie ihn.
Schritt 3c: SOLID-Prinzipien anwenden - Dependency Inversion Principle
Finden Sie den hauptsächlichen Verstoß gegen das Dependency Inversion Principle (DIP) im Code. Beheben Sie ihn.
Lösung anzeigen
Lösung
Das allgemeine Vorgehen ist auf einer eigenen Infoseite
beschrieben. Wenn Sie sich das Projekt before_refactoring anschauen, dann haben die Klassen Canvas und DrawingElement
die folgende zyklische Abhängigkeit:

Die Abhängigkeiten sind auch beide tatsächlich nötig, denn Canvas muss ausgeben können, welche Elemente
es enthält, bzw. welche Nachbar-Elemente ein DrawingElement hat. Sonst kann sich das Element nicht
z.B. in der Größe skalieren, denn es darf ja nicht mit Nachbarn kollidieren. Dafür gibt es die Methode
public List<DrawingElement> getNeighboursOf( DrawingElement drawingElement ) {
// ...
}
in Canvas. Auf der anderen Seite muss DrawingElement seine Zeichenfläche kennen, sonst kann es
keine Move-Befehle ausführen (es wüsste ja sonst nicht, wie weit es z.B. nach links oder rechts
rücken darf). Diese gegenseitige Abhängigkeit wird man also nicht so leicht los. Nach Schritt 2
(Anlegen der passenden Package-Strukturen) und Schritt 3a (Aufbrechen von DrawingElement in
die beiden Klassen Door und Window sieht dieser Zykel so aus (domain und application-Packages
sind aus Übersichtlichkeitsgründen nicht gezeigt):

Abhilfe durch das Dependency Inversion Principle
Mit dem Dependency Inversion Principle kann man diese Abhängigkeiten aufheben. (Schauen Sie sich
bitte das entsprechende Video dazu an, siehe oben auf dieser Seite!) Dafür muss man auf der
Seite des Canvas-Package Interfaces definieren, die sozusagen die “Erwartungen” der Canvas-Seite
ausdrücken.
- Man braucht ein Interface
Drawable, das aus Sicht von Canvas alle Eigenschaften und nötigen
Methoden eines Zeichenelements enthält.
- Zusätzlich braucht man noch ein Service-Interface
DrawableServiceInterface, das
die Methoden für das Lifecycle-Management eines Drawable vorgibt.
- Beide Interfaces werden dann tatsächlich auf der
DrawingElement-Seite implementiert.
Die Implementieren referenzieren die Interfaces auf der Canvas-Seite.
Damit ergibt sich folgendes Bild (die neuen Klassen sind rot eingefärbt). Am besten schauen Sie sich
das einmal im Code an. Die Abhängigkeiten sind dadurch tatsächlich nicht mehr zyklisch.
Jetzt kennt das DrawingElement-Package zwar Canvas, aber nicht mehr umgekehrt.

Kleine Abwandlung aufgrund der Randbedingungen von Spring JPA: Abstrakte Klasse statt Interface
Wie Sie im Code sehen, entspricht die Implementierung nicht 100% der obigen Skizze. Drawable
ist tatsächlich eine abstrakte Klasse, kein Interface. Das ist deswegen nötig, weil Spring JPA
keine Interfaces via @OneToMany referenzieren kann, wohl aber abstrakte Klassen. Am Prinzip
ändert dieses Detail aber nichts. Die korrekte Modellierung ist dann so:
