Los patrones de diseño son soluciones probadas a problemas recurrentes en el diseño de software orientado a objetos. No son código que copias y pegas, sino una forma de pensar la estructura de tus clases para que el código sea más flexible, reutilizable y fácil de mantener.

Se agrupan tradicionalmente en tres categorías (catálogo GoF — Gang of Four):

  • Creacionales: cómo se crean los objetos (Singleton, Factory Method, Builder...).
  • Estructurales: cómo se componen clases y objetos (Decorator, Adapter, Facade...).
  • De comportamiento: cómo interactúan y se reparten responsabilidades (Strategy, Observer, Command...).

En este artículo revisamos ocho de los patrones más usados en proyectos Java reales, cada uno con un ejemplo de código.

1. Singleton

Garantiza que una clase tenga una sola instancia en toda la aplicación y provee un punto de acceso global a ella. Se usa típicamente para configuración, conexiones a recursos compartidos o loggers.

public final class ConfiguracionApp {

    private static volatile ConfiguracionApp instancia;

    private final String entorno;

    private ConfiguracionApp() {
        this.entorno = System.getenv().getOrDefault("APP_ENV", "produccion");
    }

    public static ConfiguracionApp getInstancia() {
        if (instancia == null) {
            synchronized (ConfiguracionApp.class) {
                if (instancia == null) {
                    instancia = new ConfiguracionApp();
                }
            }
        }
        return instancia;
    }

    public String getEntorno() {
        return entorno;
    }
}

El doble chequeo (double-checked locking) con volatile evita crear instancias duplicadas en entornos multihilo sin pagar el costo de sincronizar cada llamada. Una alternativa más simple y igualmente segura en Java es usar un enum con un solo valor.

2. Factory Method

Delega la creación de objetos a subclases o métodos especializados, en lugar de instanciar directamente con new. Es útil cuando el tipo exacto de objeto a crear depende de una condición o configuración.

public interface Notificador {
    void enviar(String mensaje);
}

public class NotificadorEmail implements Notificador {
    public void enviar(String mensaje) {
        System.out.println("Email enviado: " + mensaje);
    }
}

public class NotificadorSms implements Notificador {
    public void enviar(String mensaje) {
        System.out.println("SMS enviado: " + mensaje);
    }
}

public class NotificadorFactory {
    public static Notificador crear(String tipo) {
        return switch (tipo) {
            case "email" -> new NotificadorEmail();
            case "sms" -> new NotificadorSms();
            default -> throw new IllegalArgumentException("Tipo no soportado: " + tipo);
        };
    }
}

El código que consume Notificador nunca necesita saber qué clase concreta se está usando, lo que facilita agregar nuevos tipos de notificación sin tocar el código existente (principio abierto/cerrado).

3. Builder

Permite construir objetos complejos paso a paso, evitando constructores con demasiados parámetros (el clásico "constructor telescópico"). Es ideal para objetos inmutables con muchos campos opcionales.

public final class Pedido {
    private final String cliente;
    private final String producto;
    private final int cantidad;
    private final boolean urgente;

    private Pedido(Builder builder) {
        this.cliente = builder.cliente;
        this.producto = builder.producto;
        this.cantidad = builder.cantidad;
        this.urgente = builder.urgente;
    }

    public static class Builder {
        private final String cliente;
        private final String producto;
        private int cantidad = 1;
        private boolean urgente = false;

        public Builder(String cliente, String producto) {
            this.cliente = cliente;
            this.producto = producto;
        }

        public Builder cantidad(int cantidad) {
            this.cantidad = cantidad;
            return this;
        }

        public Builder urgente(boolean urgente) {
            this.urgente = urgente;
            return this;
        }

        public Pedido build() {
            return new Pedido(this);
        }
    }
}

Uso:

Pedido pedido = new Pedido.Builder("Max", "Teclado mecánico")
        .cantidad(2)
        .urgente(true)
        .build();

4. Observer

Define una dependencia de uno-a-muchos entre objetos: cuando el "sujeto" cambia de estado, notifica automáticamente a todos sus "observadores". Es la base de los sistemas de eventos y de frameworks como Spring (ApplicationEvent).

public interface Observador {
    void onCambioPrecio(double nuevoPrecio);
}

public class Accion {
    private final List<Observador> observadores = new ArrayList<>();
    private double precio;

    public void agregarObservador(Observador observador) {
        observadores.add(observador);
    }

    public void actualizarPrecio(double nuevoPrecio) {
        this.precio = nuevoPrecio;
        for (Observador observador : observadores) {
            observador.onCambioPrecio(nuevoPrecio);
        }
    }
}

Cualquier clase que implemente Observador (una alerta, un dashboard, un log) puede suscribirse sin que Accion necesite conocer sus detalles internos.

5. Strategy

Encapsula distintos algoritmos intercambiables detrás de una misma interfaz, permitiendo cambiar el comportamiento de un objeto en tiempo de ejecución.

public interface EstrategiaDescuento {
    double aplicar(double montoOriginal);
}

public class SinDescuento implements EstrategiaDescuento {
    public double aplicar(double montoOriginal) {
        return montoOriginal;
    }
}

public class DescuentoPorcentual implements EstrategiaDescuento {
    private final double porcentaje;

    public DescuentoPorcentual(double porcentaje) {
        this.porcentaje = porcentaje;
    }

    public double aplicar(double montoOriginal) {
        return montoOriginal * (1 - porcentaje / 100);
    }
}

public class Carrito {
    private EstrategiaDescuento estrategia = new SinDescuento();

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

    public double total(double montoOriginal) {
        return estrategia.aplicar(montoOriginal);
    }
}

En vez de un enorme if/else con todos los tipos de descuento, cada regla vive en su propia clase, fácil de testear por separado.

6. Decorator

Agrega responsabilidades a un objeto dinámicamente, envolviéndolo en otro objeto con la misma interfaz, sin modificar su clase original. Es la alternativa flexible a la herencia cuando se necesitan combinar comportamientos.

public interface Cafe {
    double costo();
    String descripcion();
}

public class CafeBase implements Cafe {
    public double costo() { return 1000; }
    public String descripcion() { return "Café"; }
}

public abstract class DecoradorCafe implements Cafe {
    protected final Cafe cafe;

    protected DecoradorCafe(Cafe cafe) {
        this.cafe = cafe;
    }
}

public class ConLeche extends DecoradorCafe {
    public ConLeche(Cafe cafe) { super(cafe); }
    public double costo() { return cafe.costo() + 300; }
    public String descripcion() { return cafe.descripcion() + " con leche"; }
}

public class ConChocolate extends DecoradorCafe {
    public ConChocolate(Cafe cafe) { super(cafe); }
    public double costo() { return cafe.costo() + 500; }
    public String descripcion() { return cafe.descripcion() + " con chocolate"; }
}
Cafe pedido = new ConChocolate(new ConLeche(new CafeBase()));
System.out.println(pedido.descripcion() + ": $" + pedido.costo());
// Café con leche con chocolate: $1800

7. Adapter

Convierte la interfaz de una clase existente en otra interfaz que el código cliente espera, sin tocar la clase original. Es muy común al integrar librerías de terceros o APIs legadas que no hablan "el idioma" de tu aplicación.

// Interfaz que espera nuestra aplicación
public interface LectorPago {
    void procesar(double monto);
}

// API externa ya existente, con una firma distinta que no podemos modificar
public class PasarelaExterna {
    public void cobrar(int montoEnCentavos) {
        System.out.println("Cobrando " + montoEnCentavos + " centavos vía pasarela externa");
    }
}

// Adaptador que traduce entre ambas interfaces
public class AdaptadorPasarelaExterna implements LectorPago {
    private final PasarelaExterna pasarela;

    public AdaptadorPasarelaExterna(PasarelaExterna pasarela) {
        this.pasarela = pasarela;
    }

    public void procesar(double monto) {
        int centavos = (int) Math.round(monto * 100);
        pasarela.cobrar(centavos);
    }
}

El resto del sistema solo conoce LectorPago; si mañana cambias de pasarela de pago, basta con escribir un nuevo adaptador sin modificar el código que la usa.

8. Command

Encapsula una solicitud (una acción con sus parámetros) como un objeto, permitiendo parametrizar, encolar, deshacer o registrar operaciones sin que el emisor conozca los detalles de cómo se ejecutan.

public interface Comando {
    void ejecutar();
}

public class EncenderLuz implements Comando {
    private final Luz luz;

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

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

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

public class ControlRemoto {
    private final List<Comando> historial = new ArrayList<>();

    public void ejecutar(Comando comando) {
        comando.ejecutar();
        historial.add(comando);
    }
}

Como cada acción es un objeto, puedes encolarla, ejecutarla en otro hilo, guardarla en un historial para un "deshacer" o programarla para más tarde — algo mucho más difícil si la lógica estuviera en un solo método gigante con if/else por cada botón.

¿Cuál patrón usar?

Problema Patrón
Necesito una única instancia compartida Singleton
El tipo de objeto a crear varía según una condición Factory Method
Un objeto tiene muchos parámetros opcionales Builder
Varias partes del sistema deben reaccionar a un cambio Observer
Quiero intercambiar algoritmos sin tocar el código cliente Strategy
Necesito agregar funcionalidad sin modificar la clase original Decorator
Debo integrar una API o librería con una interfaz distinta a la mía Adapter
Quiero tratar acciones como objetos (encolar, deshacer, registrar) Command

Ningún patrón es obligatorio: úsalos cuando resuelvan un problema real de tu diseño, no como regla fija. Aplicar un patrón donde no hace falta suele agregar complejidad innecesaria.