Ir al contenido principal

Java Tip 32: Patrones de diseño en Java

En está ocasión veremos qué son los patrones de diseño.

Según la página de Refactoring Guru, los patrones de diseño (design patterns) son:

...soluciones habituales a problemas comunes en el diseño de software. Cada patrón es como un plano que se puede personalizar para resolver un problema de diseño particular de tu código.

Patrones de diseño en Java

Observemos la siguiente imagen:

Podemos ver tres tipos y sus divisiones.

  1. Patrón Creacional:  proporcionan varios mecanismos de creación de objetos que incrementan la flexibilidad y la reutilización del código existente.
  2. Patrón Estructural:  estructurales explican cómo ensamblar objetos y clases en estructuras más grandes, a la vez que se mantiene la flexibilidad y eficiencia de estas estructuras.
  3. De comportamiento: los que tratan con algoritmos y la asignación de responsabilidades entre objetos.

Existen más patrones, pero solo nos enfocaremos en estos y sus divisiones.

Patrón creacional

Se enfoca en cómo se crean los objetos en un sistema. Su objetivo es controlar y optimizar la instanciación, haciendo el código más flexible y reutilizable.

Abstract factory

Crea familias de objetos relacionados.

interface Boton {
    void render();
}

class BotonWindows implements Boton {
    public void render() {
        System.out.println("Botón estilo Windows");
    }
}

class BotonMac implements Boton {
    public void render() {
        System.out.println("Botón estilo Mac");
    }
}

interface GUIFactory {
    Boton crearBoton();
}

class WindowsFactory implements GUIFactory {
    public Boton crearBoton() {
        return new BotonWindows();
    }
}

class MacFactory implements GUIFactory {
    public Boton crearBoton() {
        return new BotonMac();
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        GUIFactory factory = new WindowsFactory();
        Boton boton = factory.crearBoton();
        boton.render();
    }
}

Builder

Construye objetos paso a paso.

class Casa {
    private String partes = "";

    public void agregar(String parte) {
        partes += parte + " ";
    }

    public void mostrar() {
        System.out.println(partes);
    }
}

class ConstructorCasa {
    private Casa casa = new Casa();

    public void construirPuerta() {
        casa.agregar("Puerta");
    }

    public void construirVentana() {
        casa.agregar("Ventana");
    }

    public Casa obtenerCasa() {
        return casa;
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        ConstructorCasa builder = new ConstructorCasa();
        builder.construirPuerta();
        builder.construirVentana();
        builder.obtenerCasa().mostrar();
    }
}

Factory method

Deja que las subclases decidan qué crear.

interface Transporte {
    void entregar();
}

class Camion implements Transporte {
    public void entregar() {
        System.out.println("Entrega por carretera");
    }
}

class Barco implements Transporte {
    public void entregar() {
        System.out.println("Entrega por mar");
    }
}

abstract class Logistica {
    abstract Transporte crearTransporte();
}

class LogisticaTerrestre extends Logistica {
    Transporte crearTransporte() {
        return new Camion();
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        Logistica logistica = new LogisticaTerrestre();
        Transporte t = logistica.crearTransporte();
        t.entregar();
    }
}

Prototype

Clona objetos existentes.

class Persona implements Cloneable {
    String nombre;
    int edad;

    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }

    public Persona clone() {
        try {
            return (Persona) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        Persona original = new Persona("Ana", 25);
        Persona copia = original.clone();

        System.out.println(copia.nombre + " " + copia.edad);
    }
}

Singleton

Crea una sola instancia.

class Singleton {
    private static Singleton instancia;

    private Singleton() {}

    public static Singleton getInstancia() {
        if (instancia == null) {
            instancia = new Singleton();
        }
        return instancia;
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        Singleton obj1 = Singleton.getInstancia();
        Singleton obj2 = Singleton.getInstancia();

        System.out.println(obj1 == obj2); // true
    }
}

Resumiendo:

  • Abstract Factor: crea familias de objetos. 
  • Builder: construcción paso a paso. 
  • Factory Method: delega la creación. 
  • Prototype: clonación. 
  • Singleton: única instancia.

Patrón Estructural

Se centra en cómo se organizan y relacionan las clases y objetos para formar estructuras más grandes. Busca simplificar las relaciones y mejorar la composición del sistema.

Adapter

Permite que interfaces incompatibles trabajen juntas. Actúa como un “traductor” entre clases.

// Interfaz esperada
interface EnchufeEuropeo {
    void conectar();
}

// Clase incompatible
class EnchufeAmericano {
    public void connect() {
        System.out.println("Conectado al enchufe americano");
    }
}

// Adaptador
class Adaptador implements EnchufeEuropeo {
    private EnchufeAmericano americano;

    public Adaptador(EnchufeAmericano americano) {
        this.americano = americano;
    }

    public void conectar() {
        americano.connect(); // Traduce la llamada
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        EnchufeAmericano americano = new EnchufeAmericano();
        EnchufeEuropeo adaptado = new Adaptador(americano);

        adaptado.conectar();
    }
}

Decorator

Permite agregar funcionalidades a un objeto dinámicamente sin modificar su código.

// Interfaz base
interface Cafe {
    String descripcion();
}

// Implementación base
class CafeSimple implements Cafe {
    public String descripcion() {
        return "Café simple";
    }
}

// Decorador base
abstract class CafeDecorator implements Cafe {
    protected Cafe cafe;

    public CafeDecorator(Cafe cafe) {
        this.cafe = cafe;
    }
}

// Decorador concreto
class Leche extends CafeDecorator {
    public Leche(Cafe cafe) {
        super(cafe);
    }

    public String descripcion() {
        return cafe.descripcion() + " con leche";
    }
}

// Otro decorador
class Azucar extends CafeDecorator {
    public Azucar(Cafe cafe) {
        super(cafe);
    }

    public String descripcion() {
        return cafe.descripcion() + " con azúcar";
    }
}

// Uso
public class Main {
    public static void main(String[] args) {
        Cafe cafe = new CafeSimple();
        cafe = new Leche(cafe);
        cafe = new Azucar(cafe);

        System.out.println(cafe.descripcion());
    }
}

Resumiendo:

  • Adapter: hace compatibles interfaces distintas.
  • Decorator: añade funcionalidades sin modificar la clase original.

De comportamiento

Se ocupa de cómo interactúan y se comunican los objetos entre sí. Ayuda a definir responsabilidades y flujos de comunicación claros.

Value object

Objeto inmutable que representa un valor, no una identidad. Se compara por contenido, no por referencia.

class Dinero {class Dinero;

    public Dinero(double cantidad) {
        this.cantidad = cantidad;
    }

    public double getCantidad() {
        return cantidad;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Dinero)) return false;
        Dinero otro = (Dinero) obj;
        return Double.compare(otro.cantidad, cantidad) == 0;
    }
}

// Uso
Dinero d1 = new Dinero(100);
Dinero d2 = new Dinero(100);
System.out.println(d1.equals(d2)); // true

Null object

Evita null usando un objeto con comportamiento “vacío”.

interface Cliente {
    String getNombre();
}

class ClienteReal implements Cliente {
    private String nombre;

    public ClienteReal(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }
}

class ClienteNulo implements Cliente {
    public String getNombre() {
        return "Cliente desconocido";
    }
}

// Uso
Cliente cliente = new ClienteNulo();
System.out.println(cliente.getNombre());

Strategy

Permite cambiar el algoritmo en tiempo de ejecución.

interface PagoStrategy {
    void pagar(int cantidad);
}

class PagoTarjeta implements PagoStrategy {
    public void pagar(int cantidad) {
        System.out.println("Pagado con tarjeta: " + cantidad);
    }
}

class PagoEfectivo implements PagoStrategy {
    public void pagar(int cantidad) {
        System.out.println("Pagado en efectivo: " + cantidad);
    }
}

class Carrito {
    private PagoStrategy estrategia;

    public void setEstrategia(PagoStrategy estrategia) {
        this.estrategia = estrategia;
    }

    public void pagar(int cantidad) {
        estrategia.pagar(cantidad);
    }
}

// Uso
Carrito c = new Carrito();
c.setEstrategia(new PagoTarjeta());
c.pagar(100);

Command

Encapsula una solicitud como un objeto.

interface Command {
    void ejecutar();
}

class Luz {
    public void encender() {
        System.out.println("Luz encendida");
    }
}

class EncenderLuzCommand implements Command {
    private Luz luz;

    public EncenderLuzCommand(Luz luz) {
        this.luz = luz;
    }

    public void ejecutar() {
        luz.encender();
    }
}

// Uso
Luz luz = new Luz();
Command comando = new EncenderLuzCommand(luz);
comando.ejecutar();

Chain responsability

Pasa una solicitud por una cadena de objetos hasta que alguien la maneje.

abstract class Handler {
    protected Handler siguiente;

    public void setSiguiente(Handler siguiente) {
        this.siguiente = siguiente;
    }

    public abstract void manejar(int nivel);
}

class SoporteBasico extends Handler {
    public void manejar(int nivel) {
        if (nivel <= 1) {
            System.out.println("Soporte básico resolvió");
        } else if (siguiente != null) {
            siguiente.manejar(nivel);
        }
    }
}

class SoporteAvanzado extends Handler {
    public void manejar(int nivel) {
        if (nivel > 1) {
            System.out.println("Soporte avanzado resolvió");
        }
    }
}

// Uso
Handler basico = new SoporteBasico();
Handler avanzado = new SoporteAvanzado();

basico.setSiguiente(avanzado);
basico.manejar(2);

Dependency injection

Las dependencias se inyectan en lugar de crearse dentro de la clase.

class Motor {
    public void encender() {
        System.out.println("Motor encendido");
    }
}

class Auto {
    private Motor motor;

    // Inyección por constructor
    public Auto(Motor motor) {
        this.motor = motor;
    }

    public void arrancar() {
        motor.encender();
    }
}

// Uso
Motor motor = new Motor();
Auto auto = new Auto(motor); // se inyecta la dependencia
auto.arrancar();

Resumen:

  • Value Object: objeto inmutable basado en valor.
  • Null Object: evita null con comportamiento vacío.
  • Strategy: intercambia algoritmos.
  • Command: encapsula acciones.
  • Chain: pasa solicitudes en cadena.
  • DI: inyecta dependencias externas

Importancia de los patrones de diseño

Ayudan a resolver problemas comunes de forma probada y reutilizable, además hacen que el código sea más limpio, flexible y profesional.

  • Mejoran la organización del código. 
  • Facilitan el mantenimiento y la escalabilidad.
  • Promueven buenas prácticas de desarrollo.
  • Permiten que otros desarrolladores entiendan más rápido el sistema.

Hemos visto los patrones de diseños más usados en Java.

Enlaces:

https://codemonkeyjunior.blogspot.com/2013/12/patrones-de-diseno.html
https://refactoring.guru/
https://martinfowler.com/articles/refactoring-2nd-ed.html



Comentarios

Entradas populares de este blog

Java Tip 1: Importaciones por default

Tip 1 : Todas las clases del paquete java.lang se importan por defecto. Podríamos hacer algo como esto: import java.lang.System ; import java.lang.String ; import java.lang.Boolean ; import java.lang.Integer ; import java.lang.Short ; import java.lang.StringBuilder ; import java.lang.StringBuffer ; O cosas (aberrantes) como: //Traernos todas las clases del paquete java.lang import java.lang.* ; Sin embargo, no es necesario. Repitamos esto como un mantra: Todas las clases del paquete  java.lang  se importan por defecto, no es necesario escribir de más .

Java Tip 28: hilos virtuales en Java

Con Java 21 entra un nuevo concepto: hilos virtuales java ( Java Virtual Threads ).  La cual es una nueva característica que nos permite crear miles o millones de hilos (tareas en paralelo). Los cuales se diferencian de los hilos comunes ( Platform Threads ) al no depender del sistema operativo , puesto que son virtuales. De estos se encargará la JVM. Al usar hilos virtuales ahorramos recursos del procesador y de memoria del sistema , sin importar si estamos creando y/o usando miles o millones. Los hilos virtuales son ideales para aplicaciones modernas donde se requiere esperar y recibir respuestas o acceder a bases de datos remotas, entre otras cosas. Además no impide usar los hilos comunes que dependen del sistema operativo. Observemos un ejemplo de uso de hilos virtuales. VirtualThreadsDemo.java package com.comunidad.demo; public class VirtualThreadsDemo { public static void main (String [] args) throws InterruptedException { // Crear un Virtual Th...

Java Tip 20: JDBC (2da parte)

Continuamos con esta serie sobre JDBC . Como dijimos la vez anterior JDBC( Java Database Connectivity ) es el estándar de conectividad de bases de datos de Java y proporciona un mecanismo para que los programas Java se conecten a las bases de datos. En este post veremos ejemplos de su uso. Requisitos: Tener nociones de Java.  Tener nociones de SQL (usaremos H2). Tener nociones de Maven. Creando una aplicación JDBC Crearemos una sencilla aplicación que se conecte a una BD H2 (ver  tutorial ). Pasos: Crearemos una tabla a la que llamaremos Cursos. La cual tendrá los siguientes campos: id, titulo, materia, instructor, fecha y hora. Crear la aplicación Java usando Maven. Crearemos instrucciones SQL para consultar la información, ingresar nuevos cursos, actualizar y eliminar registros. Ejecutar la aplicación con Maven. 1. Creamos la tabla con sus seis campos (agregaremos los datos con la aplicación Java): CREATE OR REPLACE TABLE CURSOS( id bigint ...