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

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

Wichtige Design Patterns mit Implementierungen. Singleton, Observer, Factory, Adapter, Iterator, Strategy, Decorator, Template Method mit praktischen Beispielen.

S

schutzgeist

2 min read

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

  1. Singleton: Eine Instanz pro Klasse
  2. Observer: 1:n Beziehung für Benachrichtigungen
  3. Factory: Objekterstellung ohne new-Operator
  4. Adapter: Schnittstellenanpassung
  5. Iterator: Sequenzieller Zugriff auf Sammlungen
  6. Strategy: Austauschbare Algorithmen
  7. Decorator: Dynamische Funktionalitätserweiterung
  8. 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)

PatternZweckBeispiel
SingletonNur eine InstanzDatabase Connection
FactoryObjekterstellung zentralisierenShape Factory
Abstract FactoryFamilien von ObjektenGUI Factory
BuilderKomplexe Objekte schrittweiseStringBuilder

Structural Patterns (Strukturmuster)

PatternZweckBeispiel
AdapterInkompatible SchnittstellenMedia Adapter
DecoratorDynamisch FunktionalitätCoffee Decorator
FacadeVereinfachte SchnittstelleHome Theater
ProxyStellvertreterVirtual Proxy

Behavioral Patterns (Verhaltensmuster)

PatternZweckBeispiel
ObserverBenachrichtigung bei ÄnderungenWeather Station
StrategyAlgorithmen austauschbarPayment Strategy
IteratorSequenzieller ZugriffCollection Iterator
Template MethodAlgorithmus-SkelettData 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

  1. Wann verwendet man das Singleton Pattern? Wenn nur eine Instanz einer Klasse global benötigt wird (Database Connection, Logger).

  2. Was ist der Unterschied zwischen Adapter und Decorator? Adapter passt inkompatible Schnittstellen an, Decorator fügt Funktionalität hinzu.

  3. Erklären Sie das Observer Pattern! Subject benachrichtigt Observer bei Zustandsänderungen, lose Kopplung zwischen Komponenten.

  4. Warum ist das Template Method Pattern nützlich? Definiert Algorithmus-Skelett, erlaubt Unterklassen spezifische Schritte zu implementieren.

Wichtigste Quellen

  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
Zurück zum Blog
Share:

Ähnliche Beiträge