Ir al contenido
wannacode

Singleton e inyección de dependencias

En resumen: Singleton garantiza una única instancia de una clase, y la inyección de dependencias (Dependency Injection) pasa las dependencias desde fuera, para poder probar el código con un sustituto, por ejemplo un reloj.

Problema

La prueba que pasa solo los viernes

Los viernes la tienda da un 10 % de descuento. El programa tiene un reloj, así que es un Singleton: una clase con un solo objeto, accesible desde cualquier parte. El servicio de pedidos le pregunta qué día es:

class SystemClock {
today() {
return new Date();
}
}
const clock = new SystemClock();
const FRIDAY = 5;
class OrderService {
price(amount) {
if (clock.today().getDay() === FRIDAY) {
return amount * 0.9;
}
return amount;
}
}

Alguien escribe una prueba para asegurarse de que el descuento funciona:

const service = new OrderService();
console.log(`Descuento de viernes: esperaba 90.00 €, recibí ${service.price(100).toFixed(2)} €`);

Ejecutada el lunes 26 de octubre de 2026, la prueba imprime:

Salida
Descuento de viernes: esperaba 90.00 €, recibí 100.00 €

Un viernes la misma prueba imprime 90.00. La prueba pasa un día a la semana y falla los otros seis, y el código es el mismo. Para comprobar el descuento tienes que esperar al viernes o cambiar el reloj de la computadora.

El reloj en sí está bien hecho. El problema es que price lo busca él mismo. Desde fuera no ves que el servicio depende de un reloj: new OrderService() no pide nada. La prueba no puede darle otro día, porque no puede darle nada.

La idea

El único objeto y cómo pasarlo

Un Singleton garantiza dos cosas: solo hay un objeto, y cualquiera puede tomarlo desde cualquier parte. Lo primero a veces hace falta. Lo segundo es el precio:

  • una dependencia escondida. Quién usa el reloj solo se ve si lees cada línea de cada método;
  • estado compartido. Lo que un trozo de código cambia en el objeto lo ven todos los demás, también la prueba siguiente;
  • no se puede cambiar. El código señala la clase concreta por su nombre. Otro reloj para la prueba no tiene por dónde entrar.

La solución no es quitar el reloj, sino pasarlo desde fuera. El servicio no busca su dependencia, la recibe en el constructor. Qué reloj es lo decide quien crea el servicio: el programa le pasa el real, y la prueba el suyo, con una fecha que elige la prueba.

Esto se llama inyección de dependencias (Dependency Injection, DI). Hay tres papeles:

  • el servicio dice lo que necesita: un parámetro en el constructor. Qué es el objeto no importa, mientras tenga un método today();
  • la dependencia es el objeto que recibe: el reloj real o el de la prueba;
  • el cableado se hace en un solo lugar, al inicio del programa. Ahí decides qué objeto va a dónde.

El Singleton incluso puede quedarse: el único reloj se pasa como cualquier otro objeto. Lo que ya no pasa es que alguien lo tome por su cuenta.

Míralo

Singleton, como lo ofrece el lenguaje

En la lección «Patrones de diseño» viste un Singleton como una clase que guarda su único objeto. En JavaScript casi nunca se hace así, porque el lenguaje ya tiene algo que se ejecuta una sola vez: el módulo.Un módulo (el archivo clock.js) se ejecuta una vez, en el primer import. Cada import { clock } from "./clock.js" posterior da el mismo objeto. Un objeto exportado de un módulo con export es un Singleton sin una sola línea de código extra:
class SystemClock {
constructor() {
console.log("Creo el reloj");
}
today() {
return new Date();
}
}
export const clock = new SystemClock();
console.log("Inicio");
Salida
Creo el reloj
Inicio
El objeto se crea cuando se ejecuta el módulo, antes de la primera línea que lo usa. En este programa el módulo y el código están en un archivo. En un proyecto real el reloj está en su propio archivo, y da igual en cuántos sitios se importe: hay un solo objeto.
Más precisamente, para curiosos
Con require (CommonJS, el tipo más antiguo de módulos en Node.js) es igual: el módulo se ejecuta en el primer require, y los siguientes reciben su resultado ya listo.

Míralo

El reloj entra desde fuera

El servicio pide un reloj en su constructor. Qué es no importa, mientras tenga un método today(). El módulo con clock se queda igual:
const FRIDAY = 5;
class OrderService {
constructor(clock) {
this.clock = clock;
}
price(amount) {
if (this.clock.today().getDay() === FRIDAY) {
return amount * 0.9;
}
return amount;
}
}

En el programa, el reloj real se pasa una vez, al crear el servicio:

const service = new OrderService(clock);

Ahora new OrderService(...) dice en voz alta que no puede funcionar sin un reloj. La dependencia se ve en la firma del constructor, no en algún lugar dentro de un método.

Míralo

La prueba elige el día por sí misma

La prueba necesita un reloj al que puedas decirle qué fecha devolver:

class FakeClock {
constructor(date) {
this.date = date;
}
today() {
return this.date;
}
}

La prueba lo pasa en lugar del real:

const clock = new FakeClock(new Date(2026, 9, 30));
const service = new OrderService(clock);
console.log(`Viernes: ${service.price(100).toFixed(2)} €`);
clock.date = new Date(2026, 9, 27);
console.log(`Martes: ${service.price(100).toFixed(2)} €`);
Salida
Viernes: 90.00 €
Martes: 100.00 €

const clock = new FakeClock(new Date(2026, 9, 30));
const service = new OrderService(clock);
console.log(`Viernes: ${service.price(100).toFixed(2)} €`);
clock.date = new Date(2026, 9, 27);
console.log(`Martes: ${service.price(100).toFixed(2)} €`);
Paso a paso, en palabras
  1. Antes, price buscaba él mismo el clock a nivel de módulo: un vínculo escondido con un objeto global que la prueba no puede cambiar.
  2. La prueba crea su propio reloj: un FakeClock que dice 2026-10-30, viernes.
  3. El reloj entra desde fuera, por el constructor. OrderService solo guarda una referencia a él.
  4. price(100): el servicio le pregunta a su reloj. Es viernes, así que el importe es 90.00.
  5. La prueba mueve la fecha al 2026-10-27, un martes. En OrderService no cambia nada.
  6. El mismo price(100): un martes, 100.00. La prueba da estos dos números cualquier día de la semana.
Los meses en Date se cuentan desde 0: new Date(2026, 9, 30) es el 30 de octubre.

La prueba muestra estos dos números un lunes, un viernes y un domingo. OrderService es la misma clase que se ejecuta en la tienda: la prueba solo cambia lo que le pasa.

Más precisamente, para curiosos
Un reloj con un método también puede ser un objeto simple: { today: () => new Date(2026, 9, 30) }. No hace falta una clase: al servicio solo le hace falta un método today().

Pruébalo

Predice la salida

Las clases son de las tarjetas anteriores. El 31 de octubre de 2026 es sábado. ¿Qué imprime el programa?

const clock = new FakeClock(new Date(2026, 9, 30));
const shop = new OrderService(clock);
clock.date = new Date(2026, 9, 31);
const app = new OrderService(clock);
console.log(`${shop.price(50).toFixed(2)} €`);
console.log(`${app.price(50).toFixed(2)} €`);
Respuesta

Pruébalo

Encuentra el error

El carrito es un Singleton, «para que haya uno»:

class Cart {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
}
const cart = new Cart();

Dos pruebas, cada una con su propia página de pago. check imprime OK o lo que la prueba esperaba:

const first = new CheckoutPage();
first.add("Café");
first.add("Taza");
check("Prueba 1", first.count(), 2);
const second = new CheckoutPage();
second.add("Café");
check("Prueba 2", second.count(), 1);
Salida
Prueba 1: OK
Prueba 2: FAIL, esperaba 1, recibí 3

Cada prueba pasa por separado. Juntas, la segunda falla. ¿En qué línea está el error?

Elige la línea con el error.

Más precisamente, para curiosos

Las bibliotecas de pruebas ejecutan las pruebas en un orden que no está garantizado, a veces a la vez. Una prueba que pasa solo cuando se ejecuta primero es una prueba que falla al azar. El estado compartido en un Singleton es la causa más común.

La idea

Cuándo no lo necesitas

Un Singleton tiene sentido cuando una segunda copia sería un error y el objeto no guarda nada que cambie: la configuración que se lee una vez al arrancar, el registro, el grupo de conexiones a la base de datos. Incluso entonces es mejor pasarlo desde fuera que dejar que todos lo tomen.

Pasar desde fuera también tiene un precio. No vale la pena cuando:

  • la dependencia nunca cambia y no estorba a las pruebas. Math.round no es una dependencia que se pase;
  • el programa es pequeño. El cableado a mano en el inicio del programa basta; un framework de DI es para aplicaciones grandes;
  • el constructor pide ocho dependencias. Eso no es un problema de DI: la clase hace demasiado.

Ejercicio

Black Friday

Resuelve la tarea en tu editor.

Black Friday. El 27 de noviembre de 2026 la tienda da un 30 % de descuento, y los demás viernes sigue dando un 10 %. Añade la regla a OrderService y compruébala con FakeClock para tres fechas: Black Friday, el 30 de octubre (un viernes) y el 27 de octubre (un martes). La prueba da la misma salida cualquier día.

Salida
Black Friday: 70.00 €
Viernes: 90.00 €
Martes: 100.00 €

Más tareas

  • Números de pedido. El contador de números de pedido es un Singleton y empieza en 1001. Conviértelo en una dependencia de OrderService y escribe dos pruebas que reciban 1001 como primer número.
  • Un carrito para cada cliente. Arregla del todo el error de «Encuentra el error»: el carrito no es un Singleton, y cada página recibe el suyo. Dos clientes añaden artículos distintos, y cada uno ve solo los suyos.
  • Descuento nocturno. Entre las 22:00 y las 6:00 el descuento es del 5 %. Ahora el reloj tiene que dar también la hora. ¿Qué cambias en el reloj, y cuántas pruebas necesitas para estar seguro?

Test

Ponte a prueba

1. ¿Por qué la prueba del descuento de viernes pasa solo los viernes?
2. ¿Qué cambia al pasar la dependencia desde fuera (Dependency Injection)?
3. Dos pruebas usan el mismo carrito, un Singleton. La primera añade dos artículos, la segunda añade uno y espera 1. ¿Qué pasa?
4. ¿Cuándo encaja un Singleton?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·