Programación Orientada a Objetos

Semana 7: Patrones de Diseño en Práctica

El plan para hoy

Profundizar en patrones creacionales: Builder Explorar patrones estructurales: Decorator y Adapter Analizar patrones de comportamiento: Observer y Strategy Caso práctico: Aplicación Swing con múltiples patrones

Patrón Builder vs Factory Method

El patrón Builder nos permite construir objetos complejos paso a paso.

// Producto
class Pizza {
    private String masa;
    private String salsa;
    private String queso;
    private List<String> toppings = new ArrayList<>();
    
    // Getters...
    
    @Override
    public String toString() {
        return "Pizza con masa " + masa + ", salsa " + salsa + 
               ", queso " + queso + " y toppings: " + toppings;
    }
}

// Builder
class PizzaBuilder {
    private Pizza pizza = new Pizza();
    
    public PizzaBuilder setMasa(String masa) {
        pizza.masa = masa;
        return this;
    }
    
    public PizzaBuilder setSalsa(String salsa) {
        pizza.salsa = salsa;
        return this;
    }
    
    public PizzaBuilder setQueso(String queso) {
        pizza.queso = queso;
        return this;
    }
    
    public PizzaBuilder addTopping(String topping) {
        pizza.toppings.add(topping);
        return this;
    }
    
    public Pizza build() {
        return pizza;
    }
}

// Director (opcional)
class PizzaDirector {
    public Pizza construirPizzaMargarita(PizzaBuilder builder) {
        return builder
            .setMasa("delgada")
            .setSalsa("tomate")
            .setQueso("mozzarella")
            .addTopping("albahaca")
            .build();
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        PizzaBuilder builder = new PizzaBuilder();
        
        // Construcción manual
        Pizza pizzaPersonalizada = builder
            .setMasa("gruesa")
            .setSalsa("BBQ")
            .setQueso("cheddar")
            .addTopping("jamón")
            .addTopping("piña")
            .build();
            
        // Usando director
        PizzaDirector director = new PizzaDirector();
        Pizza pizzaMargarita = director.construirPizzaMargarita(new PizzaBuilder());
    }
}

Ejemplo con Factory Method

// Producto (interface)
interface Pizza {
    String getDescription();
}

// Implementaciones concretas
class PizzaMargarita implements Pizza {
    private String masa;
    private String salsa;
    private String queso;
    private List<String> toppings = new ArrayList<>();
    
    public PizzaMargarita() {
        this.masa = "delgada";
        this.salsa = "tomate";
        this.queso = "mozzarella";
        this.toppings.add("albahaca");
    }
    
    @Override
    public String getDescription() {
        return "Pizza Margarita con masa " + masa + ", salsa " + salsa + 
               ", queso " + queso + " y toppings: " + toppings;
    }
}

class PizzaHawaiana implements Pizza {
    private String masa;
    private String salsa;
    private String queso;
    private List<String> toppings = new ArrayList<>();
    
    public PizzaHawaiana() {
        this.masa = "gruesa";
        this.salsa = "BBQ";
        this.queso = "cheddar";
        this.toppings.add("jamón");
        this.toppings.add("piña");
    }
    
    @Override
    public String getDescription() {
        return "Pizza Hawaiana con masa " + masa + ", salsa " + salsa + 
               ", queso " + queso + " y toppings: " + toppings;
    }
}

// Factory Method (interface)
interface PizzaFactory {
    Pizza crearPizza();
}

// Implementaciones concretas de Factory
class PizzaMargaritaFactory implements PizzaFactory {
    @Override
    public Pizza crearPizza() {
        return new PizzaMargarita();
    }
}

class PizzaHawaianaFactory implements PizzaFactory {
    @Override
    public Pizza crearPizza() {
        return new PizzaHawaiana();
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        // Crear pizzas usando diferentes factories
        PizzaFactory factoryMargarita = new PizzaMargaritaFactory();
        Pizza pizzaMargarita = factoryMargarita.crearPizza();
        System.out.println(pizzaMargarita.getDescription());
        
        PizzaFactory factoryHawaiana = new PizzaHawaianaFactory();
        Pizza pizzaHawaiana = factoryHawaiana.crearPizza();
        System.out.println(pizzaHawaiana.getDescription());
    }
}

Ejemplo con Abstract Factory

// Product interfaces
interface Pizza {
    String getDescription();
}

interface Bebida {
    String getDescription();
}

// Pizza implementations
class PizzaMargaritaItaliana implements Pizza {
    @Override
    public String getDescription() {
        return "Pizza Margarita Italiana con masa fina, salsa de tomate casera, mozzarella fresca y albahaca";
    }
}

class PizzaHawaianaItaliana implements Pizza {
    @Override
    public String getDescription() {
        return "Pizza Hawaiana Italiana con masa fina, salsa de tomate, mozzarella, jamón y piña";
    }
}

class PizzaMargaritaAmericana implements Pizza {
    @Override
    public String getDescription() {
        return "Pizza Margarita Americana con masa gruesa, salsa de tomate, queso mozzarella y orégano";
    }
}

class PizzaHawaianaAmericana implements Pizza {
    @Override
    public String getDescription() {
        return "Pizza Hawaiana Americana con masa gruesa, salsa BBQ, queso cheddar, jamón y piña";
    }
}

// Bebida implementations
class BebidaItaliana implements Bebida {
    @Override
    public String getDescription() {
        return "Vino tinto italiano";
    }
}

class BebidaAmericana implements Bebida {
    @Override
    public String getDescription() {
        return "Refresco de cola grande";
    }
}

// Abstract Factory interface
interface PizzaRestaurantFactory {
    Pizza crearPizza(String tipo);
    Bebida crearBebida();
}

// Concrete Factories
class PizzeriaItaliana implements PizzaRestaurantFactory {
    @Override
    public Pizza crearPizza(String tipo) {
        if (tipo.equals("margarita")) {
            return new PizzaMargaritaItaliana();
        } else if (tipo.equals("hawaiana")) {
            return new PizzaHawaianaItaliana();
        }
        throw new IllegalArgumentException("Tipo de pizza no disponible");
    }
    
    @Override
    public Bebida crearBebida() {
        return new BebidaItaliana();
    }
}

class PizzeriaAmericana implements PizzaRestaurantFactory {
    @Override
    public Pizza crearPizza(String tipo) {
        if (tipo.equals("margarita")) {
            return new PizzaMargaritaAmericana();
        } else if (tipo.equals("hawaiana")) {
            return new PizzaHawaianaAmericana();
        }
        throw new IllegalArgumentException("Tipo de pizza no disponible");
    }
    
    @Override
    public Bebida crearBebida() {
        return new BebidaAmericana();
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        // Crear un pedido italiano
        PizzaRestaurantFactory pizzeriaItaliana = new PizzeriaItaliana();
        Pizza pizzaItalianaMargarita = pizzeriaItaliana.crearPizza("margarita");
        Bebida bebidaItaliana = pizzeriaItaliana.crearBebida();
        
        System.out.println("Pedido italiano:");
        System.out.println(pizzaItalianaMargarita.getDescription());
        System.out.println(bebidaItaliana.getDescription());
        
        // Crear un pedido americano
        PizzaRestaurantFactory pizzeriaAmericana = new PizzeriaAmericana();
        Pizza pizzaAmericanaHawaiana = pizzeriaAmericana.crearPizza("hawaiana");
        Bebida bebidaAmericana = pizzeriaAmericana.crearBebida();
        
        System.out.println("\nPedido americano:");
        System.out.println(pizzaAmericanaHawaiana.getDescription());
        System.out.println(bebidaAmericana.getDescription());
    }
}

Casos Prácticos: Builder

  • Sistema de pedidos de restaurante (Subway, Chipotle)
  • Generador de documentos PDF (iText, PDFBox)
  • Configuración de conexiones a bases de datos (Hibernate)
  • Frameworks UI fluidos (SwiftUI, Flutter)
  • Constructores de consultas SQL (JPA Criteria API)

Casos Prácticos: Factory Method

  • Drivers de bases de datos (JDBC)
  • Procesadores de pago (tarjeta, PayPal, transferencia)
  • Logística y transporte (camión, barco, avión)
  • Parsers de diferentes formatos de archivo (PDF, DOC, XML)
  • Renderizadores de gráficos (según capacidades hardware)

Casos Prácticos: Abstract Factory

  • Interfaces UI multiplataforma (Windows, macOS, Linux)
  • Temas visuales en videojuegos (medieval, futurista, actual)
  • Conectores a múltiples bases de datos con sus herramientas
  • Sistemas de reservas de viaje (económico, business, premium)
  • Suites de producción multimedia (diferentes formatos)

Patrón Decorator

Permite añadir funcionalidades a objetos existentes dinámicamente.

// Componente base
interface Coffee {
    String getDescription();
    double getCost();
}

// Componente concreto
class SimpleCoffee implements Coffee {
    public String getDescription() {
        return "Café simple";
    }
    
    public double getCost() {
        return 1.0;
    }
}

// Decorator base
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;
    
    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
    
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
    
    public double getCost() {
        return decoratedCoffee.getCost();
    }
}

// Decoradores concretos
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", con leche";
    }
    
    public double getCost() {
        return decoratedCoffee.getCost() + 0.5;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }
    
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", con azúcar";
    }
    
    public double getCost() {
        return decoratedCoffee.getCost() + 0.2;
    }
}

// Uso
public class CoffeeShop {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);
        
        System.out.println("Descripción: " + coffee.getDescription());
        System.out.println("Costo: $" + coffee.getCost());
    }
}

Casos Prácticos: Decorator

  • Streams de Java (FileInputStream, BufferedInputStream)
  • Sistemas de notificación (email, SMS, push)
  • Renderizadores de texto (negrita, cursiva, subrayado)
  • Sistemas de autorización y seguridad (filtros, validaciones)
  • Bebidas personalizables en cafeterías (extras y modificadores)

Patrón Adapter

Permite que interfaces incompatibles trabajen juntas.

// Interface objetivo
interface MediaPlayer {
    void play(String fileName);
}

// Interface incompatible
interface AdvancedMediaPlayer {
    void playMp4(String fileName);
    void playAvi(String fileName);
}

// Implementación de la interface incompatible
class AdvancedMediaPlayerImpl implements AdvancedMediaPlayer {
    public void playMp4(String fileName) {
        System.out.println("Reproduciendo MP4: " + fileName);
    }
    
    public void playAvi(String fileName) {
        System.out.println("Reproduciendo AVI: " + fileName);
    }
}

// Adapter
class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedPlayer;
    
    public MediaAdapter() {
        this.advancedPlayer = new AdvancedMediaPlayerImpl();
    }
    
    public void play(String fileName) {
        if(fileName.endsWith(".mp4")) {
            advancedPlayer.playMp4(fileName);
        } else if(fileName.endsWith(".avi")) {
            advancedPlayer.playAvi(fileName);
        } else {
            System.out.println("Formato no soportado");
        }
    }
}

// Cliente que usa el adapter
class AudioPlayer implements MediaPlayer {
    private MediaAdapter mediaAdapter;
    
    public AudioPlayer() {
        this.mediaAdapter = new MediaAdapter();
    }
    
    public void play(String fileName) {
        if(fileName.endsWith(".mp3")) {
            System.out.println("Reproduciendo MP3: " + fileName);
        } else {
            mediaAdapter.play(fileName);
        }
    }
}

// Uso
public class MusicApp {
    public static void main(String[] args) {
        AudioPlayer player = new AudioPlayer();
        
        player.play("cancion.mp3");  // Reproducción nativa
        player.play("video.mp4");    // Usa adapter
        player.play("pelicula.avi"); // Usa adapter
    }
}

Casos Prácticos: Adapter

  • Frameworks de lecturas de datos legacy (JDBC-ODBC Bridge)
  • Integración de APIs de terceros (redes sociales, pagos)
  • Wrappers de librerías gráficas (JavaFX con Swing)
  • Adaptadores de formatos de archivo (XML a JSON)
  • Conectores entre sistemas incompatibles (antiguos y modernos)

Patrón Strategy

Permite definir una familia de algoritmos intercambiables.

// Strategy interface
interface SortStrategy {
    void sort(int[] array);
}

// Estrategias concretas
class BubbleSort implements SortStrategy {
    public void sort(int[] array) {
        System.out.println("Ordenando con Bubble Sort");
        // Implementación del bubble sort
    }
}

class QuickSort implements SortStrategy {
    public void sort(int[] array) {
        System.out.println("Ordenando con Quick Sort");
        // Implementación del quick sort
    }
}

class MergeSort implements SortStrategy {
    public void sort(int[] array) {
        System.out.println("Ordenando con Merge Sort");
        // Implementación del merge sort
    }
}

// Contexto
class Sorter {
    private SortStrategy strategy;
    
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void sort(int[] array) {
        if(strategy == null) {
            throw new IllegalStateException("Estrategia no definida");
        }
        strategy.sort(array);
    }
}

// Uso
public class SortingApp {
    public static void main(String[] args) {
        int[] array = {64, 34, 25, 12, 22, 11, 90};
        Sorter sorter = new Sorter();
        
        // Usando diferentes estrategias
        sorter.setStrategy(new BubbleSort());
        sorter.sort(array);  // Ordenamiento con bubble sort
        
        sorter.setStrategy(new QuickSort());
        sorter.sort(array);  // Ordenamiento con quick sort
    }
}

Patrón Observer (Repaso y Profundización)

// Interface Observable (Subject)
interface WeatherStation {
    void registerObserver(WeatherObserver observer);
    void removeObserver(WeatherObserver observer);
    void notifyObservers();
}

// Interface Observer
interface WeatherObserver {
    void update(float temperature, float humidity, float pressure);
}

// Implementación Concreta del Observable
class WeatherData implements WeatherStation {
    private List<WeatherObserver> observers = new ArrayList<>();
    private float temperature;
    private float humidity;
    private float pressure;
    
    public void registerObserver(WeatherObserver observer) {
        observers.add(observer);
    }
    
    public void removeObserver(WeatherObserver observer) {
        observers.remove(observer);
    }
    
    public void notifyObservers() {
        for(WeatherObserver observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }
    
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObservers();
    }
}

// Implementaciones Concretas de Observers
class CurrentConditionsDisplay implements WeatherObserver {
    public void update(float temperature, float humidity, float pressure) {
        System.out.println("Condiciones actuales:");
        System.out.println("Temperatura: " + temperature + "°C");
        System.out.println("Humedad: " + humidity + "%");
    }
}

class StatisticsDisplay implements WeatherObserver {
    private List<Float> temperatures = new ArrayList<>();
    
    public void update(float temperature, float humidity, float pressure) {
        temperatures.add(temperature);
        displayStats();
    }
    
    private void displayStats() {
        float avg = temperatures.stream()
            .reduce(0f, Float::sum) / temperatures.size();
        float max = Collections.max(temperatures);
        float min = Collections.min(temperatures);
        
        System.out.println("\nEstadísticas de Temperatura:");
        System.out.println("Promedio: " + avg + "°C");
        System.out.println("Máxima: " + max + "°C");
        System.out.println("Mínima: " + min + "°C");
    }
}

// Uso
public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        
        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
        StatisticsDisplay statsDisplay = new StatisticsDisplay();
        
        weatherData.registerObserver(currentDisplay);
        weatherData.registerObserver(statsDisplay);
        
        // Simulando cambios en el clima
        weatherData.setMeasurements(25.5f, 65.0f, 1013.1f);
        weatherData.setMeasurements(26.7f, 70.0f, 1014.2f);
    }
}

Caso Práctico: Convertidor de Unidades

Implementaremos un convertidor de unidades usando Swing y varios patrones de diseño.

// Strategy para diferentes tipos de conversión
interface ConversionStrategy {
    double convert(double value);
    String getFromUnit();
    String getToUnit();
}

class CelsiusToFahrenheitStrategy implements ConversionStrategy {
    public double convert(double celsius) {
        return (celsius * 9/5) + 32;
    }
    public String getFromUnit() { return "Celsius"; }
    public String getToUnit() { return "Fahrenheit"; }
}

class KilometersToMilesStrategy implements ConversionStrategy {
    public double convert(double km) {
        return km * 0.621371;
    }
    public String getFromUnit() { return "Kilómetros"; }
    public String getToUnit() { return "Millas"; }
}

// Observer para actualizar la interfaz
interface ConversionObserver {
    void onConversionPerformed(double result);
}

// La ventana principal implementa el observer
public class UnitConverterGUI extends JFrame implements ConversionObserver {
    private JTextField inputField;
    private JLabel resultLabel;
    private JComboBox<String> conversionType;
    private Map<String, ConversionStrategy> strategies;
    
    public UnitConverterGUI() {
        setupStrategies();
        setupGUI();
    }
    
    private void setupStrategies() {
        strategies = new HashMap<>();
        strategies.put("Temperatura", new CelsiusToFahrenheitStrategy());
        strategies.put("Distancia", new KilometersToMilesStrategy());
    }
    
    private void setupGUI() {
        setTitle("Convertidor de Unidades");
        setLayout(new GridLayout(4, 1, 10, 10));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        conversionType = new JComboBox<>(strategies.keySet().toArray(new String[0]));
        inputField = new JTextField();
        JButton convertButton = new JButton("Convertir");
        resultLabel = new JLabel("Resultado: ", SwingConstants.CENTER);
        
        convertButton.addActionListener(e -> performConversion());
        
        add(conversionType);
        add(inputField);
        add(convertButton);
        add(resultLabel);
        
        pack();
        setSize(300, 200);
        setLocationRelativeTo(null);
    }
    
    private void performConversion() {
        try {
            String selected = (String) conversionType.getSelectedItem();
            ConversionStrategy strategy = strategies.get(selected);
            double value = Double.parseDouble(inputField.getText());
            double result = strategy.convert(value);
            onConversionPerformed(result);
        } catch (NumberFormatException ex) {
            resultLabel.setText("Error: Ingrese un número válido");
        }
    }
    
    @Override
    public void onConversionPerformed(double result) {
        String selected = (String) conversionType.getSelectedItem();
        ConversionStrategy strategy = strategies.get(selected);
        resultLabel.setText(String.format("%.2f %s = %.2f %s", 
            Double.parseDouble(inputField.getText()),
            strategy.getFromUnit(),
            result,
            strategy.getToUnit()));
    }
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new UnitConverterGUI().setVisible(true);
        });
    }
}

¿Preguntas?