Patrones de diseño en JavaScript con ejemplos
Los patrones de diseño (design patterns) son soluciones probadas para problemas típicos en el desarrollo de software. Para un desarrollador de JavaScript, el conocimiento de patrones es especialmente importante, ya que el lenguaje tiene un modelo de objetos flexible y se usa ampliamente tanto en el cliente como en el servidor (Node.js). En este artículo analizaremos cuatro patrones clave con ejemplos prácticos en JavaScript.
¿Por qué estudiar patrones de diseño?
Los patrones ayudan a:
- Simplificar el mantenimiento y la escalabilidad del código
- Hacer el código más legible y predecible
- Evitar "reinventar la rueda"
- Mejorar la comunicación en el equipo (lenguaje común)
1. Singleton (Instancia única)
El patrón Singleton garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a ella. En Node.js, este patrón se usa a menudo para conectarse a una base de datos, trabajar con configuración o registro (logging).
class DatabaseConnection { constructor() { if (DatabaseConnection.instance) { return DatabaseConnection.instance; } this.connection = null; DatabaseConnection.instance = this; }
connect() { // Simulación de conexión a la BD this.connection = "connected"; console.log("Conexión a la BD establecida"); }
query(sql) { if (!this.connection) { throw new Error("No hay conexión a la BD"); } console.log(`Ejecutando consulta: ${sql}`); return { data: [] }; }}
// Usoconst db1 = new DatabaseConnection();const db2 = new DatabaseConnection();
db1.connect();console.log(db1 === db2); // true - ambos apuntan a la misma instanciadb2.query("SELECT * FROM users");Cuándo aplicarlo: gestión de recursos compartidos (BD, configuración, caché). Precaución: el uso excesivo puede provocar un fuerte acoplamiento del código.
2. Factory (Fábrica)
Factory Method define una interfaz para crear un objeto, pero permite que las subclases alteren el tipo de objeto creado. En JavaScript, esto a menudo se implementa mediante funciones o clases normales.
class User { constructor(name, role) { this.name = name; this.role = role; }
getPermissions() { return []; }}
class Admin extends User { getPermissions() { return ["read", "write", "delete", "manage"]; }}
class Editor extends User { getPermissions() { return ["read", "write"]; }}
class Viewer extends User { getPermissions() { return ["read"]; }}
// Fábricaclass UserFactory { createUser(name, role) { switch (role) { case "admin": return new Admin(name, role); case "editor": return new Editor(name, role); case "viewer": return new Viewer(name, role); default: throw new Error("Rol desconocido"); } }}
// Usoconst factory = new UserFactory();const admin = factory.createUser("Ana", "admin");const viewer = factory.createUser("Iván", "viewer");
console.log(admin.getPermissions()); // ["read", "write", "delete", "manage"]console.log(viewer.getPermissions()); // ["read"]Ventajas: gestión centralizada de la creación de objetos, facilidad para agregar nuevos tipos, ocultación de la lógica de instanciación.
3. Observer (Observador)
El patrón Observer define una dependencia de "uno a muchos" entre objetos. Cuando un objeto (sujeto) cambia de estado, todos los objetos dependientes (observadores) son notificados automáticamente. Esta es la base de la programación orientada a eventos en Node.js.
class EventEmitter { constructor() { this.events = {}; }
on(eventName, listener) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(listener); }
emit(eventName, ...args) { const listeners = this.events[eventName]; if (listeners) { listeners.forEach(listener => listener(...args)); } }
off(eventName, listenerToRemove) { const listeners = this.events[eventName]; if (listeners) { this.events[eventName] = listeners.filter( listener => listener !== listenerToRemove ); } }}
// Usoconst emitter = new EventEmitter();
function onUserLoggedIn(user) { console.log(`Usuario conectado: ${user}`);}
emitter.on("userLoggedIn", onUserLoggedIn);emitter.emit("userLoggedIn", "María"); // Usuario conectado: María
emitter.off("userLoggedIn", onUserLoggedIn);emitter.emit("userLoggedIn", "Pedro"); // No pasa nadaCuándo aplicarlo: sistemas de eventos, actualización de UI en tiempo real, comunicación entre componentes desacoplados. Precaución: puede causar fugas de memoria si los observadores no se eliminan correctamente.
4. Module (Módulo)
El patrón Module permite encapsular código en unidades separadas, ocultando detalles internos y exponiendo solo una API pública. En JavaScript moderno, esto se implementa mediante módulos ES (import/export) o CommonJS (require/module.exports).
// math.js - Módulo ESconst PI = 3.14159;
function add(a, b) { return a + b;}
function subtract(a, b) { return a - b;}
function circleArea(radius) { return PI * radius * radius;}
export { add, subtract, circleArea };
// app.jsimport { add, circleArea } from './math.js';
console.log(add(5, 3)); // 8console.log(circleArea(10)); // 314.159Ventajas: encapsulación, reutilización, espacio de nombres limpio, facilidad para pruebas. En Node.js también se usa el patrón Module a través de CommonJS.
Conclusión
Los patrones de diseño son herramientas poderosas en el arsenal de un desarrollador de JavaScript. Singleton ayuda a gestionar recursos compartidos, Factory simplifica la creación de objetos, Observer proporciona una comunicación flexible entre componentes, y Module organiza el código en partes lógicas. La elección del patrón depende de la tarea específica: no intente aplicar patrones donde no se necesitan, pero cuando surja un problema típico, los patrones probados le ahorrarán tiempo y esfuerzo.
Practique la implementación de estos patrones en sus proyectos y, con el tiempo, aprenderá a reconocer situaciones donde su aplicación es apropiada. ¡Feliz codificación!