Introducción
Cuando trabajamos con Node.js, especialmente en proyectos backend que crecen con el tiempo, la forma en la que organizamos el código se vuelve tan importante como la lógica de negocio. Los patrones de diseño nos ayudan a resolver problemas comunes de una manera probada y mantenible.
En este artículo voy a repasar distintos patrones de diseño aplicados a Node.js, tomando como base el libro Node.js Design Patterns, pero adaptando los ejemplos al estilo moderno de JavaScript (ES6).
Un enfoque distinto a Java y C#
Si venís de lenguajes como Java o C#, vas a notar que los patrones no se implementan exactamente igual. En JavaScript no todo gira en torno a clases e herencia; aquí pesan mucho más conceptos como funciones, composición y módulos.
Aun así, la idea central de cada patrón se mantiene intacta. El objetivo sigue siendo el mismo: desacoplar, reutilizar y hacer el código más fácil de mantener.
Patrones que vamos a ver
- Factory
- Proxy
- Decorator
- Adapter
- Strategy
- State
- Template
- Middleware
- Command
- Singleton
- Universal Module Definition (UMD)
Factory: controlar la creación de objetos
El patrón Factory se utiliza cuando no queremos que el código que consume un objeto sea responsable de crearlo directamente. En lugar de instanciar con new en todos lados, delegamos esa responsabilidad a una fábrica.
Esto nos permite validar datos, cambiar la forma de construir el objeto o incluso devolver distintas implementaciones sin afectar al resto del sistema.
class Person {
constructor(name, surname, age, sex) {
this.name = name;
this.surname = surname;
this.age = age;
this.sex = sex;
}
}
class PersonFactory {
constructor() {
this.data = {};
}
set(key, value) {
this.data[key] = value;
}
build() {
return new Person(
this.data.name,
this.data.surname,
this.data.age,
this.data.sex
);
}
}
const factory = new PersonFactory();
factory.set('name', 'Damián');
factory.set('surname', 'Cipolat');
factory.set('age', 30);
factory.set('sex', 'M');
const person = factory.build();
console.log(person);
Proxy: controlar el acceso a un objeto
El patrón Proxy actúa como un intermediario. En lugar de acceder directamente a un objeto, lo hacemos a través de otro que decide qué hacer con cada llamada.
Es muy común usar proxies para agregar validaciones, logs, control de permisos o incluso cachear resultados.
class Service {
getMessage() {
return 'Hola';
}
}
class ServiceProxy {
constructor(service) {
this.service = service;
}
getMessage() {
const result = this.service.getMessage();
return result + ' mundo';
}
}
const service = new Service();
const proxy = new ServiceProxy(service);
console.log(proxy.getMessage());
Decorator: extender comportamiento sin herencia
Decorator es ideal cuando queremos agregar nuevas funcionalidades a un objeto sin modificar su clase original. A diferencia de la herencia, el cambio se aplica solo al objeto decorado.
class User {
greet() {
return 'Hola';
}
}
class UserDecorator {
constructor(user) {
this.user = user;
}
greet() {
return this.user.greet() + ', bienvenido!';
}
}
const user = new User();
const decoratedUser = new UserDecorator(user);
console.log(decoratedUser.greet());
Adapter: hacer compatibles interfaces distintas
El Adapter entra en juego cuando tenemos dos componentes que hacen cosas similares pero con interfaces diferentes. En lugar de modificar uno de ellos, creamos un adaptador.
class OldShipping {
calculate() {
return '$50';
}
}
class ShippingAdapter {
constructor(service) {
this.service = service;
}
getCost() {
return this.service.calculate();
}
}
const adapter = new ShippingAdapter(new OldShipping());
console.log(adapter.getCost());
Strategy y State: comportamiento intercambiable
Strategy nos permite cambiar un algoritmo en tiempo de ejecución sin modificar el código que lo utiliza. State es una variación donde el comportamiento depende del estado interno.
Ambos patrones son muy útiles cuando tenemos múltiples reglas que cambian según el contexto.
Middleware: encadenar responsabilidades
En Node.js este patrón es muy común, especialmente en frameworks como Express. La idea es simple: una función procesa datos y pasa el resultado a la siguiente.
Esto permite crear flujos claros y reutilizables para validaciones, transformaciones o cálculos.
Command: acciones como objetos
Command encapsula una acción completa en un objeto. Esto hace posible ejecutar, deshacer o registrar operaciones sin acoplarlas al código que las dispara.
Es especialmente útil en sistemas que requieren historial de acciones.
Singleton: una sola instancia
El patrón Singleton asegura que exista una única instancia de un objeto. En Node.js, los módulos ya funcionan naturalmente de esta forma, lo que lo vuelve muy práctico para configuraciones o servicios compartidos.
Universal Module Definition
UMD busca que un mismo módulo funcione tanto en Node.js como en el navegador, sin importar el sistema de módulos utilizado.
Es una solución pensada para librerías que deben ejecutarse en distintos entornos.
Los patrones de diseño no son recetas mágicas, pero aplicados correctamente pueden marcar una gran diferencia en la calidad de un proyecto Node.js.
Entender cuándo usarlos y adaptarlos al estilo de JavaScript moderno es una habilidad clave para cualquier desarrollador backend.