• Home
  • Java
  • Dominando los Patrones de Diseño en Java
3.0 / 5

Dominando los Patrones de Diseño en Java

0
3

En el mundo de la ingeniería de software, convertir ideas en código funcional puede ser un desafío.

Como desarrolladores, nuestro objetivo no es solo hacer que las cosas funcionen, sino también asegurarnos de que nuestro código sea mantenible, escalable, adaptable y reutilizable.

Aquí entran en juego los patrones de diseño: los planos probados que nos permiten abordar problemas de diseño recurrentes con elegancia y eficiencia.

En esencia, un patrón de diseño es como una solución prefabricada para problemas comunes que enfrentamos al diseñar software. Estas soluciones actúan como atajos, ahorrándonos tiempo y esfuerzo mediante el uso de estrategias probadas que los expertos han perfeccionado a lo largo de los años.

En este artículo, profundizaremos en algunos de los patrones de diseño más importantes que todo desarrollador debería conocer. Exploraremos sus principios, por qué son útiles y cómo puedes usarlos en proyectos reales. Ya sea que estés lidiando con la creación de objetos, organizando relaciones entre clases o gestionando cómo se comportan los objetos, hay un patrón de diseño que puede ayudarte.

¡Vamos a empezar!

1. Patrón Singleton

El patrón Singleton es un patrón de diseño creacional que asegura que una clase solo tenga una instancia y proporciona un punto de acceso global a esa instancia. En términos más simples, es como asegurar que solo haya una copia única de un objeto particular en tu programa, y puedes acceder a ese objeto desde cualquier parte de tu código.

Tomemos un ejemplo del mundo real: el portapapeles. Imagina múltiples aplicaciones o procesos ejecutándose en una computadora, cada uno intentando acceder al portapapeles simultáneamente. Si cada aplicación creara su propia versión del portapapeles para gestionar las operaciones de copiar y pegar, podría conducir a datos conflictivos.

public class Clipboard {
    private String value;

    public void copy(String value) {
        this.value = value;
    }

    public String paste() {
        return value;
    }
}Code language: JavaScript (javascript)

En el ejemplo anterior, hemos definido una clase Clipboard capaz de copiar y pegar valores. Sin embargo, si creáramos múltiples instancias de Clipboard, cada instancia tendría sus propios datos separados.

public class Main {
    public static void main(String[] args) {
        Clipboard clipboard1 = new Clipboard();
        Clipboard clipboard2 = new Clipboard();

        clipboard1.copy("Java");
        clipboard2.copy("Patrones de diseño");

        System.out.println(clipboard1.paste()); // salida: Java
        System.out.println(clipboard2.paste()); // salida: Patrones de diseño
    }
}Code language: JavaScript (javascript)

Claramente, esto no es ideal. Esperamos que ambas instancias del portapapeles muestren el mismo valor. Aquí es donde el patrón Singleton demuestra su valía.

public class Clipboard {
    private String value;
    private static Clipboard clipboard = null;

    // Constructor privado para evitar la instanciación desde fuera
    private Clipboard() {}

    // Método para proporcionar acceso a la instancia singleton
    public static Clipboard getInstance() {
        if (clipboard == null) {
            clipboard = new Clipboard();
        }
        return clipboard;
    }

    public void copy(String value) {
        this.value = value;
    }

    public String paste() {
        return value;
    }
}Code language: PHP (php)

Al implementar el patrón Singleton, aseguramos que solo exista una instancia de la clase Clipboard durante la ejecución del programa.

public class Main {
    public static void main(String[] args) {
        Clipboard clipboard1 = Clipboard.getInstance();
        Clipboard clipboard2 = Clipboard.getInstance();

        clipboard1.copy("Java");
        clipboard2.copy("Patrones de diseño");

        System.out.println(clipboard1.paste()); // salida: Patrones de diseño
        System.out.println(clipboard2.paste()); // salida: Patrones de diseño
    }
}Code language: JavaScript (javascript)

Ahora, tanto clipboard1 como clipboard2 hacen referencia a la misma instancia de la clase Clipboard, asegurando la consistencia en toda la aplicación.

2. Patrón de Diseño Factory

El Patrón de Diseño Factory es un patrón de diseño creacional que proporciona una interfaz para crear objetos en una clase superclase, pero permite a las subclases decidir qué clase instanciar. En otras palabras, proporciona una forma de delegar la lógica de instanciación a las clases hijas.

Imagina que estás construyendo un programa que simula una calculadora simple basada en consola. Tienes diferentes tipos de operaciones como suma, resta, multiplicación, división, etc. Cada operación tiene su propio comportamiento único. Ahora, deseas crear estos objetos de operación en tu programa basándote en la elección del usuario.

El desafío es que necesitas una forma de crear estos objetos de operación sin hacer que tu código sea demasiado complejo o esté fuertemente acoplado. Esto significa que no quieres que tu código dependa demasiado de las clases específicas de las operaciones directamente. También deseas facilitar la adición de nuevos tipos de operaciones más adelante sin cambiar mucho el código.

El Patrón de Diseño Factory te ayuda a resolver este problema proporcionando una forma de crear objetos sin especificar su clase exacta. En su lugar, delegas el proceso de creación a una clase factory.

Definir la interfaz del producto (Operation)

public interface Operation {
    double calculate(double number1, double number2);
}Code language: PHP (php)

Implementar productos concretos para cada operación

// Para la suma
public class AddOperation implements Operation {
    @Override
    public double calculate(double number1, double number2) {
        return number1 + number2;
    }
}

// Para la resta
public class SubOperation implements Operation {
    @Override
    public double calculate(double number1, double number2) {
        return number1 - number2;
    }
}

// Para la multiplicación
public class MulOperation implements Operation {
    @Override
    public double calculate(double number1, double number2) {
        return number1 * number2;
    }
}

// Para la división
public class DivOperation implements Operation {
    @Override
    public double calculate(double number1, double number2) {
        if (number2 == 0) throw new ArithmeticException("¡No se puede dividir por cero!");
        return number1 / number2;
    }
}

// Clase de excepción que se invoca cuando el usuario ingresa una elección inválida para la operación
public class InvalidOperationException extends Exception {
    public InvalidOperationException(String message) {
        super(message);
    }
}Code language: PHP (php)

Crear una clase de fábrica (OperationFactory) con un método (getInstance) para crear objetos basados en algún parámetro

public interface OperationFactory {
    Operation getInstance(int choice) throws InvalidOperationException;
}

public class OperationFactoryImpl implements OperationFactory {
    @Override
    public Operation getInstance(int choice) throws InvalidOperationException {
        if (choice == 1) return new AddOperation();
        else if (choice == 2) return new SubOperation();
        else if (choice == 3) return new MulOperation();
        else if (choice == 4) return new DivOperation();
        throw new InvalidOperationException("¡Operación seleccionada inválida!");
    }
}Code language: PHP (php)

Usar factory para crear objetos sin conocer sus clases específicas

public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);

    try {
        System.out.println("\n1. Suma(+)\n2. Resta(-)\n3. Multiplicación(*)\n4. División(/)");

        // Obteniendo la elección del usuario
        System.out.println("\n\nSelecciona tu operación (1-4): ");
        int choice = scan.nextInt();

        // Obteniendo 2 operandos del usuario
        System.out.println("Ingresa el primer operando: ");
        double operand1 = scan.nextDouble();
        System.out.println("Ingresa el segundo operando: ");
        double operand2 = scan.nextDouble();

        // Crear instancia de operación basada en la elección del usuario
        OperationFactory operationFactory = new OperationFactoryImpl();
        Operation operation = operationFactory.getInstance(choice);

        // Imprimiendo el resultado
        System.out.println("\nEl resultado es " + operation.calculate(operand1, operand2) + ".");
    } catch (InputMismatchException e) {
        System.out.println("¡Tipo de entrada inválido!\n");
    } catch (InvalidOperationException | ArithmeticException e) {
        System.out.println(e.getMessage());
    }

    scan.close();
}Code language: JavaScript (javascript)

Aquí, la clase Main demuestra el uso de la fábrica para crear diferentes objetos de operación sin conocer sus clases de implementación específicas (acoplamiento suelto). Solo interactúa con la interfaz de la fábrica. No solo eso, sino que también podemos agregar fácilmente nuevos tipos de operaciones sin cambiar el código del cliente existente. Solo necesitamos crear un nuevo producto concreto y actualizar la fábrica si es necesario.

3. Patrón Builder

El Patrón Builder proporciona una forma de construir un objeto permitiéndote establecer sus diversas propiedades (o atributos) de manera paso a paso.

Algunos de los parámetros pueden ser opcionales para un objeto, pero estamos obligados a enviar todos los parámetros y los parámetros opcionales deben enviarse como NULL. Podemos resolver este problema con un gran número de parámetros proporcionando un constructor con parámetros requeridos y luego diferentes métodos setter para establecer los parámetros opcionales.

Este patrón es particularmente útil cuando se trata de objetos que tienen muchos parámetros opcionales o configuraciones.

Imagina que estamos desarrollando una entidad de usuario. Los usuarios

tienen atributos como id, nombre, email, edad y dirección, algunos de los cuales pueden ser opcionales. Usar constructores para establecer todos estos atributos puede volverse confuso y difícil de manejar a medida que crece el número de parámetros. Aquí es donde el Patrón Builder entra en juego, permitiéndonos construir el objeto de una manera más legible y manejable.

Paso 1: Definir la clase User y su constructor privado

public class User {
    private int id;
    private String name;
    private String email;
    private int age;
    private String address;

    // Constructor privado para la clase User
    private User(UserBuilder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.email = builder.email;
        this.age = builder.age;
        this.address = builder.address;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", name=" + name + ", email=" + email + ", age=" + age + ", address=" + address + "]";
    }

    // Clase estática interna UserBuilder
    public static class UserBuilder {
        private int id;
        private String name;
        private String email;
        private int age;
        private String address;

        // Métodos de construcción para UserBuilder
        public UserBuilder setId(int id) {
            this.id = id;
            return this;
        }

        public UserBuilder setName(String name) {
            this.name = name;
            return this;
        }

        public UserBuilder setEmail(String email) {
            this.email = email;
            return this;
        }

        public UserBuilder setAge(int age) {
            this.age = age;
            return this;
        }

        public UserBuilder setAddress(String address) {
            this.address = address;
            return this;
        }

        // Método de construcción final para crear el objeto User
        public User build() {
            return new User(this);
        }
    }
}Code language: JavaScript (javascript)

Paso 2: Crear un objeto User utilizando el patrón Builder

public static void main(String[] args) {
    // Crear un objeto User utilizando el patrón Builder
    User user1 = new User.UserBuilder()
        .setId(1)
        .setName("John Doe")
        .setEmail("john.doe@example.com")
        .setAge(30)
        .setAddress("123 Main St")
        .build();

    User user2 = new User.UserBuilder()
        .setId(2)
        .setName("Jane Doe")
        .build(); // Solo se configuran id y nombre, los demás atributos son opcionales

    // Imprimir los detalles del usuario
    System.out.println(user1);
    System.out.println(user2);
}Code language: JavaScript (javascript)

Aquí, hemos usado el patrón Builder para construir objetos User. Esto nos permite establecer solo los atributos que queremos, y podemos omitir aquellos que no son necesarios. El resultado es un código más limpio y legible.

4. Patrón Adapter

El patrón Adapter es un patrón de diseño estructural que permite que objetos con interfaces incompatibles trabajen juntos. Actúa como un puente entre dos interfaces incompatibles.

Imagina una situación donde dos clases o componentes realizan tareas similares pero tienen nombres de métodos, tipos de parámetros o estructuras diferentes. El patrón Adapter permite que estas interfaces incompatibles trabajen juntas proporcionando un envoltorio (el adaptador) que traduce la interfaz de una clase en una interfaz que el cliente espera.

  1. Target: es la interfaz que espera el cliente.
  2. Adaptee: es la clase que necesita ser adaptada.
  3. Adapter: es la clase que implementa la interfaz Target y envuelve la clase Adaptee.
  4. Cliente: es la clase que utiliza el adaptador para interactuar con Adaptee a través de la interfaz Target.

Ejemplo del patrón Adapter en Java

// Interfaz Target
interface CellPhone {
    void call();
}

// Adaptee (la clase que se va a adaptar)
class FriendCellPhone {
    public void ring() {
        System.out.println("Ringing");
    }
}

// Clase Adapter que implementa la interfaz Target
class CellPhoneAdapter implements CellPhone {
    private FriendCellPhone friendCellPhone;

    public CellPhoneAdapter(FriendCellPhone friendCellPhone) {
        this.friendCellPhone = friendCellPhone;
    }

    @Override
    public void call() {
        friendCellPhone.ring();
    }
}

// Clase Cliente
public class AdapterMain {
    public static void main(String[] args) {
        // Usando el adaptador para hacer que Adaptee funcione con la interfaz Target
        FriendCellPhone adaptee = new FriendCellPhone();
        CellPhone adapter = new CellPhoneAdapter(adaptee);
        adapter.call();
    }
}Code language: PHP (php)

En este ejemplo:

  • CellPhone es la interfaz Target que espera tu código cliente y no tienes una implementación de ella.
  • FriendCellPhone es la clase que quieres adaptar/reutilizar (el Adaptee), que tiene un método llamado ring en lugar de crear una nueva implementación de la interfaz CellPhone.
  • CellPhoneAdapter es la clase adaptadora que implementa la interfaz CellPhone y envuelve una instancia de FriendCellPhone. El método call en el adaptador delega la llamada al método ring de la clase FriendCellPhone.
  • AdapterMain es la clase cliente que demuestra el uso del patrón Adapter en acción.

¿Por qué usar el patrón Adapter?

  1. Adaptación de clases existentes: El Adaptee puede ser una clase de una biblioteca de terceros o un código heredado que no puedes modificar directamente. Usando un adaptador, puedes adaptar su interfaz para que coincida con la interfaz esperada por el cliente sin modificar el código original.
  2. Funcionalidad específica: El cliente puede requerir solo una funcionalidad específica del Adaptee. Usando un adaptador, puedes proporcionar una interfaz a medida que expone solo la funcionalidad necesaria, en lugar de exponer toda la interfaz del Adaptee.
  3. Reutilización y flexibilidad: Aunque puedes lograr una funcionalidad similar creando una instancia de la interfaz Target directamente, usar un adaptador proporciona beneficios en términos de reutilización de código, mantenibilidad y flexibilidad, especialmente al trabajar con código existente o bibliotecas de terceros.

5. Patrón Decorator

El patrón Decorator es un patrón de diseño en programación orientada a objetos que permite añadir comportamiento a objetos individuales, ya sea estáticamente o dinámicamente, sin afectar el comportamiento de otros objetos de la misma clase.

En este patrón, hay una clase base (o interfaz) que define la funcionalidad común y una o más clases decoradoras que añaden comportamiento adicional. Estas clases decoradoras envuelven el objeto original, aumentando su comportamiento de una manera modular y flexible.

Ejemplo del patrón Decorator en Java

// Interfaz Shape
interface Shape {
    void draw();
    String getName();
}

// Shape concreto: Circle
class Circle implements Shape {
    private String name;

    public Circle(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public void draw() {
        System.out.println("Drawing circle, " + getName() + ".");
    }
}

// Clase Decorator abstracta
abstract class ShapeDecorator implements Shape {
    private Shape decoratedShape;

    public ShapeDecorator(Shape decoratedShape) {
        this.decoratedShape = decoratedShape;
    }

    @Override
    public void draw() {
        decoratedShape.draw();
    }

    @Override
    public String getName() {
        return decoratedShape.getName();
    }
}

// Decorator concreto: BorderDecorator
class BorderDecorator extends ShapeDecorator {
    private String color;
    private int widthInPxs;

    public BorderDecorator(Shape decoratedShape, String color, int widthInPxs) {
        super(decoratedShape);
        this.color = color;
        this.widthInPxs = widthInPxs;
    }

    @Override
    public void draw() {
        super.draw();
        System.out.println("Adding " + widthInPxs + "px, " + color + " color border to " + getName() + ".");
    }
}

// Decorator concreto: ColorDecorator
class ColorDecorator extends ShapeDecorator {
    private String color;

    public ColorDecorator(Shape decoratedShape, String color) {
        super(decoratedShape);
        this.color = color;
    }

    @Override
    public void draw() {
        super.draw();
        System.out.println("Filling with " + color + " color to " + getName() + ".");
    }
}

// Clase Principal
public class DecoratorMain {
    public static void main(String[] args) {
        // Crear un círculo
        Shape circle1 = new Circle("circle1");

        // Decorar el círculo con un borde
        Shape circle1WithBorder = new BorderDecorator(circle1, "red", 2);

        // Decorar el círculo con un color
        Shape circle1WithBorderAndColor = new ColorDecorator(circle1WithBorder, "blue");

        // Dibujar el círculo decorado
        circle1WithBorderAndColor.draw();
    }
}Code language: JavaScript (javascript)

Con la implementación del Patrón Decorator, nuestra aplicación de dibujo gana la notable capacidad de embellecer no solo círculos, sino también una plétora de formas geométricas como rectángulos, triángulos y más. Además, la extensibilidad de este patrón nos permite integrar decoradores adicionales de manera fluida, ofreciendo características como transparencia, diversos estilos de borde (sólido, punteado) y mucho más. Esta capacidad de mejora dinámica, lograda sin alterar la estructura central de las formas, subraya el poder del patrón para promover la reutilización de código, flexibilidad y escalabilidad.

6. Patrón Observer

El Patrón Observer es un patrón de diseño de comportamiento comúnmente utilizado en la programación orientada a objetos para establecer una dependencia de uno a muchos entre objetos. En este patrón, un objeto (llamado sujeto u observable) mantiene una lista de sus dependientes (observadores) y les notifica cualquier cambio de estado, generalmente llamando a uno de sus métodos.

Ejemplo del patrón Observer en Java

public enum EventType {
    NEW_VIDEO,
    LIVE_STREAM
}

public class YoutubeEvent {
    private EventType eventType;
    private String topic;

    public YoutubeEvent(EventType eventType, String topic) {
        this.eventType = eventType;
        this.topic = topic;
    }

    // getters y setters

    @Override
    public String toString() {
        return eventType.name() + " on " + topic;
    }
}

public interface Subject {
    void addSubscriber(Observer observer);
    void removeSubscriber(Observer observer);
    void notifyAllSubscribers(YoutubeEvent event);
}

public interface Observer {
    void notifyMe(String youtubeChannelName, YoutubeEvent event);
}

package observer;

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

public class YoutubeChannel implements Subject {

    private String name;
    private List<Observer> subscribers = new ArrayList<>();

    public YoutubeChannel(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public void addSubscriber(Observer observer) {
        subscribers.add(observer);
    }

    @Override
    public void removeSubscriber(Observer observer) {
        subscribers.remove(observer);
    }

    @Override
    public void notifyAllSubscribers(YoutubeEvent event) {
        for (Observer observer : subscribers) {
            observer.notifyMe(getName(), event);
        }
    }
}

package observer;

public class YoutubeSubscriber implements Observer {
    private String name;

    public YoutubeSubscriber(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void notifyMe(String youtubeChannelName, YoutubeEvent event) {
        System.out.println("Dear " + getName() + ", Notification from " + youtubeChannelName + ": " + event);
    }
}

public class ObserverMain {
    public static void main(String[] args) throws InterruptedException {
        YoutubeChannel myChannel = new YoutubeChannel("MyChannel");

        Observer john = new YoutubeSubscriber("John");
        Observer bob = new YoutubeSubscriber("Bob");
        Observer tom = new YoutubeSubscriber("Tom");

        myChannel.addSubscriber(john);
        myChannel.addSubscriber(bob);
        myChannel.addSubscriber(tom);

        myChannel.notifyAllSubscribers(new YoutubeEvent(EventType.NEW_VIDEO, "Design patterns"));
        myChannel.removeSubscriber(tom);


 System.out.println();
        Thread.sleep(5000);
        myChannel.notifyAllSubscribers(new YoutubeEvent(EventType.LIVE_STREAM, "JAVA for beginners"));
    }
}Code language: PHP (php)

Salida:

Dear John, Notification from MyChannel: NEW_VIDEO on Design patterns
Dear Bob, Notification from MyChannel: NEW_VIDEO on Design patterns
Dear Tom, Notification from MyChannel: NEW_VIDEO on Design patterns

Dear John, Notification from MyChannel: LIVE_STREAM on JAVA for beginners
Dear Bob, Notification from MyChannel: LIVE_STREAM on JAVA for beginnersCode language: JavaScript (javascript)

Usando el patrón de diseño Observer, el canal de YouTube puede notificar fácilmente a todos sus suscriptores cada vez que se sube un nuevo video sin acoplar estrechamente el canal y sus suscriptores. Esto promueve un diseño más flexible y mantenible.

En conclusión, los patrones de diseño son herramientas indispensables para los desarrolladores de Java, ofreciendo soluciones probadas a problemas recurrentes de diseño y promoviendo la reutilización del código, mantenibilidad y escalabilidad. Al comprender e implementar estos patrones de manera efectiva, los desarrolladores pueden crear soluciones de software robustas, flexibles y fácilmente mantenibles. Si bien dominar los patrones de diseño requiere práctica y experiencia, los beneficios que aportan al desarrollo de software son invaluables. Ya sea que estés trabajando en un proyecto pequeño o en una aplicación empresarial a gran escala, aprovechar los patrones de diseño te permite escribir código más limpio y eficiente y, en última instancia, convertirte en un desarrollador de Java más competente.


Espero que hayas encontrado la información útil y hayas obtenido algunas ideas valiosas sobre el tema. Desde entender qué son los patrones de diseño hasta explorar ejemplos del mundo real, hemos cubierto mucho.

¡Sigue aprendiendo, explorando y creando cosas increíbles con JAVA!

¡Feliz programación!

THIS IS AN OPTIONAL

Related Post

LEAVE YOUR COMMENTS