Hallo Spaß-Coder.
In unserem Artikel Warum so gemustert? haben wir erläutert, welche Bedeutung Muster in der Softwareentwicklung haben. Weiterhin haben wir angekündigt, weitere Entwurfsmuster vorzustellen, womit wir uns auch in diesem Artikel auseinander setzten werden.
Wer von euch hat schon einmal ein großes Ganzes bestehend aus mehreren Einzelteilen per Software entwickelt, wie zum Beispiel ein Brettspiel, ein Auto, eine Spielwelt oder ein Haus? All diese Dinge haben gemeinsam, dass sie aus bestimmten Komponenten bestehen. So hat ein Auto üblicherweise eine Anzahl Räder, eine Karosserie, ein Motor – reicht schon fast, oder? 😉 Dabei spielt es keine Rolle, um welches Auto es sich tatsächlich handelt.
Die naheliegende schnelle Implementierung erfolgt vermutlich sehr konkret. Wenn wir ein Schachspiel entwickeln wollen, würden wir ein passendes Spielbrett erstellen sowie die zugehörigen Spielfiguren. Was nun, wenn wir ein weiteres Brettspiel entwickeln wollen? Natürlich können wir dies einfach wieder konkret implementieren. Wer hier genauer hinschaut, entdeckt jedoch schon ein Muster. Ein Brettspiel besteht immer aus einem Spielbrett und Spielfiguren. Diese sind natürlich unterschiedlich in ihren Ausprägungen, aber lassen sich entsprechend kategorisieren.
Der konkrete Hausbau
Wechseln wir das Beispiel und bauen uns ein Haus 🙂 Na gut, ein Fertighaus. Zur Vereinfachung unseres Vorhabens genügt uns ein Haus bestehend aus vier Wänden, einem Dach und einer Türe. Je nach Art des Hauses bestehen diese Komponenten aus unterschiedlichen Materialien.
Ein Baumhaus wird vermutlich selten aus Stein, sondern überwiegend aus Holz gebaut und anstelle einer einbruchsicheren Stahltüre verzichten wir für einen schönen Ausblick einfach auf die Eingangstüre.
Erstellen wir dazu eine entsprechende Klasse für die Konstruktion der genannten Komponenten, könnte diese wie folgt aussehen:
1 2 3 4 5 6 7 8 9 |
public class TreeHouseFactory { Wall createWall() { return new WoodenWall(); } Roof createRoof() { return new WoodenRoof(); } } |
Hier haben wir eine kleine Fabrik für den Baumhausbau. Wir verwenden die beiden Komponenten Wall und Roof, welche hier aus Holz erstellt werden. Soweit so gut.
Wie es so ist kommt nun die nächste Anforderung und wir sollen neben unserem Baumhaus auch ein Country-Haus erzeugen. Dieses soll aus Holzwänden, einem Holzdach sowie einer Holztüre bestehen. Nichts leichter als das.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class CountryHouseFactory { Door createDoor() { return new WoodenDoor(); } Wall createWall() { return new WoodenWall(); } Roof createRoof() { return new WoodenRoof(); } } |
Wer von euch besitzt nun scharfe Augen und erkennt im Vergleich mit dem Baumhaus ein Muster? Angenommen wir hätten das Baumhaus auch mit einer Türe ausgestattet, hätten wir die gleichen Komponenten(-arten) verwendet.
Dies habt ihr wahrscheinlich schon aus der Aussage abgelesen, dass ein Haus grundsätzlich aus bestimmten Komponenten besteht. Welche Möglichkeiten haben wir nun, dieses Muster nutzbar zu machen?
Der abstrakte Hausbau
Schauen wir uns das Entwurfsmuster Abstrakte Fabrik (Abstract Factory) im Zusammenhang mit unserem Bauvorhaben für die Häuser an und wie es uns dabei helfen kann, den Code wiederverwendbar und sauber zu strukturieren.
Also noch einmal ganz langsam. Wir benötigen für ein Haus immer Wände, ein Dach und eine Türe – mit der Option, die Türe auch weg zu lassen. Dann machen wir daraus doch eine abstrakte Fabrik für Häuser.
1 2 3 4 5 |
public abstract class HouseFactory { abstract Door createDoor(); abstract Wall createWall(); abstract Roof createRoof(); } |
Genau dies ist das Muster! Egal welches Haus wir bauen wollen, diese Komponenten sind immer mit dabei. Die beiden Fabriken für unser Baumhaus und das Country-Haus lassen sich dahingendend ganz leicht anpassen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class TreeHouseFactory extends HouseFactory { @Override Door createDoor() { return new NoDoor(); } @Override Wall createWall() { return new WoodenWall(); } @Override Roof createRoof() { return new WoodenRoof(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class CountryHouseFactory extends HouseFactory { @Override Door createDoor() { return new WoodenDoor(); } @Override Wall createWall() { return new WoodenWall(); } @Override Roof createRoof() { return new WoodenRoof(); } } |
Jede konkrete Fabrik legt damit fest, welche Komponentenarten jeweils dazu gehören. Die Zusammenstellung ist jedoch jedesmal identisch: Wände, Dach und Türe.
Wann ist dieses Entwurfsmuster sinnvoll anzuwenden?
Bei diesem Muster geht es darum, Produktfamilien zu erzeugen. Damit sind zusammengehörige Komponenten gemeint, welche in einer bestimmten Konstellation wiederholt auftreten. Zum Beispiel bei GUI-Bibliotheken können verschiedene Look & Feels über dieses Muster abgebildet werden.
Dabei herauszufinden, wann das Muster sinnvoll eingesetzt werden kann, spielt insbesondere das Open-Closed-Prinzip eine wichtige Rolle. Wie wir in dem Prinzip gelernt haben, wollen wir unseren Code offen für Erweiterungen halten und gleichzeitig geschlossen gegenüber Änderungen. Immer dann, wenn wir das Schlüsselwort new verwenden, erzeugen wir eine konkrete Abhängigkeit. Immer dann, wenn wir eine konkrete Abhängigkeit haben, verletzen wir das Open-Closed-Prinzip, da wir an dieser Stelle keine Erweiterungen vornehmen können, ohne den bestehenden Code zu verändern.
Wie aber erkennen wir das? Hierzu ist es notwendig, dass wir uns gedanklich von unserem Code trennen und uns auf die konzeptionelle Ebene unserer implementierten Lösung begeben. Hier müssen wir nun Codeteile identifizieren, die sich ändern können und von denen trennen, die gleich bleiben. Für unser Häuserbeispiel bedeutet das:
- Bestandteile der einzelnen Häusertypen ändern sich
- Der Vorgang des Hausbaus bleibt gleich
Gleiche Codeteile bleiben unverändert. In unserem Fall sind das die Komponenten selbst und der Client, der diese verwendet, da dieser gegen eine Schnittstelle programmiert ist. Neue Häusertypen mit veränderten Komponenten lassen sich aber problemlos über neue, konkrete Fabriken ergänzen.
Just-in-time Erzeugung
Wie aber legen wir fest, welche Fabrik wir nun benutzen? Da haben wir doch wieder eine konkrete Abhängigkeit, oder?
Ja, das haben wir tatsächlich, sobald wir die Fabrik mit new instanziieren. Diese Abhängigkeit lässt sich auch nicht auflösen, irgendwann muss ja festgelegt werden, welche Fabrik nun den Bauauftrag erhalten soll. Wir können die Abhängigkeit aber über eine Programmiertechnik abschwächen, nämlich Dependency Injection. Darüber legen wir erst zur Laufzeit fest, welche Fabrik tatsächlich verwendet wird.
Zusammenfassung
Wir nutzen die Möglichkeit der abstrakten Fabrik für die Erzeugung unserer Instanzen, um die benötigten Klassen nicht direkt zu instanziieren, sondern die Erzeugung von der Verwendung zu trennen. Dadurch können wir Teile unseres Codes, der sich voraussichtlich ändern wird von den Teilen trennen, die sich nicht ändern werden. Dies ist eines der Prinzipien guter Software-Entwicklung.
Wir hoffen euch ein weiteres Werkzeug zu eurem Köfferchen hinzugefügt zu haben, dass euch dabei hilft, ein besserer Programmierer zu werden.
Eure Spaß-Coder
Den Code zu unserem Beispiel könnt ihr auf Github finden:
- https://github.com/invidit/CodeQuality/tree/master/DesignPattern/AbstractFactory
Dieser Artikel basiert neben unseren Erfahrungen auf den Ausführungen aus: