Saltar a contenido

Patrones de diseño

1. ¿Qué son los Patrones de Diseño?

Los patrones de diseño son:

  • Soluciones típicas a problemas comunes en el diseño de software
  • Plantillas reutilizables para resolver problemas
  • No código listo para copiar-pegar, sino descripciones/directrices
  • Experiencia colectiva de expertos documentada

1.1 Analogía

Los patrones de diseño son como arquitectura en construcción:

Problema: "Quiero una cocina funcional y hermosa"

Sin patrón:

- Cada arquitecto lo hace diferente
- Algunos diseños no funcionan
- Se pierde tiempo rediseñando

Con patrón (estándares arquitectónicos):

- Cocina abierta: Contador, horno, fregadero en línea
- Se sabe que funciona
- Se reduce tiempo de diseño
- Resultado consistente

1.2 Beneficios

  • Reutilización: Soluciones probadas que funcionan
  • Comunicación: Equipo habla el mismo idioma
  • Mantenibilidad: Código más fácil de entender
  • Escalabilidad: Diseños que crecen bien
  • Calidad: Menos bugs, mejor estructura
  • Velocidad: Menos tiempo de diseño

2. Clasificación de Patrones de Diseño

Los patrones se clasifican en 3 categorías principales:

2.1 Patrones Creacionales

¿Qué hacen?: Manejan la creación de objetos

¿Por qué son importantes?: Separar la creación de objetos de su uso

2.1.1 Singleton

Problema: Necesito que una clase tenga solo una instancia en toda la aplicación

Solución: Singleton asegura eso

Uso real: Conexión a BD, Logger, Configuración global

public class BaseDatos {
    // Única instancia
    private static BaseDatos instancia;

    // Constructor privado - no se puede hacer new
    private BaseDatos() {
        // Inicialización
    }

    // Método para obtener la instancia
    public static BaseDatos obtener() {
        if (instancia == null) {
            instancia = new BaseDatos();
        }
        return instancia;
    }
}

// Uso
BaseDatos bd1 = BaseDatos.obtener();
BaseDatos bd2 = BaseDatos.obtener();
// bd1 y bd2 son la MISMA instancia

2.1.2 Factory Method

Problema: Necesito crear objetos, pero no sé qué clase específica crear

Solución: Una "fábrica" decide qué crear según el contexto

Uso real: Parser de documentos (PDF, Word, etc.), Logger con diferentes destinos

// Sin Factory Method (acoplado)
public class MiAplicacion {
    public void procesar(String tipo) {
        if (tipo.equals("pdf")) {
            Documento doc = new DocumentoPDF();  // Acoplado a PDF
        } else if (tipo.equals("word")) {
            Documento doc = new DocumentoWord();  // Acoplado a Word
        }
    }
}

// Con Factory Method (desacoplado)
public abstract class DocumentoFactory {
    abstract Documento crear();
}

public class FactoryPDF extends DocumentoFactory {
    @Override
    Documento crear() {
        return new DocumentoPDF();
    }
}

// Uso
public class MiAplicacion {
    public void procesar(DocumentoFactory factory) {
        Documento doc = factory.crear();  // No necesita saber qué tipo
        doc.procesar();
    }
}

2.1.3 Builder

Problema: Necesito crear objetos complejos con muchos parámetros opcionales

Solución: Constructor paso a paso ("builder")

Uso real: Construcción de queries SQL, configuración, objetos complejos

// Sin Builder (muchos constructores)
public class Persona {
    public Persona(String nombre, int edad) { }
    public Persona(String nombre, int edad, String email) { }
    public Persona(String nombre, int edad, String email, String telefono) { }
    // ... muchos constructores
}

// Con Builder (elegante)
public class Persona {
    private String nombre;
    private int edad;
    private String email;
    private String telefono;

    public static class Builder {
        private String nombre;
        private int edad;
        private String email;
        private String telefono;

        public Builder nombre(String nombre) {
            this.nombre = nombre;
            return this;  // Retorna para encadenar
        }

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

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

        public Persona build() {
            Persona p = new Persona();
            p.nombre = this.nombre;
            p.edad = this.edad;
            // ...
            return p;
        }
    }
}

// Uso (muy legible)
Persona p = new Persona.Builder()
    .nombre("Juan")
    .edad(30)
    .email("juan@example.com")
    .build();

2.1.4 Abstract Factory

Problema: Crear familias de objetos relacionados

Uso real: Interfaces para diferentes sistemas operativos, Temas (claro/oscuro)

2.1.5 Prototype

Problema: Crear nuevos objetos clonando existentes

Uso real: Copiar documentos, duplicar configuraciones

2.2 Patrones Estructurales

¿Qué hacen?: Manejan la composición de clases y objetos

¿Por qué son importantes?: Crear estructuras que funcionan bien

2.2.1 Adapter (o Wrapper)

Problema: Dos interfaces incompatibles necesitan trabajar juntas

Solución: Crear un adaptador que traduce entre ellas

Uso real: Conectar código antiguo con nuevo, integrar librerías externas

// Interfaz antigua
public interface SistemaAntiguo {
    void envieDatos(String datos);
}

// Interfaz nueva
public interface SistemaModerno {
    void procesarInformacion(String info);
}

// El sistema antiguo no puede usar el moderno directamente
// Creamos un Adapter

public class AdaptadorSistemas implements SistemaAntiguo {
    private SistemaModerno sistemaModerno;

    public AdaptadorSistemas(SistemaModerno moderno) {
        this.sistemaModerno = moderno;
    }

    @Override
    public void envieDatos(String datos) {
        // Traduce el llamado antiguo al nuevo
        sistemaModerno.procesarInformacion(datos);
    }
}

// Uso
SistemaModerno moderno = new SistemaModerno();
SistemaAntiguo antiguo = new AdaptadorSistemas(moderno);
antiguo.envieDatos("datos");  // Funciona con interfaz antigua

2.2.2 Decorator

Problema: Añadir responsabilidades a objetos dinámicamente

Solución: Envolver el objeto con "decoradores"

Uso real: Buffering, logging, compresión

public interface Stream {
    void escribir(String datos);
}

public class StreamBasico implements Stream {
    @Override
    public void escribir(String datos) {
        System.out.println("Escribiendo: " + datos);
    }
}

// Decorador 1: Añade compresión
public class StreamComprimido implements Stream {
    private Stream stream;

    public StreamComprimido(Stream s) {
        this.stream = s;
    }

    @Override
    public void escribir(String datos) {
        String comprimido = comprimir(datos);
        stream.escribir(comprimido);
    }

    private String comprimir(String datos) {
        // Lógica de compresión
        return datos;
    }
}

// Decorador 2: Añade encriptación
public class StreamEncriptado implements Stream {
    private Stream stream;

    public StreamEncriptado(Stream s) {
        this.stream = s;
    }

    @Override
    public void escribir(String datos) {
        String encriptado = encriptar(datos);
        stream.escribir(encriptado);
    }
}

// Uso (composición - los combino)
Stream stream = new StreamBasico();
stream = new StreamComprimido(stream);    // Añade compresión
stream = new StreamEncriptado(stream);     // Añade encriptación
stream.escribir("datos secretos");
// Se comprime Y se encripta

2.2.3 Facade

Problema: Interfaz complicada con muchos subsistemas

Solución: Crear una "fachada" simple que oculta la complejidad

Uso real: Librerías complejas (BD, frameworks), API simplificada

// Subsistemas complejos
class Motor { void iniciar() { } }
class Transmision { void engranar(int marcha) { } }
class Combustible { void inyectar() { } }
class Ignicion { void activar() { } }

// Fachada simple
public class Automovil {
    private Motor motor = new Motor();
    private Transmision transmision = new Transmision();
    private Combustible combustible = new Combustible();
    private Ignicion ignicion = new Ignicion();

    // Usuario solo necesita llamar a arrancar()
    public void arrancar() {
        ignicion.activar();
        combustible.inyectar();
        motor.iniciar();
        transmision.engranar(1);
        System.out.println("Auto listo");
    }
}

// Uso simple
Automovil auto = new Automovil();
auto.arrancar();  // ¡Eso es todo! La complejidad está oculta

2.2.4 Composite

Problema: Tratar objetos simples y compuestos de la misma manera

Uso real: Estructura de carpetas/archivos, menús anidados

2.2.5 Proxy

Problema: Necesito controlar el acceso a otro objeto

Uso real: Lazy loading, validación de acceso, logging

2.3 Patrones de Comportamiento

¿Qué hacen?: Manejan comunicación entre objetos

¿Por qué son importantes?: Cómo los objetos se hablan entre sí

2.3.1 Observer

Problema: Necesito que múltiples objetos reaccionen a cambios

Solución: El objeto observado notifica a los "observadores"

Uso real: Event handlers, MVC, sistemas de notificación

// Interfaz del observador
public interface Observador {
    void actualizar(String mensaje);
}

// El sujeto que es observado
public class Sujeto {
    private List<Observador> observadores = new ArrayList<>();

    public void suscribir(Observador o) {
        observadores.add(o);
    }

    public void cambio(String mensaje) {
        // Notifica a todos los observadores
        for (Observador o : observadores) {
            o.actualizar(mensaje);
        }
    }
}

// Observadores específicos
public class Logger implements Observador {
    @Override
    public void actualizar(String mensaje) {
        System.out.println("[LOG] " + mensaje);
    }
}

public class Email implements Observador {
    @Override
    public void actualizar(String mensaje) {
        System.out.println("[EMAIL] Enviando: " + mensaje);
    }
}

// Uso
Sujeto sujeto = new Sujeto();
sujeto.suscribir(new Logger());
sujeto.suscribir(new Email());

sujeto.cambio("Algo pasó");
// Output:
// [LOG] Algo pasó
// [EMAIL] Enviando: Algo pasó

2.3.2 Strategy

Problema: Múltiples formas de hacer algo, elegible en tiempo de ejecución

Solución: Encapsular algoritmo en clases intercambiables

Uso real: Diferentes métodos de pago, algoritmos de compresión, ordenamiento

// Interfaz de estrategia
public interface EstrategiaPago {
    void pagar(double cantidad);
}

// Estrategias concretas
public class PagoTarjeta implements EstrategiaPago {
    @Override
    public void pagar(double cantidad) {
        System.out.println("Pagando " + cantidad + " con tarjeta");
    }
}

public class PagoPayPal implements EstrategiaPago {
    @Override
    public void pagar(double cantidad) {
        System.out.println("Pagando " + cantidad + " con PayPal");
    }
}

// Uso
public class Compra {
    private EstrategiaPago estrategia;

    public Compra(EstrategiaPago e) {
        this.estrategia = e;
    }

    public void procesar(double total) {
        estrategia.pagar(total);
    }
}

// En tiempo de ejecución, elegimos estrategia
if (usuarioPrefiereTarjeta) {
    compra = new Compra(new PagoTarjeta());
} else {
    compra = new Compra(new PagoPayPal());
}

2.3.3 Command

Problema: Encapsular una solicitud como objeto

Uso real: Deshacer/Rehacer, colas de trabajo, macros

2.3.4 Template Method

Problema: Algoritmo con pasos que varían

Solución: Define estructura en clase base, subclases implementan pasos

2.3.5 Iterator

Problema: Acceder secuencialmente a elementos sin exponer estructura

Uso real: Iteradores de colecciones, recorridos de árboles

3. Importancia de los Patrones de Diseño

3.1 Estándar de Soluciones Robustas

Los patrones representan soluciones comprobadas y optimizadas:

  • Evitan errores comunes
  • Han sido refinados durante años
  • Funcionan en múltiples contextos

3.2 Mejora de la Comunicación

Un "patrón de diseño" es un lenguaje común:

Sin patrones:
- Dev 1: "Necesitamos una clase que maneje un objeto único"
- Dev 2: "¿Eh? ¿Cómo?"

Con patrones:
- Dev 1: "Vamos a usar Singleton"
- Dev 2: "Ah, perfecto, entiendo exactamente lo que quieres"

3.3 Fomento de la Reusabilidad

Los patrones diseñan código que se reutiliza:

Factory: Código para crear objetos es independiente de las clases concretas
Builder: Lógica de construcción reutilizable
Decorator: Funcionalidades se pueden combinar de múltiples formas

3.4 Facilitación de la Refactorización

Los patrones proporcionan "dirección":

"Este código necesita refactorización"
- Identifica patrón que encaja
- Conoce exactamente cómo refactorizar

3.5 Reducción de Tiempo y Costos

Sin patrones:
- Diseño: 1 semana (inventando la rueda)
- Desarrollo: 2 semanas
- Testing: 1 semana
- Total: 4 semanas

Con patrones:
- Diseño: 1 día (aplicar patrón conocido)
- Desarrollo: 1 semana (código más claro)
- Testing: 3 días (estructurado)
- Total: 2 semanas

3.6 Adaptabilidad y Escalabilidad

Los patrones permiten que el software crezca sin romperse:

  • Factory: Agregar nuevos tipos sin cambiar código existente
  • Strategy: Agregar nuevos algoritmos dinámicamente
  • Decorator: Agregar funcionalidad sin modificar original

4. Uso en la Industria

4.1 Software Empresarial

MVC (Model-Vista-Controlador):

  • Separa lógica de negocio de UI
  • Usado en frameworks: Spring, Django, Laravel

Singleton: Gestión de conexiones a BD

Repository: Acceso a datos desacoplado

4.2 Software Embebido

Observer: Comunicación sin polling continuo

Facade: Interfaz simplificada para hardware

4.3 Videojuegos

Flyweight: Reducir memoria de objetos similares (enemigos, árboles)

Command: Sistema de undo/redo

State: Máquina de estados (idle, correr, saltar)

4.4 Aplicaciones Móviles

Adapter: Integrar librerías con interfaces diferentes

Strategy: Diferentes comportamientos según dispositivo/orientación

Dependency Injection: Facilita testing

4.5 Cloud y Microservicios

API Gateway: Patrón Facade para cluster de servicios

Circuit Breaker: Manejar fallos en servicios distribuidos

Service Locator: Descubrir servicios dinámicamente