Ingeniería de Software: Patrones de diseño

Tiempo de lectura: 5 minutos

Los patrones de diseño son soluciones probadas y estándar para problemas comunes de diseño de software. Proporcionan un enfoque reutilizable y estructurado para resolver problemas específicos durante el desarrollo de software orientado a objetos.

A continuación, se presentan algunos patrones de diseño comunes y cómo se aplican en el desarrollo de software:

  • Singleton: Este patrón garantiza que una clase tenga una sola instancia y proporciona un punto de acceso global a esa instancia. Es útil cuando se necesita una única instancia compartida en toda la aplicación, como un objeto de registro o un objeto de configuración.

Ejemplo de Uso del Patrón Singleton en Java:

   public class Singleton {
       private static Singleton instance;

       // Constructor privado para evitar la instanciación externa
       private Singleton() {}

       // Método estático para obtener la instancia única
       public static Singleton getInstance() {
           if (instance == null) {
               instance = new Singleton();
           }
           return instance;
       }
   }

En este ejemplo, la clase Singleton garantiza que solo exista una instancia de la clase en toda la aplicación. El método getInstance() proporciona un punto de acceso global a esa instancia única.

  • Factory: El patrón Factory se utiliza para encapsular la creación de objetos y ocultar los detalles de implementación. Define una interfaz para crear un objeto, pero permite a las subclases decidir qué clase concreta instanciar. Esto facilita la creación de objetos sin exponer la lógica de creación.

Ejemplo:

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("Woof");
    }
}

class Cat implements Animal {
    public void makeSound() {
        System.out.println("Meow");
    }
}

class AnimalFactory {
    public static Animal createAnimal(String type) {
        if (type.equalsIgnoreCase("dog")) {
            return new Dog();
        } else if (type.equalsIgnoreCase("cat")) {
            return new Cat();
        } else {
            return null;
        }
    }
}

En este ejemplo, AnimalFactory encapsula la creación de objetos Animal. Dependiendo del tipo pasado como argumento a createAnimal(), devuelve una instancia de Dog o Cat.

  • Builder: Este patrón se utiliza para construir objetos complejos paso a paso. Permite la creación de diferentes representaciones de un objeto utilizando el mismo proceso de construcción. Es útil cuando se necesita crear objetos con muchas opciones de configuración.

Ejemplo:

class Pizza {
    private String dough = "";
    private String sauce = "";
    private String topping = "";
    
    public void setDough(String dough) {
        this.dough = dough;
    }
    
    public void setSauce(String sauce) {
        this.sauce = sauce;
    }
    
    public void setTopping(String topping) {
        this.topping = topping;
    }
}

class PizzaBuilder {
    private Pizza pizza;
    
    public PizzaBuilder() {
        pizza = new Pizza();
    }
    
    public PizzaBuilder addDough(String dough) {
        pizza.setDough(dough);
        return this;
    }
    
    public PizzaBuilder addSauce(String sauce) {
        pizza.setSauce(sauce);
        return this;
    }
    
    public PizzaBuilder addTopping(String topping) {
        pizza.setTopping(topping);
        return this;
    }
    
    public Pizza build() {
        return pizza;
    }
}

En este ejemplo, PizzaBuilder facilita la creación de objetos Pizza permitiendo configurar opcionalmente la masa, la salsa y los ingredientes antes de construir la pizza.

  • Observer: El patrón Observer, establece una relación de dependencia uno a muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente. Es útil para implementar la propagación de cambios en tiempo real, como en los sistemas de eventos o notificaciones.

Ejemplo:

import java.util.ArrayList;
import java.util.List;

interface Observer {
    void update(String message);
}

class MessagePublisher {
    private List<Observer> observers = new ArrayList<>();
    
    public void attach(Observer observer) {
        observers.add(observer);
    }
    
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

class MessageSubscriber implements Observer {
    private String name;
    
    public MessageSubscriber(String name) {
        this.name = name;
    }
    
    public void update(String message) {
        System.out.println(name + " received message: " + message);
    }
}

En este ejemplo, MessagePublisher es el sujeto que envía mensajes a los observadores (MessageSubscriber). Los observadores se registran con el sujeto y se notifican automáticamente cuando hay un nuevo mensaje.

  • Strategy: Este patrón define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables. Permite que el algoritmo varíe independientemente de los clientes que lo utilizan. Es útil cuando se necesita cambiar dinámicamente el comportamiento de un objeto en tiempo de ejecución.

Ejemplo:

interface PaymentStrategy {
    void pay(int amount);
}

class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String cvv;
    private String expiryDate;
    
    public CreditCardPayment(String cardNumber, String cvv, String expiryDate) {
        this.cardNumber = cardNumber;
        this.cvv = cvv;
        this.expiryDate = expiryDate;
    }
    
    public void pay(int amount) {
        System.out.println(amount + " paid with credit/debit card");
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;
    private String password;
    
    public PayPalPayment(String email, String password) {
        this.email = email;
        this.password = password;
    }
    
    public void pay(int amount) {
        System.out.println(amount + " paid using PayPal");
    }
}

class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
    
    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

En este ejemplo, ShoppingCart utiliza una estrategia de pago (PaymentStrategy) para realizar pagos. Dependiendo del método de pago seleccionado por el cliente, se utiliza una estrategia de pago específica, ya sea mediante tarjeta de crédito o PayPal.

  • Composite: El patrón Composite se utiliza para tratar objetos compuestos y simples de la misma manera. Define una estructura de árbol donde los nodos individuales y las composiciones de nodos se tratan uniformemente. Es útil para representar jerarquías de objetos, como estructuras de árbol o menús.

Ejemplo:

import java.util.ArrayList;
import java.util.List;

// Componente
interface Component {
    void operation();
}

// Hoja
class Leaf implements Component {
    private String name;
    
    public Leaf(String name) {
        this.name = name;
    }
    
    public void operation() {
        System.out.println("Leaf " + name + " operation");
    }
}

// Compuesto
class Composite implements Component {
    private List<Component> components = new ArrayList<>();
    
    public void addComponent(Component component) {
        components.add(component);
    }
    
    public void removeComponent(Component component) {
        components.remove(component);
    }
    
    public void operation() {
        for (Component component : components) {
            component.operation();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // Crear componentes
        Component leaf1 = new Leaf("Leaf 1");
        Component leaf2 = new Leaf("Leaf 2");
        Component leaf3 = new Leaf("Leaf 3");
        
        // Crear composite
        Composite composite = new Composite();
        composite.addComponent(leaf1);
        composite.addComponent(leaf2);
        
        // Agregar hojas al composite
        Composite composite2 = new Composite();
        composite2.addComponent(leaf3);
        composite.addComponent(composite2);
        
        // Llamar a la operación en el composite
        composite.operation();
    }
}

En este ejemplo, tenemos tres clases:

  • Component: Define la interfaz para los objetos en la composición.
  • Leaf: Representa las hojas de la estructura, es decir, los objetos finales que no tienen hijos.
  • Composite: Representa los componentes que tienen hijos, es decir, la estructura compuesta por otros componentes.

En el método main, creamos un árbol de componentes donde un Composite puede contener otros componentes (ya sean Leaf o Composite). Al llamar al método operation() en el Composite raíz, se invocará recursivamente el método operation() en todos los componentes contenidos, permitiendo que la operación se propague a través de la estructura compuesta.

Al comprender y aplicar estos patrones, los desarrolladores pueden mejorar la modularidad, la flexibilidad y la reutilización del código en sus proyectos de desarrollo de software.

Deja un comentario