Patrón Module
Encapsula estado y comportamiento privados dentro de una unidad auto-contenida con una API pública. Un patrón estructural para organizar código en módulos reutilizables y a salvo de scope.
Visión General
El Patrón Module mantiene el estado privado y las funciones auxiliares dentro de una unidad auto-contenida y expone solo la API que tú eliges. Nada se filtra al namespace global, así que partes no relacionadas de un codebase no pueden chocar ni meterse en los internals de las demás.
Antes de ES6, JavaScript lo lograba con Immediately Invoked Function Expressions: envuelves todo en una función, la llamas y devuelves lo público. Los lenguajes modernos traen módulos nativos (ES modules, paquetes Python, paquetes Java), pero la idea central es la misma en ambos casos: ocultar internals, publicar una interfaz.
Cuándo Usar
- Necesitas estado privado al que no se pueda acceder desde fuera de la unidad
- Los componentes que comparten un codebase no deben chocar en los nombres
- Quieres que los consumidores vean una API limpia mientras los detalles de implementación quedan ocultos
- Los tests necesitan límites claros entre el contrato público y los helpers privados
Si necesitas envolver una clase existente tras una interfaz distinta en lugar de encapsular estado, el que buscas es el Patrón Adapter.
Cuándo Evitar
- El lenguaje te da módulos nativos con encapsulación real — úsalos primero
- Un script simple que no gana nada más que indirección al envolverse
- Módulos que dependerían entre sí lo suficiente como para crear imports circulares
Solución
JavaScript (IIFE)
const CounterModule = (function () {
let count = 0; // Estado privado — inalcanzable desde fuera
function log(operation) {
console.log(`Counter ${operation}: ${count}`);
}
return {
increment() {
count++;
log('incremented');
return count;
},
decrement() {
count--;
log('decremented');
return count;
},
getCount() {
return count;
}
};
})();
// Uso
CounterModule.increment();
CounterModule.increment();
console.log(CounterModule.getCount()); // 2
// CounterModule.count es undefined — no accesible
JavaScript (ES Module)
// counter.js
let count = 0; // Scope de módulo — no exportado, no accesible
function log(operation) {
console.log(`Counter ${operation}: ${count}`);
}
export function increment() {
count++;
log('incremented');
return count;
}
export function decrement() {
count--;
log('decremented');
return count;
}
export function getCount() {
return count;
}
// main.js
import { increment, getCount } from './counter.js';
increment();
console.log(getCount());
Python
# counter.py
_count = 0 # Privado al módulo por convención
def _log(operation):
print(f"Counter {operation}: {_count}")
def increment():
global _count
_count += 1
_log("incremented")
return _count
def decrement():
global _count
_count -= 1
_log("decremented")
return _count
def get_count():
return _count
# main.py
from counter import increment, get_count
increment()
increment()
print(get_count()) # 2
# counter._count es accesible pero desaconsejado — el guion bajo marca la intención
Java
// com.myapp.counter.Counter.java
package com.myapp.counter;
public class Counter {
private static int count = 0;
private static void log(String operation) {
System.out.println("Counter " + operation + ": " + count);
}
public static int increment() {
count++;
log("incremented");
return count;
}
public static int decrement() {
count--;
log("decremented");
return count;
}
public static int getCount() {
return count;
}
}
// Uso
Counter.increment();
System.out.println(Counter.getCount());
Explicación
El mecanismo difiere por lenguaje, pero el contrato es idéntico. En una IIFE, el objeto retornado es lo único que escapa — count y log viven dentro del closure, y el motor de JavaScript los mantiene vivos solo porque los métodos retornados aún los referencian. No hay sintaxis para entrar desde fuera; la privacidad no es una convención, la impone el scope.
Los ES modules hacen lo mismo de forma estática. Las declaraciones de nivel superior que no se exportan son invisibles para los importers — count y log existen en el scope del módulo y en ningún otro sitio. La diferencia importa en el build: un bundler puede analizar los import/export sin ejecutar nada, que es lo que hace posible el tree-shaking (la guía de optimización del tamaño del bundle profundiza en por qué eso importa para el código que se publica). Una IIFE es opaca a ese análisis — el bundler no puede saber qué partes se usan, así que todo viaja.
Python toma la forma más débil: import da scope de módulo real, pero _count solo es privado por convención — cualquiera puede acceder a counter._count si insiste. Java queda en medio: los miembros private son realmente inaccesibles desde otras clases, pero “módulo” en el sentido Java (un package, o un módulo JPMS desde Java 9) va más de agrupar clases que de ocultar valores.
Una consecuencia que conviene entender: el estado del módulo es estado compartido. Cada importer de counter.js recibe el mismo count — el módulo es un namespace singleton, lo quisieras o no. Cuando necesitas varios contadores independientes, necesitas una factory o una clase, no un módulo. (Y si te tienta mantener un módulo global por cada slice de estado de la app, el Patrón Singleton tiene la misma forma con las mismas trampas.)
Variantes
| Variante | Mecanismo | Caso de Uso |
|---|---|---|
| IIFE Module | Privacidad por closure | JavaScript pre-ES6, scripts inline |
| Revealing Module | Retorna un object literal que mapea nombres públicos a funciones privadas | Definición de API más limpia en JS |
| CommonJS | module.exports / require | Node.js antes de ES modules, algunas herramientas |
| ES Module | export/import | Estándar moderno de JavaScript |
| Namespace Module | Namespace con object literal | Agrupar utilidades bajo un global |
Lo Que Funciona
- Un concern por módulo. Un módulo que cuenta, formatea fechas y hace HTTP requests son tres módulos disfrazados con gabardina.
- Exporta explícitamente. Los wildcard exports ocultan el contrato — un lector debería ver la API pública de un vistazo.
- Nombra los miembros privados claramente. El
_prefijode Python dice “interno, no tocar” sin imponerlo; los closures de JavaScript no necesitan la pista. - Mata las dependencias circulares pronto. A importa B, B importa A — extrae lo compartido a un tercer módulo antes de que el ciclo se osifique.
- Mantén los módulos pequeños. Pasadas las ~200 líneas, un módulo normalmente pide división; pasadas 500, la exige.
Errores Comunes
- Estado global en módulos los hace no testeables. Un módulo contador con
countcompartido no se puede resetear entre tests — acabarás con una suite dependiente del orden. export *wildcard exports filtra cada helper interno como API pública accidental. En cuanto alguien lo importa, te toca mantenerlo.- Side effects en import — conectar a una base de datos, registrar listeners, mutar globales al cargar — hacen los módulos impredecibles y rompen el aislamiento de tests.
- Rutas profundamente anidadas (
a.b.c.d.e) crean cadenas de imports frágiles que se rompen en cada refactor. Mantén el grafo superficial. - Tratar módulos como clases. Un módulo es un namespace, no un blueprint — no existe
new CounterModule(). Cuando necesitas instancias, escribe una factory o una clase.
Ejemplos del Mundo Real
Módulos Core de Node.js
fs, path y http envuelven operaciones del SO tras APIs estables. Los bindings C++ internos quedan privados — nunca los tocas directamente, que es justo la idea.
Python Standard Library
json, re y urllib ocultan parsers y compiladores de regex tras un puñado de funciones. json.loads es una función pública; el scanner de debajo es privado del módulo.
Angular Modules
NgModule agrupa componentes, servicios y directivas en unidades de feature con exports explícitos — la versión del framework de “ocultar internals, publicar una interfaz”.
Troubleshooting
- Imports circulares en runtime: A importa B, B importa A — Node te devuelve un módulo parcialmente inicializado (
undefineddonde esperabas funciones). Extrae la dependencia compartida a un tercer módulo que ambos importen. - Estado que se filtra entre tests: el estado del módulo persiste durante todo el proceso de test. Resetealo explícitamente en el setup, o inyecta una factory para que cada test construya una instancia fresca.
requirede un ES module falla: Node no puede hacerrequire()de un.mjso de un paquete"type": "module"en versiones antiguas — cambia aimporto fíjate una versión donderequire(esm)esté soportado.- El import funciona en local pero rompe en el bundler: suele ser un desajuste de mayúsculas (
./Counter.jsvs./counter.js) — el CI en Linux lo encontrará aunque macOS no. - Todo exportado “por si acaso”: una superficie pública hinchada se convierte en deuda de mantenimiento. Exporta lo que los consumidores necesitan hoy; añade el resto cuando lo pidan.
Temas Avanzados
Escenario: Patrón Module para encapsulación en JS
// Module pattern: encapsulación privada con closures
const PaymentModule = (function() {
// Privado: no accesible desde fuera
let apiKey = "";
let transactions: Transaction[] = [];
const MAX_TRANSACTIONS = 1000;
// Privado: validar API key
function validateKey(key: string): boolean {
return key.startsWith("pk_") && key.length === 32;
}
// Privado: logging interno
function log(msg: string) {
console.log(`[PAYMENT] ${new Date().toISOString()} ${msg}`);
}
// Público: API expuesta
return {
init(key: string) {
if (!validateKey(key)) throw new Error("Invalid API key");
apiKey = key;
log("Module initialized");
},
charge(amount: number, currency: string): Promise<Transaction> {
if (!apiKey) throw new Error("Module not initialized");
if (transactions.length >= MAX_TRANSACTIONS) {
throw new Error("Transaction limit reached");
}
log(`Charging ${amount} ${currency}`);
const tx = { id: crypto.randomUUID(), amount, currency, status: "pending" };
transactions.push(tx);
return Promise.resolve(tx);
},
getTransactions(): Transaction[] { return [...transactions]; },
getStats() {
return {
total: transactions.length,
pending: transactions.filter(t => t.status === "pending").length,
completed: transactions.filter(t => t.status === "completed").length,
};
}
};
})();
// Uso
PaymentModule.init("pk_test_1234567890123456789012345678");
const tx = await PaymentModule.charge(99.99, "USD");
console.log(PaymentModule.getStats()); // { total: 1, pending: 1, completed: 0 }
// PaymentModule.apiKey -> undefined (privado)
// PaymentModule.validateKey -> undefined (privado)
Este ejemplo enseña por qué el patrón sobrevivió a ES6: init crea un ciclo de vida que el llamador controla, apiKey y transactions son inalcanzables sin trucos de reflection, y el tope de MAX_TRANSACTIONS demuestra enforcement de estado que sería incómodo de atornillar a un objeto plano. La contrapartida es la de antes — todo viaja, siempre, porque el bundler no puede ver dentro del closure.
Lecturas Adicionales
- MDN — JavaScript modules — la referencia definitiva de la semántica
import/export - Recursos complementarios — ejemplos ejecutables en IIFE, ES module, Python y Java más el escenario de pagos
Preguntas frecuentes
¿Es el Patrón Module lo mismo que una clase?
No. Una clase es un blueprint que instancias muchas veces; un módulo es un namespace único que se evalúa una vez. Usa módulos para organizar código y clases cuando necesites varias instancias con su propio estado.
¿Pueden los módulos depender entre sí?
Sí, es lo normal — solo evita los ciclos. Si A importa B y B importa A, extrae la pieza compartida a un tercer módulo del que ambos dependan.
¿Debería seguir usando el IIFE Module Pattern en 2026?
Solo para código legacy o scripts inline sin build step. Los ES modules son el estándar — nativos en browsers y Node, analizables estáticamente, tree-shakeables. Una IIFE publica todo siempre; un ES module deja que el bundler descarte lo que no importas.
¿Cómo migro una IIFE module a un ES module?
Mueve el contenido del archivo al nivel superior, borra la función envolvente y reemplaza el objeto retornado con export individuales. Los miembros privados quedan sin exportar — eran privados por scope antes y ahora son privados por no-export. El comportamiento es idéntico; la sintaxis es más limpia.
¿Cómo testeo funciones privadas de un módulo?
No lo haces — las testeas a través de la API pública. Si una función privada es lo bastante compleja como para necesitar tests directos, es señal de que pertenece a su propio módulo.
¿Qué diferencia hay entre un módulo y un singleton?
Un módulo es un namespace a nivel de archivo; un singleton es deliberadamente una única instancia de una clase u objeto. Todo ES module es un singleton en la práctica, pero los singletons también pueden crearse de forma lazy e inyectarse — los módulos no se pueden intercambiar así.
Recursos Relacionados
Patrón Facade
Provee una interfaz simplificada a un subsistema complejo. Un patrón estructural que oculta detalles de implementación detrás de una API limpia.
PatternPatrón Singleton: Ejemplos en Java, Python y TypeScript
Garantiza que una clase tenga una única instancia y ofrece acceso global a ella — el patrón creacional para controlar la creación de objetos, con implementaciones listas para producción.
PatternPatrón Repository
Abstrae la lógica de acceso a datos detrás de una interfaz limpia. Patrón de diseño arquitectural para capas de datos testeables y mantenibles.
PatternPatrón Plugin
Habilita extensiones de terceros definiendo puntos de extensión en un sistema host que carga y ejecuta módulos externos en vivo.
GuideOptimización del Bundle JS: Guía Práctica Frontend
Guía práctica para medir y reducir el tamaño del bundle JavaScript con tree shaking, code splitting, reemplazo de dependencias, compresión y budgets en CI.