Wie versprochen kommt hier der zweite Teil meiner Reihe zu Entwurfsmustern. Diesmal dreht sich alles um die abstrakte Fabrik.
Der Beispielcode kann in dem GitHub Repository seism0saurus/design-pattern-abstractfactory eingesehen werden. Ich habe ein Package für den Quellcode ohne das Entwurfsmuster und ein Package für den Code mit dem Entwurfsmuster erstellt. Beide enthalten eine Example Klasse, deren Main Methode ausgeführt werden kann.
Zweck
Wie bereits bei der Fabrik(methode) geht es auch bei der abstrakten Fabrik darum, das Erzeugen von Objekten für den Client zu vereinfachen. Um zueinander passende Objekte einer Objektfamilie zu erstellen, erzeugt die abstrakte Fabrik familienspezifische Fabriken. Diese implementieren alle ein Interface, über das der Client dann passende Objekte erzeugen kann.
Beispiel 3 Gänge Menüs unterschiedlicher Küchen
Da ich gerne koche, habe ich als Beispiel für die abstrakte Fabrik unterschiedliche Menüs gewählt. Außer sehr experimentierfreudige EsserInnen erwartet man normalerweise, dass die verschiedenen Gänge eines Menüs zueinander passen, dass die Geschmäcker harmonieren oder gewollte Kontraste ergeben. Meistens möchte man nicht, dass in meinem koreanischen Menü plötzlich ein Tomaten Mozzarella Salat auftaucht.
Vorher
Ziel des Beispielprogramms ist es, je nach Geschmack ein Menü zusammenzustellen. Die Fabrik kennen wir noch aus dem letzten Beitrag und deshalb habe ich sie im Code vor der Einführung unserer abstrakten Fabrik bereits eingebaut. Der Client baut sich eine zum Geschmack passende MenuFabric und erzeugt darüber drei Gerichte, die er zu einem Menü kombiniert.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Example {
public static void main(String[] args) {
String flavour = "korean";
switch (flavour) {
case "vegetarian" -> {
VegetarianMenuFactory vegetarianMenuFactory = new VegetarianMenuFactory();
TomatoMozzarellaPesto tomatoMozarellaPesto = vegetarianMenuFactory.getTomatoMozarellaPesto();
CeleryCutlet celeryCutlet = vegetarianMenuFactory.getCeleryCutlet();
ChocolateLavaCake chocolateLavaCake = vegetarianMenuFactory.getChocolateLavaCake();
System.out.println("First course: " + tomatoMozarellaPesto);
System.out.println("Main course: " + celeryCutlet);
System.out.println("Desert: " + chocolateLavaCake);
}
case "mexican" -> {
MexicanMenuFactory mexicanMenuFactory = new MexicanMenuFactory();
NopalitosSalad nopalitosSalad = mexicanMenuFactory.getNopalitosSalad();
Enchiladas enchiladas = mexicanMenuFactory.getEnchiladas();
OrangeBiscuits orangeBiscuits = mexicanMenuFactory.getOrangeBiscuits();
System.out.println("First course: " + nopalitosSalad);
System.out.println("Main course: " + enchiladas);
System.out.println("Desert: " + orangeBiscuits);
}
case "korean" -> {
KoreanMenuFactory koreanMenuFactory = new KoreanMenuFactory();
KoreanSpringRoll koreanSpringRoll = koreanMenuFactory.getKoreanSpringRoll();
TofuWithVegetables tofuWithVegetables = koreanMenuFactory.getTofuWithVegetables();
HoneyCakeFromRice honeyCakeFromRice = koreanMenuFactory.getHoneyCakeFromRice();
System.out.println("First course: " + koreanSpringRoll);
System.out.println("Main course: " + tofuWithVegetables);
System.out.println("Desert: " + honeyCakeFromRice);
}
}
}
}
Hier haben wir mehrere Probleme. Zum einen muss der Client genau wissen, welche Fabrik er benötigt, um ein zum Geschmack passendes Menü zu erzeugen. Zum anderen kann er auch nicht davon abgehalten werden, eine Frühlingsrolle mit den Enchiladas zu kombinieren. Es könnte aus Versehen auch passieren, dass der Schokokuchen mit flüssigem Kern als Hauptgericht serviert wird. Insgesamt ist das Intefacer für den Client müham und fehleranfällig zu benutzen.
Außerdem muss der Clientcode beim Hinzufügen einer neuen Küche oder beim Austausch eines Ganges angepasst werden.
Nachher oder die abstrakte Fabrik in der Praxis
Um den Client von den einzelnen Fabriken für die verschiedenen Küchen zu entkoppeln, führen wir eine abstrakte Fabrik ein. Diese gibt je nach ausgewählter Küche eine konkrete Implementierung des Fabrikinterfaces zurück. Das Fabrikinterface verfügt über drei Methoden, die jeweils einen ersten Gang, einen Hauptgang oder eine Nachspeise liefern. Diese sind ebenfalls als Interfaces implementiert.
Ruft der Client nun die abstrakte Fabrik auf, erhält er eine konkrete Implementierung, z.B. die KoreanMenuFactory. Beim aufruf der Fabrikmethoden erhält der Client konkrete Gerichte, die die Interfaces der Gänge implementieren. Auf diese Art ist der Client komplett von den konkreten Implementierungen entkoppelt. Es können sogar neue Küchen hinzugefügt oder einzelne Gerichte ausgetauscht werden ohne dass der Client angepasst werden muss.
Kernstück der abstrakten Fabrik ist das Interface MenuFactory und die AbstractMenuFactory. Sie ist eine abstrakte Klasse mit einer statischen Methode,
die die eigentlichen Implementierungen der MenuFactory erzeugt. Dafür benutze ich wieder die
Switch Expressions aus java 12
und ein ENUM Flavour
.
Fangen wir mit dem Interface für die konkreten Fabriken an.
1
2
3
4
5
6
7
8
public interface MenuFactory {
FirstCourse getFirstCourse();
MainCourse getMainCourse();
Dessert getDesert();
}
Damit die verschiedenen Gerichte mit dem neuen MenuFactory Interface zusammenarbeiten können, müssen sie jeweils eines der drei Interfaces für die drei Gänge implementieren.
1
2
public interface Dessert {
}
1
2
3
4
5
6
public class HoneyCakeFromRice implements Dessert {
@Override
public String toString() {
return "Desert: Sweet honey cakes from rice (Bu Tshim Dǒk)";
}
}
Die drei bestehenden Fabriken für die Menüs werden so angepasst, dass sie das MenuFactory Interface implementieren.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class KoreanMenuFactory implements MenuFactory {
@Override
public FirstCourse getFirstCourse() {
return new KoreanSpringRoll();
}
@Override
public MainCourse getMainCourse() {
return new TofuWithVegetables();
}
@Override
public Dessert getDesert() {
return new HoneyCakeFromRice();
}
}
Jetzt fehlen noch das Enum für Typsicherheit und die eigentliche abstrakte Fabrik.
1
2
3
public enum Flavour {
VEGETARIAN, MEXICAN, KOREAN
}
1
2
3
4
5
6
7
8
9
10
public abstract class AbstractMenuFactory {
public static MenuFactory getFactory(Flavour flavour) {
return switch (flavour) {
case VEGETARIAN -> new VegetarianMenuFactory();
case MEXICAN -> new MexicanMenuFactory();
case KOREAN -> new KoreanMenuFactory();
};
}
}
Damit haben wir alle nötigen Komponenten der abstrakten Fabrik implementiert und können die Beispielklasse anpassen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Example {
public static void main(String[] args) {
MenuFactory menuFactory = AbstractMenuFactory.getFactory(Flavour.KOREAN);
FirstCourse firstCourse = menuFactory.getFirstCourse();
MainCourse mainCourse = menuFactory.getMainCourse();
Dessert dessert = menuFactory.getDesert();
System.out.println(firstCourse);
System.out.println(mainCourse);
System.out.println(dessert);
}
}
Der Clientcode ist sehr viel schlanker. Als Client muss man sich nicht mehr darum kümmern, in welcher Reihenfolge die Gerichte zusammengestellt werden müssen oder ob sie zu einander passen. Er muss lediglich die Küche festlegen und die abstrakte Fabrik liefert ihm die passende Fabrikimplementierung.
Da der Client nur gegen die Interfaces der MenuFabrik und der Gänge programmiert ist, können diese leicht ausgetauscht oder um neue Küchen ergänzt werden.
Vorteile
Die Vorteile des Entwurfsmusters abstrakte Fabrik sind:
- Alle erzeugten Objekte arbeiten miteinander zusammen.
- Der Client ist nur lose an die erzeugten Objekte gekoppelt.
- Die Erzeugung der Objekte ist jeweils in einer Klasse implementiert.
- Es können neue Objektfamilien eingebaut oder einzelne Objekte ausgetauscht werden.
Nachteile
Der Code wird deutlich komplexer, da eine zahlreiche Interfaces implementiert werden müssen.
Zusammenfassung
Die abstrakte Fabrik ist ein etwas komplizierteres Enwturfsmuster, dass uns aber eine hohe Flexibilität und eine gute Kapselung bietet.
Im nächsten Artikel stelle ich euch dann das Singleton
oder das Einzelstück
vor, eines der bekannteren Entwurfsmuster.
Bis bald,
seism0saurus