Skip to content
IRC-Coding IRC-Coding
Design Patterns Singleton Observer Factory Adapter Iterator Strategy Decorator

Design Patterns Catalog: Singleton, Observer, Factory & More

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

S

schutzgeist

2 min read

Design Patterns Catalog: Singleton, Observer, Factory, Adapter & Co.

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 reusable, maintainable, and extensible architectures.

Compact Technical Description

Design Patterns (design patterns) are described solutions for typical problems in object-oriented software development. They were categorized by the “Gang of Four” (GoF) and document best practices.

Categories of GoF Patterns:

Creational Patterns

  • Singleton: Ensures that only one instance exists
  • Factory: Object creation without direct instantiation
  • Abstract Factory: Families of related objects
  • Builder: Create complex objects step by step

Structural Patterns

  • Adapter: Adapt incompatible interfaces
  • Decorator: Dynamically add functionality
  • Facade: Simplified interface for subsystems
  • Proxy: Representative for objects

Behavioral Patterns

  • Observer: Notification on state changes
  • Strategy: Make algorithms interchangeable
  • Iterator: Sequential access to collections
  • Template Method: Define skeleton of algorithms

Exam-Relevant Key Points

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

Core Components

  1. Singleton: One instance per class
  2. Observer: 1:n relationship for notifications
  3. Factory: Object creation without new operator
  4. Adapter: Interface adaptation
  5. Iterator: Sequential access to collections
  6. Strategy: Interchangeable algorithms
  7. Decorator: Dynamic functionality extension
  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 establishment
        System.out.println("Database connection established");
    }
    
    // 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("Execute: " + 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("Same object: " + (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("Temperature display: " + 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("Humidity display: " + 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);
        
        // Change 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("Draw circle");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw rectangle");
    }
}

class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw triangle");
    }
}

// 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("Unknown shape type: " + shapeType);
        }
    }
}

// Usage
public class FactoryDemo {
    public static void main(String[] args) {
        ShapeFactory factory = new ShapeFactory();
        
        // Create shapes without 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("Error: " + 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 own formats
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing MP3 file: " + fileName);
        }
        // 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);
        }
        
        // Comparison with Java standard iterator
        List<String> nameList = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
        System.out.println("\nWith 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("\nWith 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 via 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 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 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
SingletonOnly one instanceDatabase Connection
FactoryCentralize object creationShape Factory
Abstract FactoryFamilies of objectsGUI Factory
BuilderComplex objects step by stepStringBuilder

Structural Patterns

PatternPurposeExample
AdapterIncompatible interfacesMedia Adapter
DecoratorDynamic functionalityCoffee Decorator
FacadeSimplified interfaceHome Theater
ProxyRepresentativeVirtual Proxy

Behavioral Patterns

PatternPurposeExample
ObserverNotification on changesWeather Station
StrategyInterchangeable algorithmsPayment Strategy
IteratorSequential accessCollection Iterator
Template MethodAlgorithm skeletonData Processor

Benefits of Design Patterns

Reusability

  • Proven solutions can be reused
  • Reduce development effort
  • Consistent architecture

Maintainability

  • Clear responsibilities
  • Loose coupling between components
  • Easy modification and extension

Communication

  • Shared language for developers
  • Documented solution approaches
  • Quick code understanding

Quality

  • Tested and proven solutions
  • Error reduction
  • Better code quality

Common Exam Questions

  1. When do you use the Singleton Pattern? When only one instance of a class is needed globally (Database Connection, Logger).

  2. What is the difference between Adapter and Decorator? Adapter adapts incompatible interfaces, Decorator adds functionality.

  3. Explain the Observer Pattern! Subject notifies Observer on state changes, loose coupling between components.

  4. Why is the Template Method Pattern useful? Defines algorithm skeleton, allows subclasses to implement specific steps.

Most Important Sources

  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