Design Patterns Katalog: Singleton, Observer, Factory, Adapter & Co.
Dieser Beitrag ist ein umfassender Katalog der wichtigsten Design Patterns – inklusive Singleton, Observer, Factory, Adapter, Iterator, Strategy, Decorator und Template Method mit praktischen Implementierungen.
In a Nutshell
Design Patterns sind bewährte Lösungsvorlagen für wiederkehrende Probleme in der Softwareentwicklung. Sie ermöglichen wiederverwendbare, wartbare und erweiterbare Architekturen.
Kompakte Fachbeschreibung
Design Patterns (Entwurfsmuster) sind beschriebene Lösungen für typische Probleme in der objektorientierten Softwareentwicklung. Sie wurden von der “Gang of Four” (GoF) kategorisiert und dokumentieren bewährte Praktiken.
Kategorien der GoF-Pattern:
Creational Patterns (Erzeugungsmuster)
- Singleton: Stellt sicher, dass nur eine Instanz existiert
- Factory: Objekterstellung ohne direkte Instanziierung
- Abstract Factory: Familien von verwandten Objekten
- Builder: Komplexe Objekte schrittweise erstellen
Structural Patterns (Strukturmuster)
- Adapter: Inkompatible Schnittstellen anpassen
- Decorator: Dynamisch Funktionalität hinzufügen
- Facade: Vereinfachte Schnittstelle für Subsysteme
- Proxy: Stellvertreter für Objekte
Behavioral Patterns (Verhaltensmuster)
- Observer: Benachrichtigung bei Zustandsänderungen
- Strategy: Algorithmen austauschbar machen
- Iterator: Sequenziellen Zugriff auf Sammlungen
- Template Method: Skelett von Algorithmen definieren
Prüfungsrelevante Stichpunkte
- Singleton: Globale Instanz, thread-safe Implementierung
- Observer: Subject-Observer Beziehung, lose Kopplung
- Factory: Objekterstellung zentralisieren, Polymorphie
- Adapter: Schnittstellenanpassung, Komposition vs Vererbung
- Iterator: Einheitliche Zugriff auf Container, hasNext/next
- Strategy: Algorithmen austauschbar, Open-Closed Principle
- Decorator: Funktionalität erweitern, dynamisch
- Template Method: Algorithmus-Skelett, Hook-Methoden
- IHK-relevant: Wichtig für Softwarearchitektur und -design
Kernkomponenten
- Singleton: Eine Instanz pro Klasse
- Observer: 1:n Beziehung für Benachrichtigungen
- Factory: Objekterstellung ohne new-Operator
- Adapter: Schnittstellenanpassung
- Iterator: Sequenzieller Zugriff auf Sammlungen
- Strategy: Austauschbare Algorithmen
- Decorator: Dynamische Funktionalitätserweiterung
- Template Method: Algorithmus-Skelett mit variablen Schritten
Praxisbeispiele
1. Singleton Pattern
// Thread-safe Singleton mit Lazy Initialization
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private final String connectionUrl;
// Privater Konstruktor verhindert externe Instanziierung
private DatabaseConnection() {
this.connectionUrl = "jdbc:mysql://localhost:3306/mydb";
// Simulierte Verbindungsaufbau
System.out.println("Datenbankverbindung hergestellt");
}
// Double-Checked Locking für Thread-Safety
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public void executeQuery(String sql) {
System.out.println("Führe aus: " + sql);
}
public String getConnectionUrl() {
return connectionUrl;
}
}
// Verwendung
public class SingletonDemo {
public static void main(String[] args) {
DatabaseConnection conn1 = DatabaseConnection.getInstance();
DatabaseConnection conn2 = DatabaseConnection.getInstance();
System.out.println("Gleiches Objekt: " + (conn1 == conn2)); // true
System.out.println("Connection URL: " + conn1.getConnectionUrl());
conn1.executeQuery("SELECT * FROM users");
}
}
2. Observer Pattern
import java.util.*;
// Subject (Observable)
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
// Concrete Subject
class WeatherStation implements Subject {
private List<Observer> observers = new ArrayList<>();
private float temperature;
private float humidity;
public void setMeasurements(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
measurementsChanged();
}
public void measurementsChanged() {
notifyObservers();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity);
}
}
}
// Observer
interface Observer {
void update(float temperature, float humidity);
}
// Concrete Observer
class TemperatureDisplay implements Observer {
private float temperature;
@Override
public void update(float temperature, float humidity) {
this.temperature = temperature;
display();
}
public void display() {
System.out.println("Temperaturanzeige: " + temperature + "°C");
}
}
class HumidityDisplay implements Observer {
private float humidity;
@Override
public void update(float temperature, float humidity) {
this.humidity = humidity;
display();
}
public void display() {
System.out.println("Feuchtigkeitsanzeige: " + humidity + "%");
}
}
// Verwendung
public class ObserverDemo {
public static void main(String[] args) {
WeatherStation weatherStation = new WeatherStation();
TemperatureDisplay tempDisplay = new TemperatureDisplay();
HumidityDisplay humidityDisplay = new HumidityDisplay();
// Observer registrieren
weatherStation.registerObserver(tempDisplay);
weatherStation.registerObserver(humidityDisplay);
// Messungen ändern
weatherStation.setMeasurements(25.5f, 65.0f);
weatherStation.setMeasurements(26.0f, 63.5f);
// Observer entfernen
weatherStation.removeObserver(humidityDisplay);
weatherStation.setMeasurements(24.8f, 67.2f);
}
}
3. Factory Pattern
// Product Interface
interface Shape {
void draw();
}
// Concrete Products
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Zeichne Kreis");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Zeichne Rechteck");
}
}
class Triangle implements Shape {
@Override
public void draw() {
System.out.println("Zeichne Dreieck");
}
}
// Factory
class ShapeFactory {
public Shape createShape(String shapeType) {
if (shapeType == null || shapeType.isEmpty()) {
return null;
}
switch (shapeType.toUpperCase()) {
case "CIRCLE":
return new Circle();
case "RECTANGLE":
return new Rectangle();
case "TRIANGLE":
return new Triangle();
default:
throw new IllegalArgumentException("Unbekannter Shape-Typ: " + shapeType);
}
}
}
// Verwendung
public class FactoryDemo {
public static void main(String[] args) {
ShapeFactory factory = new ShapeFactory();
// Shapes erstellen ohne new-Operator
Shape circle = factory.createShape("CIRCLE");
Shape rectangle = factory.createShape("RECTANGLE");
Shape triangle = factory.createShape("TRIANGLE");
// Shapes verwenden
circle.draw();
rectangle.draw();
triangle.draw();
// Factory mit Konfiguration
createShapesFromConfig(factory);
}
private static void createShapesFromConfig(ShapeFactory factory) {
String[] shapes = {"CIRCLE", "RECTANGLE", "CIRCLE", "TRIANGLE"};
for (String shapeType : shapes) {
try {
Shape shape = factory.createShape(shapeType);
shape.draw();
} catch (IllegalArgumentException e) {
System.err.println("Fehler: " + e.getMessage());
}
}
}
}
4. Adapter Pattern
// Target Interface
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee (inkompatible Schnittstelle)
interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
// Concrete Adaptee
class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Spielt VLC-Datei: " + fileName);
}
@Override
public void playMp4(String fileName) {
// Nicht unterstützt
}
}
class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// Nicht unterstützt
}
@Override
public void playMp4(String fileName) {
System.out.println("Spielt MP4-Datei: " + fileName);
}
}
// Adapter
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}
// Client
class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
// Eigene Formate unterstützen
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Spielt MP3-Datei: " + fileName);
}
// Andere Formate über Adapter
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Ungültiges Medienformat: " + audioType);
}
}
}
// Verwendung
public class AdapterDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "song.mp3");
audioPlayer.play("mp4", "video.mp4");
audioPlayer.play("vlc", "movie.vlc");
audioPlayer.play("avi", "video.avi");
}
}
5. Iterator Pattern
import java.util.*;
// Aggregate Interface
interface Container {
Iterator getIterator();
}
// Iterator Interface
interface Iterator {
boolean hasNext();
Object next();
}
// Concrete Aggregate
class NameRepository implements Container {
private String[] names = {"Alice", "Bob", "Charlie", "Diana"};
@Override
public Iterator getIterator() {
return new NameIterator();
}
private class NameIterator implements Iterator {
private int index;
@Override
public boolean hasNext() {
return index < names.length;
}
@Override
public Object next() {
if (this.hasNext()) {
return names[index++];
}
return null;
}
}
}
// Verwendung
public class IteratorDemo {
public static void main(String[] args) {
NameRepository namesRepository = new NameRepository();
for (Iterator iterator = namesRepository.getIterator(); iterator.hasNext();) {
String name = (String) iterator.next();
System.out.println("Name: " + name);
}
// Vergleich mit Java Standard Iterator
List<String> nameList = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
System.out.println("\nMit Java Iterator:");
for (Iterator<String> it = nameList.iterator(); it.hasNext();) {
String name = it.next();
System.out.println("Name: " + name);
}
// Enhanced for loop (syntactic sugar)
System.out.println("\nMit Enhanced for loop:");
for (String name : nameList) {
System.out.println("Name: " + name);
}
}
}
6. Strategy Pattern
// Strategy Interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String date;
public CreditCardPayment(String name, String cardNumber, String cvv, String date) {
this.name = name;
this.cardNumber = cardNumber;
this.cvv = cvv;
this.date = date;
}
@Override
public void pay(int amount) {
System.out.println(amount + " € mit Kreditkarte bezahlt.");
System.out.println("Karteninhaber: " + name);
System.out.println("Kartennummer: **** **** **** " + cardNumber.substring(cardNumber.length() - 4));
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
private String password;
public PayPalPayment(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + " € mit PayPal bezahlt.");
System.out.println("PayPal-Account: " + email);
}
}
class BitcoinPayment implements PaymentStrategy {
private String walletAddress;
public BitcoinPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(int amount) {
System.out.println(amount + " € mit Bitcoin bezahlt.");
System.out.println("Wallet: " + walletAddress.substring(0, 8) + "...");
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("Keine Zahlungsmethode ausgewählt");
}
paymentStrategy.pay(amount);
}
}
// Verwendung
public class StrategyDemo {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Verschiedene Zahlungsmethoden
cart.setPaymentStrategy(new CreditCardPayment(
"Max Mustermann", "1234567890123456", "123", "12/25"));
cart.checkout(100);
System.out.println("---");
cart.setPaymentStrategy(new PayPalPayment("max@example.com", "password123"));
cart.checkout(250);
System.out.println("---");
cart.setPaymentStrategy(new BitcoinPayment("1A2B3C4D5E6F7G8H9I0J"));
cart.checkout(500);
// Dynamische Strategieauswahl
dynamicStrategySelection(cart);
}
private static void dynamicStrategySelection(ShoppingCart cart) {
Scanner scanner = new Scanner(System.in);
System.out.println("\nWählen Sie Zahlungsmethode:");
System.out.println("1. Kreditkarte");
System.out.println("2. PayPal");
System.out.println("3. Bitcoin");
int choice = scanner.nextInt();
int amount = 75;
switch (choice) {
case 1:
cart.setPaymentStrategy(new CreditCardPayment(
"Test User", "1111222233334444", "999", "12/24"));
break;
case 2:
cart.setPaymentStrategy(new PayPalPayment("test@example.com", "test123"));
break;
case 3:
cart.setPaymentStrategy(new BitcoinPayment("ABCDEF123456789"));
break;
default:
System.out.println("Ungültige Auswahl");
return;
}
cart.checkout(amount);
scanner.close();
}
}
7. Decorator Pattern
// Component Interface
interface Coffee {
String getDescription();
double getCost();
}
// Concrete Component
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Einfacher Kaffee";
}
@Override
public double getCost() {
return 2.0;
}
}
// Abstract Decorator
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}
// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Milch";
}
@Override
public double getCost() {
return super.getCost() + 0.5;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Zucker";
}
@Override
public double getCost() {
return super.getCost() + 0.2;
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Schlagobers";
}
@Override
public double getCost() {
return super.getCost() + 1.0;
}
}
// Verwendung
public class DecoratorDemo {
public static void main(String[] args) {
// Einfacher Kaffee
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " kostet " + coffee.getCost() + "€");
// Mit Milch
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " kostet " + coffee.getCost() + "€");
// Mit Zucker
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " kostet " + coffee.getCost() + "€");
// Mit Schlagobers
coffee = new WhippedCreamDecorator(coffee);
System.out.println(coffee.getDescription() + " kostet " + coffee.getCost() + "€");
// Flexible Kombination
Coffee customCoffee = new SimpleCoffee();
customCoffee = new MilkDecorator(customCoffee);
customCoffee = new SugarDecorator(customCoffee);
customCoffee = new SugarDecorator(customCoffee); // Doppelter Zucker
System.out.println("\nCustom Kaffee:");
System.out.println(customCoffee.getDescription() + " kostet " + customCoffee.getCost() + "€");
}
}
8. Template Method Pattern
// Abstract Class mit Template Method
abstract class DataProcessor {
// Template Method (final, damit Unterklassen nicht überschreiben können)
public final void processData() {
loadData();
if (validateData()) {
transformData();
saveData();
} else {
handleError();
}
cleanup();
}
// Abstrakte Methoden (müssen von Unterklassen implementiert werden)
protected abstract void loadData();
protected abstract void transformData();
protected abstract void saveData();
// Hook Methods (können überschrieben werden)
protected boolean validateData() {
return true; // Standard-Implementierung
}
protected void handleError() {
System.out.println("Datenvalidierung fehlgeschlagen!");
}
protected void cleanup() {
System.out.println("Aufräumen abgeschlossen");
}
}
// Concrete Implementierung
class CSVDataProcessor extends DataProcessor {
private String data;
@Override
protected void loadData() {
System.out.println("Lade CSV-Daten...");
data = "name,age,city\nAlice,25,Berlin\nBob,30,Hamburg";
}
@Override
protected void transformData() {
System.out.println("Transformiere CSV-Daten...");
// CSV zu JSON konvertieren
data = data.replace("\n", ",").replace("name", "\"name\"")
.replace("age", "\"age\"").replace("city", "\"city\"");
}
@Override
protected void saveData() {
System.out.println("Speichere transformierte Daten...");
System.out.println("Daten: " + data);
}
@Override
protected boolean validateData() {
// CSV-spezifische Validierung
return data != null && data.contains("name") && data.contains("age");
}
}
class XMLDataProcessor extends DataProcessor {
private String data;
@Override
protected void loadData() {
System.out.println("Lade XML-Daten...");
data = "<users><user name='Alice' age='25'/><user name='Bob' age='30'/></users>";
}
@Override
protected void transformData() {
System.out.println("Transformiere XML-Daten...");
// XML zu JSON konvertieren
data = "[{\"name\":\"Alice\",\"age\":25},{\"name\":\"Bob\",\"age\":30}]";
}
@Override
protected void saveData() {
System.out.println("Speichere transformierte Daten...");
System.out.println("Daten: " + data);
}
@Override
protected void handleError() {
System.out.println("XML-Verarbeitungsfehler!");
// Spezielle Fehlerbehandlung für XML
}
}
// Verwendung
public class TemplateMethodDemo {
public static void main(String[] args) {
System.out.println("=== CSV Prozessor ===");
DataProcessor csvProcessor = new CSVDataProcessor();
csvProcessor.processData();
System.out.println("\n=== XML Prozessor ===");
DataProcessor xmlProcessor = new XMLDataProcessor();
xmlProcessor.processData();
// Demonstration des Template Method Patterns
demonstrateTemplateMethod();
}
private static void demonstrateTemplateMethod() {
System.out.println("\n=== Template Method Demonstration ===");
// Der Algorithmus-Ablauf ist immer gleich, nur die Implementierung variiert
DataProcessor[] processors = {
new CSVDataProcessor(),
new XMLDataProcessor()
};
for (DataProcessor processor : processors) {
System.out.println("\nVerarbeite mit " + processor.getClass().getSimpleName() + ":");
processor.processData();
}
}
}
Pattern-Kategorien Übersicht
Creational Patterns (Erzeugungsmuster)
| Pattern | Zweck | Beispiel |
|---|---|---|
| Singleton | Nur eine Instanz | Database Connection |
| Factory | Objekterstellung zentralisieren | Shape Factory |
| Abstract Factory | Familien von Objekten | GUI Factory |
| Builder | Komplexe Objekte schrittweise | StringBuilder |
Structural Patterns (Strukturmuster)
| Pattern | Zweck | Beispiel |
|---|---|---|
| Adapter | Inkompatible Schnittstellen | Media Adapter |
| Decorator | Dynamisch Funktionalität | Coffee Decorator |
| Facade | Vereinfachte Schnittstelle | Home Theater |
| Proxy | Stellvertreter | Virtual Proxy |
Behavioral Patterns (Verhaltensmuster)
| Pattern | Zweck | Beispiel |
|---|---|---|
| Observer | Benachrichtigung bei Änderungen | Weather Station |
| Strategy | Algorithmen austauschbar | Payment Strategy |
| Iterator | Sequenzieller Zugriff | Collection Iterator |
| Template Method | Algorithmus-Skelett | Data Processor |
Vorteile von Design Patterns
Wiederverwendbarkeit
- Bewährte Lösungen können wiederverwendet werden
- Reduzieren Entwicklungsaufwand
- Konsistente Architektur
Wartbarkeit
- Klare Verantwortlichkeiten
- Lose Kopplung zwischen Komponenten
- Einfache Modifikation und Erweiterung
Kommunikation
- Gemeinsame Sprache für Entwickler
- Dokumentierte Lösungsansätze
- Schnelles Verständnis von Code
Qualität
- Getestete und bewährte Lösungen
- Reduzierung von Fehlern
- Bessere Code-Qualität
Häufige Prüfungsfragen
-
Wann verwendet man das Singleton Pattern? Wenn nur eine Instanz einer Klasse global benötigt wird (Database Connection, Logger).
-
Was ist der Unterschied zwischen Adapter und Decorator? Adapter passt inkompatible Schnittstellen an, Decorator fügt Funktionalität hinzu.
-
Erklären Sie das Observer Pattern! Subject benachrichtigt Observer bei Zustandsänderungen, lose Kopplung zwischen Komponenten.
-
Warum ist das Template Method Pattern nützlich? Definiert Algorithmus-Skelett, erlaubt Unterklassen spezifische Schritte zu implementieren.
Wichtigste Quellen
- https://refactoring.guru/design-patterns
- https://www.oreilly.com/library/view/head-first-design/0596007124
- https://en.wikipedia.org/wiki/Software_design_pattern