Skip to content
IRC-CodingIRC-Coding
Design PatternsSingletonObserverFactoryAdapterIteratorStrategyDecoratorAlgorithmsFundamentals

Design Patterns Catalog: Singleton, Observer, Factory

Essential design patterns with implementations. Singleton, Observer, Factory, Adapter, Iterator, Strategy, Decorator, Template Method examples.

S

schutzgeist

11 min read
Design Patterns Catalog: Singleton, Observer, Factory

Design Patterns Catalog: Singleton, Observer, Factory, Adapter & More

Related Design Patterns Articles:

This article is a comprehensive catalog of the most important Design Patterns – including Singleton, Observer, Factory, Adapter, Iterator, Strategy, Decorator, and Template Method with practical implementations.

In a Nutshell

Design Patterns are proven solution templates for recurring problems in software development. They enable you to build reusable, maintainable, and extensible architectures.

Technical Overview

Design Patterns (or Entwurfsmuster) are documented solutions for typical problems in object-oriented software development. They were categorized by the “Gang of Four” (GoF) and represent best practices proven in the field.

GoF Pattern Categories:

Creational Patterns

  • Singleton: Ensures only one instance of a class exists
  • Factory: Creates objects without direct instantiation
  • Abstract Factory: Handles families of related objects
  • Builder: Constructs complex objects step by step

Structural Patterns

  • Adapter: Makes incompatible interfaces work together
  • Decorator: Adds functionality dynamically
  • Facade: Provides a simplified interface for subsystems
  • Proxy: Acts as a stand-in for an object

Behavioral Patterns

  • Observer: Notifies objects when state changes
  • Strategy: Makes algorithms interchangeable
  • Iterator: Provides sequential access to collections
  • Template Method: Defines an algorithm’s skeleton

Key Exam Topics

  • Singleton: Global instance, thread-safe implementation
  • Observer: Subject-observer relationship, loose coupling
  • Factory: Centralized object creation, polymorphism
  • Adapter: Interface adaptation, composition vs. inheritance
  • Iterator: Uniform access to containers, hasNext/next
  • Strategy: Interchangeable algorithms, Open-Closed Principle
  • Decorator: Function enrichment, dynamic behavior
  • Template Method: Algorithm skeleton, hook methods
  • IHK-relevant: Essential for software architecture and design

Core Components

  1. Singleton: One instance per class
  2. Observer: 1-to-many relationship for notifications
  3. Factory: Object creation without the new operator
  4. Adapter: Interface adaptation
  5. Iterator: Sequential access to collections
  6. Strategy: Swappable algorithms
  7. Decorator: Dynamic functionality enhancement
  8. Template Method: Algorithm skeleton with variable steps

Practical Examples

1. Singleton Pattern

// Thread-safe Singleton with 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 (incompatible interface)
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("Playing VLC file: " + fileName);
    }
    
    @Override
    public void playMp4(String fileName) {
        // Not supported
    }
}

class Mp4Player implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        // Not supported
    }
    
    @Override
    public void playMp4(String fileName) {
        System.out.println("Playing MP4 file: " + 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) {
        // Support native formats
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing MP3 file: " + fileName);
        }
        // Route other formats through adapter
        else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media format: " + audioType);
        }
    }
}

// Usage
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;
        }
    }
}

// Usage
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);
        }
        
        // Comparison with Java standard Iterator
        List<String> nameList = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
        System.out.println("\nUsing 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("\nUsing 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 + " € paid with credit card.");
        System.out.println("Card holder: " + name);
        System.out.println("Card number: **** **** **** " + 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 + " € paid with PayPal.");
        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 + " € paid with Bitcoin.");
        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("No payment method selected");
        }
        paymentStrategy.pay(amount);
    }
}

// Usage
public class StrategyDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        
        // Different payment methods
        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);
        
        // Dynamic strategy selection
        dynamicStrategySelection(cart);
    }
    
    private static void dynamicStrategySelection(ShoppingCart cart) {
        Scanner scanner = new Scanner(System.in);
        
        System.out.println("\nSelect payment method:");
        System.out.println("1. Credit card");
        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("Invalid choice");
                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 "Simple coffee";
    }
    
    @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() + ", milk";
    }
    
    @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() + ", sugar";
    }
    
    @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() + ", whipped cream";
    }
    
    @Override
    public double getCost() {
        return super.getCost() + 1.0;
    }
}

// Usage
public class DecoratorDemo {
    public static void main(String[] args) {
        // Simple coffee
        Coffee coffee = new SimpleCoffee();
        System.out.println(coffee.getDescription() + " costs " + coffee.getCost() + "€");
        
        // With milk
        coffee = new MilkDecorator(coffee);
        System.out.println(coffee.getDescription() + " costs " + coffee.getCost() + "€");
        
        // With sugar
        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.getDescription() + " costs " + coffee.getCost() + "€");
        
        // With whipped cream
        coffee = new WhippedCreamDecorator(coffee);
        System.out.println(coffee.getDescription() + " costs " + coffee.getCost() + "€");
        
        // Flexible combination
        Coffee customCoffee = new SimpleCoffee();
        customCoffee = new MilkDecorator(customCoffee);
        customCoffee = new SugarDecorator(customCoffee);
        customCoffee = new SugarDecorator(customCoffee); // Double sugar
        
        System.out.println("\nCustom coffee:");
        System.out.println(customCoffee.getDescription() + " costs " + customCoffee.getCost() + "€");
    }
}

8. Template Method Pattern

// Abstract Class with Template Method
abstract class DataProcessor {
    
    // Template Method (final, so subclasses cannot override)
    public final void processData() {
        loadData();
        if (validateData()) {
            transformData();
            saveData();
        } else {
            handleError();
        }
        cleanup();
    }
    
    // Abstract methods (must be implemented by subclasses)
    protected abstract void loadData();
    protected abstract void transformData();
    protected abstract void saveData();
    
    // Hook Methods (can be overridden)
    protected boolean validateData() {
        return true; // Default implementation
    }
    
    protected void handleError() {
        System.out.println("Data validation failed!");
    }
    
    protected void cleanup() {
        System.out.println("Cleanup completed");
    }
}

// Concrete Implementation
class CSVDataProcessor extends DataProcessor {
    private String data;
    
    @Override
    protected void loadData() {
        System.out.println("Loading CSV data...");
        data = "name,age,city\nAlice,25,Berlin\nBob,30,Hamburg";
    }
    
    @Override
    protected void transformData() {
        System.out.println("Transforming CSV data...");
        // Convert CSV to JSON
        data = data.replace("\n", ",").replace("name", "\"name\"")
                    .replace("age", "\"age\"").replace("city", "\"city\"");
    }
    
    @Override
    protected void saveData() {
        System.out.println("Saving transformed data...");
        System.out.println("Data: " + data);
    }
    
    @Override
    protected boolean validateData() {
        // CSV-specific validation
        return data != null && data.contains("name") && data.contains("age");
    }
}

class XMLDataProcessor extends DataProcessor {
    private String data;
    
    @Override
    protected void loadData() {
        System.out.println("Loading XML data...");
        data = "<users><user name='Alice' age='25'/><user name='Bob' age='30'/></users>";
    }
    
    @Override
    protected void transformData() {
        System.out.println("Transforming XML data...");
        // Convert XML to JSON
        data = "[{\"name\":\"Alice\",\"age\":25},{\"name\":\"Bob\",\"age\":30}]";
    }
    
    @Override
    protected void saveData() {
        System.out.println("Saving transformed data...");
        System.out.println("Data: " + data);
    }
    
    @Override
    protected void handleError() {
        System.out.println("XML processing error!");
        // Special error handling for XML
    }
}

// Usage
public class TemplateMethodDemo {
    public static void main(String[] args) {
        System.out.println("=== CSV Processor ===");
        DataProcessor csvProcessor = new CSVDataProcessor();
        csvProcessor.processData();
        
        System.out.println("\n=== XML Processor ===");
        DataProcessor xmlProcessor = new XMLDataProcessor();
        xmlProcessor.processData();
        
        // Demonstration of the Template Method Pattern
        demonstrateTemplateMethod();
    }
    
    private static void demonstrateTemplateMethod() {
        System.out.println("\n=== Template Method Demonstration ===");
        
        // The algorithm flow is always the same; only the implementation varies
        DataProcessor[] processors = {
            new CSVDataProcessor(),
            new XMLDataProcessor()
        };
        
        for (DataProcessor processor : processors) {
            System.out.println("\nProcessing with " + processor.getClass().getSimpleName() + ":");
            processor.processData();
        }
    }
}

Pattern Categories Overview

Creational Patterns

PatternPurposeExample
SingletonEnsure only one instanceDatabase Connection
FactoryCentralize object creationShape Factory
Abstract FactoryCreate families of objectsGUI Factory
BuilderConstruct complex objects step by stepStringBuilder

Structural Patterns

PatternPurposeExample
AdapterReconcile incompatible interfacesMedia Adapter
DecoratorAdd functionality dynamicallyCoffee Decorator
FacadeSimplify complex interfacesHome Theater
ProxyProvide a stand-inVirtual Proxy

Behavioral Patterns

PatternPurposeExample
ObserverNotify on state changesWeather Station
StrategySwap algorithms at runtimePayment Strategy
IteratorAccess elements sequentiallyCollection Iterator
Template MethodDefine algorithm skeletonData Processor

Benefits of Design Patterns

Reusability

  • Leverage proven solutions across projects
  • Cut development time
  • Maintain architectural consistency

Maintainability

  • Clear separation of concerns
  • Loose coupling between components
  • Easier to modify and extend

Communication

  • Shared vocabulary among developers
  • Well-documented approaches
  • Faster code comprehension

Quality

  • Battle-tested solutions
  • Fewer bugs
  • Higher code quality

Common Exam Questions

  1. When should you use the Singleton Pattern? When you need exactly one instance of a class accessible globally (database connections, loggers).

  2. What’s the difference between Adapter and Decorator? Adapter makes incompatible interfaces work together; Decorator adds behavior to existing objects.

  3. Explain the Observer Pattern. A subject notifies observers when its state changes, keeping components loosely coupled.

  4. Why is Template Method Pattern useful? It defines the skeleton of an algorithm, letting subclasses override specific steps without changing the algorithm’s structure.

Key Resources

  1. https://refactoring.guru/design-patterns
  2. https://www.oreilly.com/library/view/head-first-design/0596007124
  3. https://en.wikipedia.org/wiki/Software_design_pattern

Design Patterns

Books about design patterns and software design

Design Patterns von Gang of Four

Design Patterns von Gang of Four

Bei Amazon ansehen

Affiliate-Link: Bei einem Kauf erhalten wir möglicherweise eine Provision.

Patterns of Enterprise Application Architecture von Martin Fowler

Patterns of Enterprise Application Architecture von Martin Fowler

Bei Amazon ansehen

Affiliate-Link: Bei einem Kauf erhalten wir möglicherweise eine Provision.

Refactoring von Martin Fowler

Refactoring von Martin Fowler

Bei Amazon ansehen

Affiliate-Link: Bei einem Kauf erhalten wir möglicherweise eine Provision.

Back to Blog
Share:

Nächster Artikel in Software Architecture

Weiterlesen
God Interfaces: Anti-Pattern & Interface Segregation

Related Posts