Design Pattern
Design Patterns (deutsch: Entwurfsmuster) sind bewährte Lösungsschablonen für wiederkehrende Probleme in der Softwareentwicklung. Sie beschreiben keine fertigen Code-Bausteine, sondern abstrakte Konzepte, die du auf deine konkreten Anforderungen anpassen kannst. Design Patterns wurden 1994 durch das Buch Design Patterns: Elements of Reusable Object-Oriented Software der sogenannten Gang of Four (GoF) – Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides – populär gemacht.
Die drei Kategorien von Design Patterns
Die Gang of Four definierte 23 grundlegende Entwurfsmuster, die in drei Kategorien eingeteilt werden. Diese Einteilung hilft dir, das richtige Pattern für dein Problem zu finden.
| Kategorie | Beschreibung | Beispiele |
|---|---|---|
| Erzeugungsmuster (Creational) | Kontrollieren die Objekterstellung | Singleton, Factory, Builder, Prototype |
| Strukturmuster (Structural) | Organisieren Klassen und Objekte zu größeren Strukturen | Adapter, Decorator, Facade, Composite |
| Verhaltensmuster (Behavioral) | Modellieren Interaktion und Kommunikation zwischen Objekten | Observer, Strategy, State, Command |
Erzeugungsmuster (Creational Patterns)
Erzeugungsmuster befassen sich damit, wie Objekte erstellt werden. Sie kapseln den Erstellungsprozess und machen ihn flexibler.
Singleton
Das Singleton-Pattern stellt sicher, dass von einer Klasse nur genau eine Instanz existiert und bietet einen globalen Zugriffspunkt darauf. Typische Anwendungsfälle sind Logger, Konfigurationsobjekte oder Datenbankverbindungen.
public class Logger {
// Einzige Instanz der Klasse
private static Logger instance;
// Privater Konstruktor verhindert externe Instanziierung
private Logger() {}
// Globaler Zugriffspunkt
public static Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
// Verwendung
Logger logger = Logger.getInstance();
logger.log("Anwendung gestartet");
Hinweis: Das Singleton-Pattern ist in der modernen Entwicklung umstritten, da es globalen Zustand einführt und Unit-Tests erschwert. Als Alternative wird oft Dependency Injection empfohlen.
Factory Method
Das Factory-Pattern definiert eine Schnittstelle zur Objekterzeugung, überlässt aber den Unterklassen die Entscheidung, welche konkrete Klasse instanziiert wird. Das macht deinen Code flexibler und erweiterbar.
// Abstrakte Produktklasse
abstract class Dokument {
abstract void oeffnen();
}
// Konkrete Produkte
class PDFDokument extends Dokument {
void oeffnen() {
System.out.println("PDF wird geöffnet");
}
}
class WordDokument extends Dokument {
void oeffnen() {
System.out.println("Word-Dokument wird geöffnet");
}
}
// Factory
class DokumentFactory {
public static Dokument erstelle(String typ) {
return switch (typ) {
case "pdf" -> new PDFDokument();
case "word" -> new WordDokument();
default -> throw new IllegalArgumentException("Unbekannter Typ");
};
}
}
// Verwendung
Dokument doc = DokumentFactory.erstelle("pdf");
doc.oeffnen();
Builder
Das Builder-Pattern trennt die Konstruktion eines komplexen Objekts von seiner Repräsentation. Es ist besonders nützlich, wenn ein Objekt viele optionale Parameter hat.
public class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers;
this.body = builder.body;
}
public static class Builder {
private final String url; // Pflichtfeld
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private String body = null;
public Builder(String url) {
this.url = url;
}
public Builder method(String method) {
this.method = method;
return this;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
}
// Verwendung - lesbar und flexibel
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.body("{\"name\": \"Max\"}")
.build();
Strukturmuster (Structural Patterns)
Strukturmuster beschreiben, wie Klassen und Objekte zu größeren Strukturen zusammengesetzt werden können. Sie helfen, Beziehungen zwischen Komponenten zu vereinfachen.
Adapter
Das Adapter-Pattern wandelt die Schnittstelle einer Klasse in eine andere Schnittstelle um, die der Client erwartet. Es ermöglicht die Zusammenarbeit von Klassen, die sonst inkompatibel wären.
// Alte Schnittstelle, die nicht geändert werden kann
class AlterDrucker {
void druckeText(String text) {
System.out.println("Alter Drucker: " + text);
}
}
// Neue Schnittstelle, die das System erwartet
interface ModernerDrucker {
void print(String content, String format);
}
// Adapter macht alte Klasse kompatibel
class DruckerAdapter implements ModernerDrucker {
private AlterDrucker alterDrucker;
public DruckerAdapter(AlterDrucker alterDrucker) {
this.alterDrucker = alterDrucker;
}
@Override
public void print(String content, String format) {
// Übersetzt den Aufruf für die alte Schnittstelle
alterDrucker.druckeText(content);
}
}
// Verwendung
ModernerDrucker drucker = new DruckerAdapter(new AlterDrucker());
drucker.print("Hallo Welt", "plain");
Decorator
Das Decorator-Pattern erweitert das Verhalten eines Objekts dynamisch zur Laufzeit, ohne dessen Klasse zu ändern. Es ist eine flexible Alternative zur Vererbung.
// Basis-Interface
interface Kaffee {
String getBeschreibung();
double getPreis();
}
// Konkrete Komponente
class Espresso implements Kaffee {
public String getBeschreibung() { return "Espresso"; }
public double getPreis() { return 1.50; }
}
// Abstrakter Decorator
abstract class KaffeeDecorator implements Kaffee {
protected Kaffee kaffee;
public KaffeeDecorator(Kaffee kaffee) {
this.kaffee = kaffee;
}
}
// Konkrete Decorators
class MitMilch extends KaffeeDecorator {
public MitMilch(Kaffee kaffee) { super(kaffee); }
public String getBeschreibung() {
return kaffee.getBeschreibung() + " + Milch";
}
public double getPreis() {
return kaffee.getPreis() + 0.30;
}
}
class MitZucker extends KaffeeDecorator {
public MitZucker(Kaffee kaffee) { super(kaffee); }
public String getBeschreibung() {
return kaffee.getBeschreibung() + " + Zucker";
}
public double getPreis() {
return kaffee.getPreis() + 0.10;
}
}
// Verwendung - Decorators können kombiniert werden
Kaffee meinKaffee = new MitZucker(new MitMilch(new Espresso()));
System.out.println(meinKaffee.getBeschreibung()); // Espresso + Milch + Zucker
System.out.println(meinKaffee.getPreis()); // 1.90
Facade
Das Facade-Pattern bietet eine vereinfachte Schnittstelle zu einem komplexen Subsystem. Es verbirgt die Komplexität und macht das System einfacher nutzbar.
// Komplexes Subsystem
class CPU {
void start() { System.out.println("CPU startet"); }
}
class Arbeitsspeicher {
void laden() { System.out.println("RAM wird initialisiert"); }
}
class Festplatte {
void lesen() { System.out.println("Bootsektor wird gelesen"); }
}
// Facade vereinfacht die Nutzung
class ComputerFacade {
private CPU cpu;
private Arbeitsspeicher ram;
private Festplatte festplatte;
public ComputerFacade() {
this.cpu = new CPU();
this.ram = new Arbeitsspeicher();
this.festplatte = new Festplatte();
}
// Eine einfache Methode statt drei komplexer Aufrufe
public void starten() {
cpu.start();
ram.laden();
festplatte.lesen();
System.out.println("Computer ist bereit!");
}
}
// Verwendung - der Client kennt nur die Facade
ComputerFacade computer = new ComputerFacade();
computer.starten();
Verhaltensmuster (Behavioral Patterns)
Verhaltensmuster definieren, wie Objekte miteinander kommunizieren und wie Verantwortlichkeiten zwischen ihnen verteilt werden.
Observer
Das Observer-Pattern (Beobachter) definiert eine 1-zu-n-Abhängigkeit: Wenn ein Objekt seinen Zustand ändert, werden alle abhängigen Objekte automatisch benachrichtigt. Dieses Pattern ist fundamental für Event-Handling in GUIs.
import java.util.ArrayList;
import java.util.List;
// Observer-Interface
interface Observer {
void update(String nachricht);
}
// Subject (das beobachtete Objekt)
class Newsletter {
private List<Observer> abonnenten = new ArrayList<>();
public void abonnieren(Observer observer) {
abonnenten.add(observer);
}
public void abmelden(Observer observer) {
abonnenten.remove(observer);
}
public void neueAusgabe(String inhalt) {
// Alle Abonnenten benachrichtigen
for (Observer observer : abonnenten) {
observer.update(inhalt);
}
}
}
// Konkrete Observer
class EmailAbonnent implements Observer {
private String email;
public EmailAbonnent(String email) {
this.email = email;
}
@Override
public void update(String nachricht) {
System.out.println("Email an " + email + ": " + nachricht);
}
}
// Verwendung
Newsletter newsletter = new Newsletter();
newsletter.abonnieren(new EmailAbonnent("max@example.com"));
newsletter.abonnieren(new EmailAbonnent("anna@example.com"));
newsletter.neueAusgabe("Neue Ausgabe erschienen!");
// Beide Abonnenten werden automatisch benachrichtigt
Strategy
Das Strategy-Pattern definiert eine Familie von austauschbaren Algorithmen. Der Algorithmus kann zur Laufzeit gewechselt werden, ohne den Client-Code zu ändern.
// Strategy-Interface
interface Sortierung {
void sortiere(int[] zahlen);
}
// Konkrete Strategien
class BubbleSort implements Sortierung {
public void sortiere(int[] zahlen) {
System.out.println("Sortiere mit Bubble Sort");
// Implementierung...
}
}
class QuickSort implements Sortierung {
public void sortiere(int[] zahlen) {
System.out.println("Sortiere mit Quick Sort");
// Implementierung...
}
}
// Kontext verwendet die Strategy
class Sortierer {
private Sortierung strategie;
public void setStrategie(Sortierung strategie) {
this.strategie = strategie;
}
public void sortiere(int[] zahlen) {
strategie.sortiere(zahlen);
}
}
// Verwendung - Strategie kann zur Laufzeit gewechselt werden
Sortierer sortierer = new Sortierer();
int[] zahlen = {5, 2, 8, 1, 9};
sortierer.setStrategie(new BubbleSort());
sortierer.sortiere(zahlen);
sortierer.setStrategie(new QuickSort());
sortierer.sortiere(zahlen);
Wann solltest du Design Patterns einsetzen?
Design Patterns sind mächtige Werkzeuge, sollten aber mit Bedacht eingesetzt werden:
- Ja, wenn: Du ein wiederkehrendes Problem erkennst, das ein Pattern löst
- Ja, wenn: Du flexible, erweiterbare Software benötigst
- Ja, wenn: Du mit anderen Entwicklern in einer gemeinsamen Sprache kommunizieren willst
- Nein, wenn: Das Problem einfach ist und kein Pattern erfordert
- Nein, wenn: Du ein Pattern nur anwendest, weil es "cool" ist (Over-Engineering)
- Nein, wenn: Das Team die Patterns nicht kennt und der Code dadurch unverständlich wird
Design Patterns in der IT-Ausbildung
Design Patterns sind ein wichtiges Thema in der Ausbildung zum Fachinformatiker für Anwendungsentwicklung. Du solltest die grundlegenden Patterns kennen und verstehen, wann sie eingesetzt werden. In Prüfungen wird oft nach dem Singleton, Factory oder Observer gefragt. Auch im Berufsalltag wirst du auf Patterns treffen – sei es in Frameworks wie Spring (Dependency Injection, Factory) oder in GUI-Bibliotheken (Observer für Events).
Das Verständnis von Design Patterns hilft dir auch, bestehenden Code besser zu lesen und zu verstehen. Viele Frameworks und Bibliotheken nutzen diese Muster, und wenn du sie erkennst, erschließt sich dir die Architektur viel schneller.
Quellen und weiterführende Links
- Refactoring Guru: Design Patterns – Ausführliche Erklärungen mit Codebeispielen in verschiedenen Sprachen
- IONOS: Was sind Design Patterns? – Deutsche Einführung in Entwurfsmuster
- Wikipedia: Entwurfsmuster – Übersicht aller 23 GoF-Patterns
- Design Patterns (Gang of Four) – Das Originalbuch von 1994