Skip to content
IRC-CodingIRC-Coding
Design PatternsSingletonObserverFactoryAdapterIteratorStrategyDecoratorAlgorithmsAlgorithmFundamentals

Design Patterns Catalog: Singleton, Observer, Factory & More

Master essential design patterns with practical implementations: Singleton, Observer, Factory, Adapter, Iterator, Strategy, Decorator, Template Method.

S

schutzgeist

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

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

More Design Patterns Articles:

This article is a comprehensive catalog of the essential 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 (also known as Entwurfsmuster) are documented solutions for typical challenges in object-oriented software development. They were categorized by the Gang of Four (GoF) and represent best practices in software design.

GoF Pattern Categories:

Creational Patterns

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

Structural Patterns

  • Adapter: Reconciles incompatible interfaces
  • Decorator: Adds functionality dynamically
  • Facade: Provides a simplified interface to subsystems
  • Proxy: Acts as a placeholder for objects

Behavioral Patterns

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

Key Exam Concepts

  • Singleton: Global instance, thread-safe implementation
  • Observer: Subject-Observer relationship, loose coupling
  • Factory: Centralizes object creation, enables polymorphism
  • Adapter: Interface adaptation, composition vs. inheritance
  • Iterator: Uniform access to containers, hasNext/next
  • Strategy: Interchangeable algorithms, Open-Closed Principle
  • Decorator: Extends functionality dynamically
  • Template Method: Algorithm skeleton, hook methods
  • Professional Relevance: Critical for software architecture and design

Core Components

  1. Singleton: One instance per class
  2. Observer: One-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: Interchangeable 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;
    
    // Private constructor prevents external instantiation
    private DatabaseConnection() {
        this.connectionUrl = "jdbc:mysql://localhost:3306/mydb";
        // Simulated connection setup
        System.out.println("Datenbankverbindung hergestellt");
    }
    
    // Double-Checked Locking for 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;
    }
}

// Usage
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 + "%");
    }
}

// Usage
public class ObserverDemo {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();
        
        TemperatureDisplay tempDisplay = new TemperatureDisplay();
        HumidityDisplay humidityDisplay = new HumidityDisplay();
        
        // Register observers
        weatherStation.registerObserver(tempDisplay);
        weatherStation.registerObserver(humidityDisplay);
        
        // Update measurements
        weatherStation.setMeasurements(25.5f, 65.0f);
        weatherStation.setMeasurements(26.0f, 63.5f);
        
        // Remove observer
        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);
        }
    }
}

// Usage
public class FactoryDemo {
    public static void main(String[] args) {
        ShapeFactory factory = new ShapeFactory();
        
        // Create shapes without the new operator
        Shape circle = factory.createShape("CIRCLE");
        Shape rectangle = factory.createShape("RECTANGLE");
        Shape triangle = factory.createShape("TRIANGLE");
        
        // Use shapes
        circle.draw();
        rectangle.draw();
        triangle.draw();
        
        // Factory with configuration
        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);
        }
        // Delegate other formats via 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);
        }
        
        // Compare 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 by credit card.");
        System.out.println("Cardholder: " + 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 by 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 by 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 selection");
                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 to prevent subclasses from overriding)
    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 complete");
    }
}

// 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 Template Method Pattern
        demonstrateTemplateMethod();
    }
    
    private static void demonstrateTemplateMethod() {
        System.out.println("\n=== Template Method Demonstration ===");
        
        // The algorithm flow remains constant; 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
SingletonSingle instance onlyDatabase Connection
FactoryCentralize object creationShape Factory
Abstract FactoryFamilies of objectsGUI Factory
BuilderComplex 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 an algorithm skeletonData Processor

Benefits of Design Patterns

Reusability

  • Proven solutions you can leverage across projects
  • Cut development time and effort
  • Maintain consistent architecture

Maintainability

  • Clear responsibilities at every layer
  • Loose coupling keeps components independent
  • Changes and extensions become straightforward

Communication

  • Shared vocabulary across your team
  • Well-documented, recognized approaches
  • New developers grasp the code faster

Quality

  • Solutions already battle-tested
  • Fewer bugs and edge cases
  • Higher overall code standards

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. How do Adapter and Decorator differ? Adapter bridges incompatible interfaces; Decorator layers on new functionality.

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

  4. Why is Template Method valuable? It outlines the algorithm structure while letting subclasses fill in specific steps.

Essential 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:

Related Posts