Skip to content
IRC-CodingIRC-Coding
Design PatternsSingletonObserverFactoryAdapterIteratorStrategyDecoratorAlgoritmosAlgoritmoFundamentos

Catálogo de Design Patterns: Singleton, Observer, Factory

Design Patterns esenciales con implementaciones: Singleton, Observer, Factory, Adapter, Iterator, Strategy, Decorator, Template Method.

S

schutzgeist

12 min read
Catálogo de Design Patterns: Singleton, Observer, Factory

Catálogo de Design Patterns: Singleton, Observer, Factory, Adapter y otros

Otros artículos sobre Design Patterns:

Este artículo es un catálogo exhaustivo de los Design Patterns más importantes, incluyendo Singleton, Observer, Factory, Adapter, Iterator, Strategy, Decorator y Template Method con implementaciones prácticas.

En pocas palabras

Los Design Patterns son plantillas de solución probadas para problemas recurrentes en el desarrollo de software. Permiten crear arquitecturas reutilizables, mantenibles y escalables.

Descripción técnica compacta

Los Design Patterns (patrones de diseño) son soluciones documentadas para problemas típicos en el desarrollo de software orientado a objetos. Fueron categorizados por la “Gang of Four” (GoF) y documentan prácticas probadas.

Categorías de patrones GoF:

Patrones Creacionales

  • Singleton: Garantiza que solo exista una instancia
  • Factory: Creación de objetos sin instanciación directa
  • Abstract Factory: Familias de objetos relacionados
  • Builder: Construcción paso a paso de objetos complejos

Patrones Estructurales

  • Adapter: Adapta interfaces incompatibles
  • Decorator: Añade funcionalidad dinámicamente
  • Facade: Interfaz simplificada para subsistemas
  • Proxy: Representante de objetos

Patrones de Comportamiento

  • Observer: Notificación ante cambios de estado
  • Strategy: Algoritmos intercambiables
  • Iterator: Acceso secuencial a colecciones
  • Template Method: Define el esqueleto de algoritmos

Puntos clave para examen

  • Singleton: Instancia global, implementación thread-safe
  • Observer: Relación Subject-Observer, acoplamiento débil
  • Factory: Centraliza creación de objetos, polimorfismo
  • Adapter: Adaptación de interfaces, composición vs herencia
  • Iterator: Acceso uniforme a contenedores, hasNext/next
  • Strategy: Algoritmos intercambiables, Open-Closed Principle
  • Decorator: Extiende funcionalidad, dinámicamente
  • Template Method: Esqueleto de algoritmo, métodos hook
  • Relevancia IHK: Importante para arquitectura y diseño de software

Componentes principales

  1. Singleton: Una instancia por clase
  2. Observer: Relación 1:n para notificaciones
  3. Factory: Creación de objetos sin new
  4. Adapter: Adaptación de interfaces
  5. Iterator: Acceso secuencial a colecciones
  6. Strategy: Algoritmos intercambiables
  7. Decorator: Extensión dinámica de funcionalidad
  8. Template Method: Esqueleto de algoritmo con pasos variables

Ejemplos prácticos

1. Patrón Singleton

// Singleton thread-safe con inicialización perezosa
public class DatabaseConnection {
    private static volatile DatabaseConnection instance;
    private final String connectionUrl;
    
    // Constructor privado evita instanciación externa
    private DatabaseConnection() {
        this.connectionUrl = "jdbc:mysql://localhost:3306/mydb";
        // Simulación de establecimiento de conexión
        System.out.println("Datenbankverbindung hergestellt");
    }
    
    // Double-Checked Locking para 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;
    }
}

// Uso
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. Patrón Observer

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

// Uso
public class ObserverDemo {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();
        
        TemperatureDisplay tempDisplay = new TemperatureDisplay();
        HumidityDisplay humidityDisplay = new HumidityDisplay();
        
        // Registrar observers
        weatherStation.registerObserver(tempDisplay);
        weatherStation.registerObserver(humidityDisplay);
        
        // Cambiar mediciones
        weatherStation.setMeasurements(25.5f, 65.0f);
        weatherStation.setMeasurements(26.0f, 63.5f);
        
        // Remover observer
        weatherStation.removeObserver(humidityDisplay);
        weatherStation.setMeasurements(24.8f, 67.2f);
    }
}

3. Patrón Factory

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

// Uso
public class FactoryDemo {
    public static void main(String[] args) {
        ShapeFactory factory = new ShapeFactory();
        
        // Crear shapes sin operador new
        Shape circle = factory.createShape("CIRCLE");
        Shape rectangle = factory.createShape("RECTANGLE");
        Shape triangle = factory.createShape("TRIANGLE");
        
        // Usar shapes
        circle.draw();
        rectangle.draw();
        triangle.draw();
        
        // Factory con configuración
        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 (interfaz incompatible)
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("Reproduciendo archivo VLC: " + fileName);
    }
    
    @Override
    public void playMp4(String fileName) {
        // No soportado
    }
}

class Mp4Player implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        // No soportado
    }
    
    @Override
    public void playMp4(String fileName) {
        System.out.println("Reproduciendo archivo MP4: " + 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) {
        // Soportar formatos propios
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Reproduciendo archivo MP3: " + fileName);
        }
        // Otros formatos a través del adaptador
        else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Formato multimedia inválido: " + audioType);
        }
    }
}

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

// Uso
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);
        }
        
        // Comparación con Iterator estándar de Java
        List<String> nameList = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
        System.out.println("\nCon Iterator de Java:");
        for (Iterator<String> it = nameList.iterator(); it.hasNext();) {
            String name = it.next();
            System.out.println("Name: " + name);
        }
        
        // Enhanced for loop (azúcar sintáctico)
        System.out.println("\nCon 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 + " € pagados con tarjeta de crédito.");
        System.out.println("Titular: " + name);
        System.out.println("Número de tarjeta: **** **** **** " + 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 + " € pagados con PayPal.");
        System.out.println("Cuenta PayPal: " + 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 + " € pagados con Bitcoin.");
        System.out.println("Billetera: " + 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 se ha seleccionado método de pago");
        }
        paymentStrategy.pay(amount);
    }
}

// Uso
public class StrategyDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        
        // Diferentes métodos de pago
        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);
        
        // Selección dinámica de estrategia
        dynamicStrategySelection(cart);
    }
    
    private static void dynamicStrategySelection(ShoppingCart cart) {
        Scanner scanner = new Scanner(System.in);
        
        System.out.println("\nSelecciona método de pago:");
        System.out.println("1. Tarjeta de crédito");
        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("Selección inválida");
                return;
        }
        
        cart.checkout(amount);
        scanner.close();
    }
}

7. Patrón Decorator

// 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() + "€");
    }
}

Este patrón te permite agregar funcionalidades de forma dinámica a objetos. En lugar de heredar, envuelves un objeto con decoradores que amplían su comportamiento. En el ejemplo, partimos de un café simple y vamos agregando ingredientes: leche, azúcar, crema batida. Cada decorador suma su precio y descripción sin modificar la clase original.

8. Patrón Template Method

// 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();
        }
    }
}

El Template Method define la estructura de un algoritmo en la clase base, pero deja que las subclases implementen los detalles específicos. La clase abstracta DataProcessor define el flujo: cargar datos, validar, transformar, guardar y limpiar. Cada procesador concreto (CSV, XML) implementa sus propios pasos sin cambiar el orden del algoritmo.

Resumen de Categorías de Patrones

Patrones Creacionales

PatrónPropósitoEjemplo
SingletonGarantizar una única instanciaDatabase Connection
FactoryCentralizar la creación de objetosShape Factory
Abstract FactoryFamilias de objetos relacionadosGUI Factory
BuilderConstruir objetos complejos paso a pasoStringBuilder

Patrones Estructurales

PatrónPropósitoEjemplo
AdapterInterfaces incompatiblesMedia Adapter
DecoratorAñadir funcionalidad dinámicamenteCoffee Decorator
FacadeInterfaz simplificadaHome Theater
ProxyRepresentanteVirtual Proxy

Patrones de Comportamiento

PatrónPropósitoEjemplo
ObserverNotificación ante cambiosWeather Station
StrategyAlgoritmos intercambiablesPayment Strategy
IteratorAcceso secuencialCollection Iterator
Template MethodEstructura del algoritmoData Processor

Ventajas de los Design Patterns

Reutilización

  • Las soluciones probadas pueden adaptarse a nuevos proyectos
  • Disminuyen el esfuerzo de desarrollo
  • Garantizan coherencia arquitectónica

Mantenibilidad

  • Responsabilidades claramente definidas
  • Bajo acoplamiento entre componentes
  • Cambios y extensiones simplificados

Comunicación

  • Lenguaje común entre desarrolladores
  • Soluciones documentadas
  • Comprensión rápida del código

Calidad

  • Soluciones testeadas y validadas
  • Menos errores en el código
  • Mejor calidad general

Preguntas frecuentes en exámenes

  1. ¿Cuándo usas el patrón Singleton? Cuando necesitas una única instancia de una clase accesible globalmente (conexión a base de datos, logger).

  2. ¿Cuál es la diferencia entre Adapter y Decorator? Adapter adapta interfaces incompatibles, mientras que Decorator añade funcionalidad a un objeto existente.

  3. Explica el patrón Observer El Subject notifica a los Observers cuando cambia su estado, manteniendo bajo acoplamiento entre componentes.

  4. ¿Por qué es útil el patrón Template Method? Define la estructura de un algoritmo permitiendo que las subclases implementen pasos específicos.

Referencias principales

  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

Lecturas recomendadas: Design Patterns

Keine Bücher für Kategorie "design-patterns" gefunden.

Volver al blog
Share:

Entradas relacionadas