Introducción a los Patrones de Diseño en Java
Introducción a los patrones de diseño en Java: qué son y cómo usar los más comunes con ejemplos prácticos.
Un patrón de diseño es una solución reutilizable con nombre para un problema que aparece frecuentemente al construir software. Los patrones no son bibliotecas que se importan ni código que se copia — son formas de organizar clases y objetos en las que los desarrolladores experimentados se han puesto de acuerdo con el tiempo. Aprenderlos te da un vocabulario compartido: di "usaremos un Factory aquí" y otro desarrollador Java sabrá de inmediato aproximadamente qué quieres decir.
Esta página presenta qué son los patrones de diseño, las tres familias en las que se dividen y tres de los más comunes — Strategy, Factory y Singleton — con un ejemplo ejecutable que los combina. Se asume que estás familiarizado con las interfaces y el polimorfismo, ya que prácticamente todo patrón se apoya en ellos.
Los patrones fueron popularizados por el libro de 1994 Design Patterns de la "Gang of Four", que catalogó 23 de ellos. No necesitas los 23 para empezar. Un puñado, aplicado en el momento correcto, hace que el código sea más fácil de cambiar sin romperlo.
Las tres familias
El catálogo clásico divide los patrones en tres grupos según para qué sirven:
| Familia | Preocupación | Ejemplos |
|---|---|---|
| Creacional | Cómo se crean los objetos | Singleton, Factory, Builder |
| Estructural | Cómo se componen los objetos | Adapter, Decorator, Facade |
| Comportamiento | Cómo interactúan los objetos | Strategy, Observer, Iterator |
Java en sí está lleno de ellos. StringBuilder es un Builder, Iterator es el patrón Iterator y java.util.logging usa Singletons. Ya estabas usando patrones sin nombrarlos.
Strategy: intercambiar el algoritmo
El patrón Strategy encapsula una familia de algoritmos intercambiables detrás de una interfaz común, de modo que el código que los llama puede cambiar entre ellos sin modificarse. Se define la interfaz, se escribe cada variante como su propia clase y se deja que un contexto mantenga la que necesite.
interface DiscountStrategy {
double apply(double price);
}
class PercentOff implements DiscountStrategy {
private final double percent;
PercentOff(double percent) { this.percent = percent; }
public double apply(double price) { return price * (1 - percent / 100); }
}Una clase contexto mantiene un DiscountStrategy y delega en él, en lugar de ramificarse con una cadena de if/switch. Agregar un nuevo descuento significa agregar una clase — no editar el código existente. (El contexto completo de Checkout, junto con las variantes NoDiscount y FlatOff, aparecen en el demo ejecutable a continuación.)
Factory: centralizar la creación
Un Factory es un único método (o clase) responsable de decidir qué tipo concreto instanciar. Los llamadores piden un objeto por descripción y reciben uno que cumple la interfaz, sin conocer la clase exacta.
static DiscountStrategy forCustomer(String tier) {
return switch (tier) {
case "gold" -> new PercentOff(20);
case "silver" -> new PercentOff(10);
default -> new NoDiscount();
};
}La lógica de creación vive en un solo lugar. Si las reglas cambian, se edita el factory — todos los llamadores siguen funcionando sin cambios.
Singleton: exactamente una instancia
Un Singleton garantiza que una clase tenga una sola instancia y proporciona un punto de acceso global a ella. En Java moderno, un enum es la forma más sencilla y segura para hilos de escribir uno — la JVM garantiza que sus constantes se crean exactamente una vez. Consulta The Singleton Pattern para las variantes de inicialización perezosa y double-checked-locking.
enum Config {
INSTANCE;
private final String env = "production";
public String env() { return env; }
}
// usage
String e = Config.INSTANCE.env();Usa los Singletons con moderación — introducen estado global, lo que dificulta las pruebas. Con frecuencia, un único objeto pasado a través de un constructor (inyección de dependencias) es la mejor opción.
Cuándo no recurrir a un patrón
Los patrones añaden estructura, y la estructura tiene un costo. Un switch con dos casos no necesita el patrón Strategy; una clase que se crea una sola vez no necesita un Factory. Recurre a un patrón cuando sientas el dolor que resuelve — ramificaciones duplicadas, llamadas a new dispersas, conexión enredada de objetos — no antes. Aplicar patrones en exceso produce código más difícil de leer que el problema que se pretendía simplificar.
Strategy y Factory juntos
El ejemplo ejecutable a continuación combina dos patrones. DiscountStrategy es la interfaz Strategy con tres implementaciones; forCustomer es un Factory que elige una. El contexto Checkout delega en la estrategia que tenga, de modo que su código nunca cambia aunque varíe el comportamiento de precios.
Qué extraer de la ejecución:
- Cada nivel imprime un total diferente —
regularse mantiene en $100.00,silverbaja a $90.00,golda $80.00,coupona $95.00 — aunqueCheckout.totalllama al mismo método único. - El contexto
Checkoutnunca ramifica sobre el nivel en sí; simplemente delega en elDiscountStrategyque tiene en ese momento. - El Factory
forCustomeres el único lugar que sabe qué clase concreta corresponde a qué nivel, por lo que la lógica de selección vive en un solo sitio. - Intercambiar el comportamiento es simplemente
setStrategy(...)en tiempo de ejecución — agregar un cuarto descuento significaría una nueva clase más un caso en el factory, sin cambios enCheckout. - Las últimas líneas confirman la estrategia activa: tras reseleccionar
gold, la política muestra20.0% offy el total es $80.00, lo que prueba que el contexto refleja la última estrategia establecida.