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