🧩 ¿Qué son los patrones de diseño?
Un patrón de diseño (design pattern) es una solución general y reutilizable a un problema que ocurre con frecuencia en el diseño de software orientado a objetos. No es un fragmento de código listo para copiar y pegar, sino una plantilla conceptual que describe cómo resolver un tipo de problema y que puede adaptarse a situaciones concretas.
La idea de los patrones nace de una observación fundamental: los desarrolladores experimentados se enfrentan una y otra vez a problemas similares, y con el tiempo descubren que ciertas estructuras de clases y objetos funcionan mejor que otras. Un patrón de diseño captura esa experiencia colectiva y la formaliza en un formato que puede transmitirse, enseñarse y reutilizarse.
Cada patrón se define mediante cuatro elementos esenciales:
| Elemento | Descripción |
|---|---|
| Nombre | Identificador que permite referirse al patrón de forma concisa (ej. Singleton, Observer) |
| Problema | La situación recurrente de diseño donde el patrón es aplicable |
| Solución | La estructura de clases, objetos y relaciones que resuelve el problema |
| Consecuencias | Los beneficios y compromisos (trade-offs) que implica usar el patrón |
🏛️ Origen en la arquitectura: Christopher Alexander
El concepto de «patrón de diseño» no nació en la informática, sino en la arquitectura tradicional. El arquitecto austro-británico-americano Christopher Alexander (Viena, 1936 – 2022) fue el primero en formalizar la idea de que los buenos diseños comparten soluciones recurrentes a problemas comunes.
En 1977, Alexander publicó su obra maestra: A Pattern Language: Towns, Buildings, Construction, un libro de más de 1.100 páginas que catalogaba 253 patrones arquitectónicos, desde la escala de regiones y ciudades hasta detalles como la ubicación de una ventana o el diseño de una escalera. Cada patrón seguía una estructura rigurosa: nombre, problema, contexto, solución y relaciones con otros patrones.
La filosofía de Alexander se resumía en una idea poderosa: los problemas de diseño se repiten, y las mejores soluciones también. Si identificamos esas soluciones recurrentes y las catalogamos, cualquier persona — no solo un experto — puede crear diseños de calidad combinando patrones probados.
▶️ De la arquitectura al software
En 1987, una década después de A Pattern Language, los ingenieros de software Kent Beck y Ward Cunningham presentaron en la conferencia OOPSLA (Object-Oriented Programming, Systems, Languages & Applications) la idea de aplicar los patrones de Alexander al diseño de software. Su propuesta causó un impacto enorme: los desarrolladores reconocieron de inmediato que los mismos principios de soluciones recurrentes eran aplicables a los problemas del diseño orientado a objetos.
Como dato adicional, fue precisamente Ward Cunningham quien, inspirado por los patrones de Alexander, creó la primera wiki de la historia — la WikiWikiWeb — como repositorio colaborativo para documentar patrones de diseño de software. Esta tecnología sería más tarde la base de Wikipedia.
📖 El Gang of Four y el libro que cambió la ingeniería de software
El paso definitivo de los patrones de diseño de la arquitectura a la ingeniería de software se produjo en 1994, con la publicación de Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley). Sus cuatro autores son conocidos colectivamente como el Gang of Four (GoF):
| Autor | Afiliación en 1994 | Contribución destacada |
|---|---|---|
| Erich Gamma | Zúrich, Suiza | Co-creador de JUnit y Eclipse |
| Richard Helm | Montreal, Canadá | Consultor de arquitectura de software |
| Ralph Johnson | Universidad de Illinois | Investigador en frameworks orientados a objetos |
| John Vlissides | IBM Research, Nueva York | Investigador en patrones y herramientas de diseño (1961–2005) |
El libro se gestó a partir de un encuentro entre Gamma y Helm en la conferencia OOPSLA de 1990, donde ambos descubrieron su interés compartido por catalogar soluciones recurrentes en el diseño orientado a objetos. Pronto se unieron Johnson y Vlissides. El resultado fue un catálogo de 23 patrones de diseño organizados en tres categorías que se ha vendido en más de 500.000 copias y ha sido traducido a 13 idiomas.
Más de tres décadas después de su publicación, el libro sigue siendo un referente. Los 23 patrones GoF forman la base sobre la que se construyen frameworks modernos como Spring, Jakarta EE, Hibernate, Angular y React. Comprender estos patrones no es solo una cuestión académica: es la clave para entender cómo funcionan las herramientas que usamos a diario en la industria del software.
⚡ ¿Por qué importan los patrones de diseño?
Los patrones de diseño no son simples curiosidades teóricas. Su adopción generalizada responde a beneficios concretos y medibles en el desarrollo profesional de software:
✅ Vocabulario compartido: Cuando un equipo habla de «aplicar un Observer» o «usar un Factory Method», todos entienden inmediatamente la estructura propuesta. Esto reduce errores de comunicación y acelera las revisiones de código.
✅ Soluciones probadas: Cada patrón ha sido validado durante décadas en proyectos reales. Usarlos evita reinventar la rueda y reduce el riesgo de introducir defectos de diseño.
✅ Flexibilidad y mantenibilidad: Los patrones promueven el bajo acoplamiento y la alta cohesión, lo que hace que el código sea más fácil de modificar, extender y probar.
✅ Comprensión de frameworks: Spring usa Factory, Singleton, Proxy y Template Method internamente. Hibernate aplica Proxy, Iterator y Unit of Work. Sin conocer los patrones, es difícil entender por qué estos frameworks funcionan como funcionan.
❌ Riesgo de abuso: El uso indiscriminado de patrones donde no son necesarios conduce al over-engineering: código innecesariamente complejo que resulta más difícil de mantener que una solución directa.
📋 Clasificación de los 23 patrones GoF
El Gang of Four organizó los 23 patrones en tres familias según su propósito. Esta clasificación es universalmente aceptada y se utiliza en todos los libros, cursos y frameworks del mundo:
| Categoría | Propósito | Nº de patrones |
|---|---|---|
| 🔨 Creacionales | Controlan cómo se crean los objetos | 5 |
| 🏗️ Estructurales | Definen cómo se componen clases y objetos | 7 |
| 🔄 De comportamiento | Gestionan la comunicación entre objetos | 11 |
🔨 Patrones creacionales
Los patrones creacionales abstraen el proceso de instanciación de objetos. En lugar de crear objetos directamente con new, estos patrones proporcionan mecanismos que hacen el sistema independiente de cómo se crean, componen y representan sus objetos.
| Patrón | Problema que resuelve | Ejemplo real |
|---|---|---|
| Singleton | Garantizar que una clase tenga una sola instancia | Conexión a base de datos, logger |
| Factory Method | Delegar la creación de objetos a subclases | Crear documentos PDF, Word o HTML según tipo |
| Abstract Factory | Crear familias de objetos relacionados sin especificar clases concretas | Kits de interfaz gráfica multiplataforma |
| Builder | Construir objetos complejos paso a paso | Construir un pedido con productos, descuentos y envío |
| Prototype | Crear objetos copiando un prototipo existente | Clonar configuraciones en un editor gráfico |
// Interfaz del producto
interface Notificacion {
void enviar(String mensaje);
}
// Productos concretos
class NotificacionEmail implements Notificacion {
public void enviar(String mensaje) {
System.out.println("Email: " + mensaje);
}
}
class NotificacionSMS implements Notificacion {
public void enviar(String mensaje) {
System.out.println("SMS: " + mensaje);
}
}
// Factory Method: la subclase decide qué objeto crear
abstract class NotificacionFactory {
public abstract Notificacion crearNotificacion();
public void notificar(String mensaje) {
Notificacion n = crearNotificacion(); // Factory Method
n.enviar(mensaje);
}
}
class EmailFactory extends NotificacionFactory {
public Notificacion crearNotificacion() {
return new NotificacionEmail();
}
}
class SMSFactory extends NotificacionFactory {
public Notificacion crearNotificacion() {
return new NotificacionSMS();
}
}
🏗️ Patrones estructurales
Los patrones estructurales se ocupan de cómo se ensamblan clases y objetos para formar estructuras más grandes y complejas. Facilitan el diseño identificando formas sencillas de establecer relaciones entre entidades.
| Patrón | Problema que resuelve | Ejemplo real |
|---|---|---|
| Adapter | Hacer compatible una interfaz con otra | Adaptar una API antigua a un sistema nuevo |
| Bridge | Separar la abstracción de su implementación | Formas geométricas con distintos motores de renderizado |
| Composite | Tratar objetos individuales y compuestos de forma uniforme | Menú con submenús anidados, sistema de archivos |
| Decorator | Añadir funcionalidad a un objeto dinámicamente | Streams de Java: BufferedReader(new FileReader(...)) |
| Facade | Simplificar el acceso a un subsistema complejo | API unificada para un sistema de pagos |
| Flyweight | Compartir estado para soportar gran cantidad de objetos | Caracteres en un editor de texto (mismo glifo, distinta posición) |
| Proxy | Proporcionar un sustituto o representante de otro objeto | Carga perezosa de imágenes, proxy de seguridad |
// Componente base
interface Cafe {
String descripcion();
double precio();
}
// Componente concreto
class CafeSimple implements Cafe {
public String descripcion() { return "Café solo"; }
public double precio() { return 1.50; }
}
// Decorator base
abstract class CafeDecorator implements Cafe {
protected Cafe cafeDecorado;
public CafeDecorator(Cafe cafe) {
this.cafeDecorado = cafe;
}
}
// Decoradores concretos
class ConLeche extends CafeDecorator {
public ConLeche(Cafe cafe) { super(cafe); }
public String descripcion() { return cafeDecorado.descripcion() + " + leche"; }
public double precio() { return cafeDecorado.precio() + 0.30; }
}
class ConCanela extends CafeDecorator {
public ConCanela(Cafe cafe) { super(cafe); }
public String descripcion() { return cafeDecorado.descripcion() + " + canela"; }
public double precio() { return cafeDecorado.precio() + 0.15; }
}
// Uso: se "decoran" objetos dinámicamente
// Cafe pedido = new ConCanela(new ConLeche(new CafeSimple()));
// pedido.descripcion() → "Café solo + leche + canela"
// pedido.precio() → 1.95
🔄 Patrones de comportamiento
Los patrones de comportamiento se centran en los algoritmos y la asignación de responsabilidades entre objetos. Describen no solo los patrones de objetos o clases, sino también los patrones de comunicación entre ellos.
| Patrón | Problema que resuelve | Ejemplo real |
|---|---|---|
| Observer | Notificar cambios de estado a múltiples objetos | Listeners de eventos, suscripciones |
| Strategy | Intercambiar algoritmos en tiempo de ejecución | Distintas estrategias de ordenación |
| Command | Encapsular una petición como un objeto | Deshacer/rehacer en un editor |
| Iterator | Recorrer una colección sin exponer su estructura interna | for (Item i : coleccion) en Java |
| Template Method | Definir el esqueleto de un algoritmo, delegando pasos a subclases | Proceso de autenticación con pasos personalizables |
| State | Cambiar el comportamiento de un objeto según su estado | Máquina expendedora, estados de un pedido |
| Mediator | Reducir dependencias directas entre objetos | Torre de control de un aeropuerto |
| Chain of Responsibility | Pasar una petición a lo largo de una cadena de manejadores | Filtros de servlet, middleware |
| Visitor | Añadir operaciones a una estructura sin modificar sus clases | Compilador que recorre un AST |
| Memento | Capturar y restaurar el estado interno de un objeto | Guardar partida en un videojuego |
| Interpreter | Definir una gramática y un intérprete para un lenguaje | Evaluador de expresiones regulares |
import java.util.ArrayList;
import java.util.List;
// Sujeto observable
class TiendaOnline {
private List<ClienteObserver> suscriptores = new ArrayList<>();
private String productoDisponible;
public void suscribir(ClienteObserver cliente) {
suscriptores.add(cliente);
}
public void notificarDisponibilidad(String producto) {
this.productoDisponible = producto;
for (ClienteObserver c : suscriptores) {
c.actualizar(producto);
}
}
}
// Observer
interface ClienteObserver {
void actualizar(String producto);
}
// Observer concreto
class ClienteEmail implements ClienteObserver {
private String nombre;
public ClienteEmail(String nombre) { this.nombre = nombre; }
public void actualizar(String producto) {
System.out.println("Notificando a " + nombre + ": ¡" + producto + " disponible!");
}
}
// Uso:
// TiendaOnline tienda = new TiendaOnline();
// tienda.suscribir(new ClienteEmail("Ana"));
// tienda.suscribir(new ClienteEmail("Carlos"));
// tienda.notificarDisponibilidad("Java 21 Certification Guide");
// → Notificando a Ana: ¡Java 21 Certification Guide disponible!
// → Notificando a Carlos: ¡Java 21 Certification Guide disponible!
💻 Ejemplo integrador en Java: patrón Singleton
El patrón Singleton garantiza que una clase tenga exactamente una sola instancia y proporciona un punto de acceso global a ella. Es uno de los patrones más sencillos de entender, pero también uno de los más polémicos por sus implicaciones en el testing y la concurrencia.
🔹 Problema
Necesitamos que ciertos recursos compartan una única instancia a lo largo de toda la aplicación: una conexión a base de datos, un registro de log, un gestor de configuración. Crear múltiples instancias desperdiciaría recursos o provocaría inconsistencias.
🔹 Solución en Java (thread-safe con Bill Pugh idiom)
public class ConfiguracionApp {
// Constructor privado: nadie puede instanciar desde fuera
private ConfiguracionApp() {
System.out.println("Cargando configuración...");
}
// Clase interna estática: se carga solo cuando se necesita (lazy)
private static class Holder {
private static final ConfiguracionApp INSTANCIA = new ConfiguracionApp();
}
// Punto de acceso global
public static ConfiguracionApp getInstancia() {
return Holder.INSTANCIA;
}
// Métodos de negocio
private String idioma = "es";
private int maxConexiones = 10;
public String getIdioma() { return idioma; }
public void setIdioma(String idioma) { this.idioma = idioma; }
public int getMaxConexiones() { return maxConexiones; }
}
public class Main {
public static void main(String[] args) {
// Obtener la instancia única
ConfiguracionApp config1 = ConfiguracionApp.getInstancia();
ConfiguracionApp config2 = ConfiguracionApp.getInstancia();
// Ambas referencias apuntan al mismo objeto
System.out.println(config1 == config2); // true
// Modificar desde cualquier referencia afecta a todas
config1.setIdioma("en");
System.out.println(config2.getIdioma()); // "en"
}
}
// Salida:
// Cargando configuración...
// true
// en
@Scope("singleton")) en lugar de implementar Singleton manualmente. El patrón sigue siendo útil para entender el concepto, pero su implementación directa se considera un antipatrón en ciertos contextos.🎯 Cuándo usar y cuándo no usar patrones
La experiencia del Gang of Four y de décadas de práctica profesional ha condensado reglas claras sobre el uso adecuado de los patrones de diseño:
✅ Usa un patrón cuando...
El problema que enfrentas coincide con el que describe el patrón. Tu diseño necesita flexibilidad ante cambios futuros que puedes anticipar razonablemente. Varios miembros del equipo necesitan entender la arquitectura rápidamente. Estás trabajando en un proyecto con un ciclo de vida largo, donde la mantenibilidad es prioritaria.
❌ Evita un patrón cuando...
La solución directa es más sencilla y no hay indicios de que el diseño necesite evolucionar. Estás añadiendo capas de abstracción «por si acaso». El patrón complica el código más de lo que lo simplifica. El equipo no conoce el patrón y no hay tiempo para formación: un patrón mal implementado es peor que ningún patrón.
📝 Ejercicios prácticos
Ejercicio 1: Identifica el patrón
Observa el siguiente fragmento de código Java e identifica qué patrón de diseño GoF se está utilizando. Justifica tu respuesta indicando qué problema resuelve.
interface Descuento {
double aplicar(double precio);
}
class DescuentoNavidad implements Descuento {
public double aplicar(double precio) { return precio * 0.80; }
}
class DescuentoBlackFriday implements Descuento {
public double aplicar(double precio) { return precio * 0.50; }
}
class CarritoCompra {
private Descuento estrategia;
public void setDescuento(Descuento d) { this.estrategia = d; }
public double calcularPrecio(double precio) { return estrategia.aplicar(precio); }
}
Ejercicio 2: Implementa un Factory Method
Crea un sistema de transporte con una interfaz Vehiculo que tenga un método entregar(). Implementa Camion y Barco como vehículos concretos. Luego crea una clase abstracta LogisticaFactory con un factory method crearVehiculo() y dos subclases: LogisticaTerrestre (que crea camiones) y LogisticaMaritima (que crea barcos).
Ejercicio 3: Clasifica los patrones
Coloca cada uno de los siguientes patrones en su categoría correcta (Creacional, Estructural o Comportamiento): Observer, Builder, Adapter, Singleton, Strategy, Composite, Factory Method, Decorator, Command, Proxy.
Ejercicio 4: Implementa un Decorator
Diseña un sistema de pedidos de pizza. Crea una interfaz Pizza con métodos descripcion() y precio(). Implementa PizzaBasica (Margarita, 8.00€). Luego crea decoradores para ingredientes extra: ConQueso (+1.50€), ConJamon (+2.00€) y ConChampiñones (+1.20€). Muestra cómo pedir una pizza con queso y champiñones.
❓ Preguntas frecuentes sobre Origen de los Patrones de Diseño
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Origen de los Patrones de Diseño? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!