Im letzten Artikel hatte ich angekündigt, die verschiedenen Entwurfsmuster der Gang of Four einzeln vorzustellen. Hier kommt nun der erste Artikel zum Entwurfsmuster Fabrik oder dem fabric pattern.
Der Beispielcode kann in dem GitHub Repository seism0saurus/design-pattern-factory 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
Die Fabrik ist ein Erzeugungsmuster und hilft dabei die aufrufende Klasse (Client) von den tatsächlichen Implementierungen eines Interfaces zu entkoppeln. Der Client muss sich weder um die Erzeugung an sich kümmern noch muss er wissen, welche Implementierung er wann erzeugen muss. Diese Aufgabe übernimmt eine Fabrik bzw. eine Fabrikmethode, die je nach übergebenen Parametern ein passendes Objekt erzeugt. Die Objekte implementieren dabei alle ein gemeinsames Interface. Der Client muss die konkreten Implementierungen nicht mehr kennen. Die Kopplung zwischen den Klassen wird reduziert und der Code so einfacher zu ändern.
Beispiel Bank und Kreditkarte
Ich möchte das Entwurfsmuster am Beispiel einer Bank und mehrerer Arten von Kreditkarten demonstrieren. Eine Bank gibt Kreditkarten an Kunden aus. Für die meisten Kunden möchte sie Standardkarten ausgeben. Für besondere Kunden sollen aber auch Gold- oder Platinkarten ausgegeben werden.
Vorher oder Antipattern
Eine naive Implementierung besteht aus den drei Klassen für die Kreditkarten und die Bank verfügt über drei Methoden, die jeweils eine der Karten erzeugt.
Ein Problem ist, dass die Bank und die Karten eng gekoppelt sind. Die Bank muss genau wissen, wie die Karten erzeugt werden.
Hier der Code der Bank:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Bank {
StandardCreditCard issueStandardCreditCard() {
return new StandardCreditCard();
}
GoldCreditCard issueGoldCreditCard() {
return new GoldCreditCard();
}
PlatinumCreditCard issuePlatinumCreditCard() {
return new PlatinumCreditCard();
}
}
Man sieht sofort die enge Kopplung der Bank Klasse und der Kreditkartenklassen. Die Bank muss alle zu erzeugenden Implementierungen kennen.
Und der Code der drei Kreditkarten:
1
2
3
4
5
6
7
8
public class StandardCreditCard {
BigDecimal limit = BigDecimal.valueOf(1000L);
@Override
public String toString() {
return "Standard credit card with limit " + limit.toString();
}
}
1
2
3
4
5
6
7
8
9
public class GoldCreditCard {
BigDecimal limit = BigDecimal.valueOf(2500L);
@Override
public String toString() {
return "Gold credit card with limit " + limit.toString();
}
}
1
2
3
4
5
6
7
8
public class PlatinumCreditCard {
BigDecimal limit = BigDecimal.valueOf(10000L);
@Override
public String toString() {
return "Platinum credit card with limit " + limit.toString();
}
}
Eine simple Beispielklasse, die die drei Typen erzeugt und ausgibt, sieht wie folgt aus.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Example {
public static void main(String[] args) {
Bank bank = new Bank();
StandardCreditCard standardCreditCard = bank.issueStandardCreditCard();
System.out.println(standardCreditCard);
GoldCreditCard goldCreditCard = bank.issueGoldCreditCard();
System.out.println(goldCreditCard);
PlatinumCreditCard platinumCreditCard = bank.issuePlatinumCreditCard();
System.out.println(platinumCreditCard);
}
}
Starte ich nun die main Methode der Example Klasse erhalte ich wie erwartet die Textausgabe mit der drei erzeugten Karten.
1
2
3
4
Standard credit card with limit 1000
Gold credit card with limit 2500
Platinum credit card with limit 10000
Nachher oder das Fabrik Muster im Beispiel
Die CeditCardFactory entkoppelt die Bank von den konkreten Implementierungen. Anstatt die drei Kartentypen direkt zu erzeugen, überlässt die Bank es der Fabrik, die konkreten Implementierungen zu erzeugen und erwartet lediglich eine CreditCard als Rückgabeobjekt.
1
2
3
4
5
6
7
8
9
10
public class CreditCardFactory {
public CreditCard issueCreditCard(CardType type) {
return switch (type) {
case STANDARD -> new StandardCreditCard();
case GOLD -> new GoldCreditCard();
case PLATINUM -> new PlatinumCreditCard();
};
}
}
Mit den in Java 12 eingeführten Switch Expressions lassen sich Fabriken besonders
elegant bauen. Mit break
oder versehentlichen Fall Throughs
muss man sich nämlich nicht mehr herumärgern.
Je nach übergebenem Kreditkartentyp wird eine entsprechende Karte erzeugt und zurückgegeben.
Hier nun der Code der Bank, nachdem wir die Fabrik eingebaut haben:
1
2
3
4
5
6
7
8
public class Bank {
CreditCardFactory factory = new CreditCardFactory();
CreditCard issueCreditCard(CardType type) {
return factory.issueCreditCard(type);
}
}
Die Bank erzeugt nun die Kreditkarten nicht mehr direkt, sondern delegiert die Erzeugung an die Fabrik. Gleichzeitig wurde die Bank von den Kreditkartenimplementierungen entkoppelt.
Um gegen das Interface CreditCard
programmieren zu können,
müssen wir es zunächst definieren und dafür sorgen, dass die Kreditkarten das Interface implementieren.
Da wir uns auf die Fabrik konzentrieren, reich für das Beispiel dieses minimale Interface.
1
2
public interface CreditCard {
}
Dadurch sind die Karten entsprechen leicht zu implementieren.
1
2
3
4
5
6
7
8
public class StandardCreditCard implements CreditCard {
BigDecimal limit = BigDecimal.valueOf(1000L);
@Override
public String toString() {
return "Standard credit card with limit " + limit.toString();
}
}
1
2
3
4
5
6
7
8
9
public class GoldCreditCard implements CreditCard {
BigDecimal limit = BigDecimal.valueOf(2500L);
@Override
public String toString() {
return "Gold credit card with limit " + limit.toString();
}
}
1
2
3
4
5
6
7
8
public class PlatinumCreditCard implements CreditCard {
BigDecimal limit = BigDecimal.valueOf(10000L);
@Override
public String toString() {
return "Platinum credit card with limit " + limit.toString();
}
}
Damit die Bank der Farbik mitteilen kann, was für einen Typ Karte sie erwartet, benötigen wir das Enum CardType
.
Eigentlich würde auch ein String statt einem Enum ausreichen, aber durch Enums können viele Fehler vermieden werden.
Ein Rechtschreibfehler führt dann nicht mehr zur Laufzeit zu einem Fehler oder NULL
Objekt, sondern wird schon beim Kompilieren entdeckt.
1
2
3
public enum CardType {
STANDARD, GOLD, PLATINUM
}
Mit folgender Beispielklasse können wir die neue Implementierung testen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Example {
public static void main(String[] args) {
Bank bank = new Bank();
CreditCard standardCreditCard = bank.issueCreditCard(CardType.STANDARD);
System.out.println(standardCreditCard);
CreditCard goldCreditCard = bank.issueCreditCard(CardType.GOLD);
System.out.println(goldCreditCard);
CreditCard platinumCreditCard = bank.issueCreditCard(CardType.PLATINUM);
System.out.println(platinumCreditCard);
}
}
Die Ausgabe in der Konsole hat sich nicht geändert und wir sehen, dass die Implementierung weiterhin wie erwatet funktioniert.
1
2
3
4
Standard credit card with limit 1000
Gold credit card with limit 2500
Platinum credit card with limit 10000
Vorteile
Die Vorteile des Entwurfsmusters Fabrik sind
- Die aufrufende Klasse muss nicht alle konkreten Implementierungen der Objekte, die sie benötigt, kennen.
- Die Erzeugung der Objekte wird an einer Stelle gekapselt. Dies ist besonders bei komplexen Instanziierungen praktisch.
Nachteile
Es gibt einen Nachteil bei diesem Entwurfsmuster. Alle Klassen müssen beim Kompilieren bekannt sein. Zur Laufzeit können keine neuen, bisher unbekannten Implementierungen des Interfaces hinzugefügt werden.
Zusammenfassung
Durch eine Fabrik kann man die Erzeugung von bestimmten Objekten an einem Ort kapseln. Gerade die Switch Expressions aus Java 12 machen die Implementierung einer Fabrik relativ einfach.
Im nächsten Artikel stelle ich euch dann die Abstract Factory
bzw. die Abstrakte Fabrik
vor.
Bis bald,
seism0saurus