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 {
private static final SystemClock INSTANCE = new SystemClock();
private SystemClock() {
}
public static SystemClock getInstance() {
return INSTANCE;
}
public LocalDate today() {
return LocalDate.now();
}
}
class OrderService {
public double price(double amount) {
if (SystemClock.getInstance().today().getDayOfWeek() == DayOfWeek.FRIDAY) {
return amount * 0.9;
}
return amount;
}
}

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

OrderService service = new OrderService();
System.out.printf("Descuento de viernes: esperaba 90.00 €, recibí %.2f €%n", service.price(100));

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 de tipo interfaz en el constructor;
  • 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 con if (instance == null). Si dos hilos llegan a la comprobación a la vez, obtienes dos objetos. El lenguaje tiene formas más seguras.El reloj del problema se crea en un campo static final. Java lo crea una vez, al cargar la clase, y eso es seguro también con muchos hilos. Si crearlo es caro y debe ocurrir solo en la primera petición, el objeto va a una clase anidada (un holder). Java carga Holder solo en el primer getInstance():
class SystemClock {
private SystemClock() {
System.out.println("Creo el reloj");
}
private static class Holder {
static final SystemClock INSTANCE = new SystemClock();
}
public static SystemClock getInstance() {
return Holder.INSTANCE;
}
}
System.out.println("Inicio");
SystemClock a = SystemClock.getInstance();
SystemClock b = SystemClock.getInstance();
System.out.println(a == b);
Salida
Inicio
Creo el reloj
true
El reloj se crea solo cuando alguien lo pide, y una sola vez. Las dos variables apuntan al mismo objeto.
Más precisamente, para curiosos
El Singleton seguro más corto en Java es un enum con un valor, INSTANCE (lo viste en «Patrones de diseño»). Java no permite new para un enum y crea el valor exactamente una vez.

Míralo

El reloj entra desde fuera

El servicio pide un Clock: una interfaz con un método. SystemClock del problema solo añade implements Clock:
interface Clock {
LocalDate today();
}
class OrderService {
private final Clock clock;
public OrderService(Clock clock) {
this.clock = clock;
}
public double price(double amount) {
if (clock.today().getDayOfWeek() == DayOfWeek.FRIDAY) {
return amount * 0.9;
}
return amount;
}
}

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

OrderService service = new OrderService(SystemClock.getInstance());

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.

En las aplicaciones grandes el cableado lo hace un framework: Spring crea los objetos y entrega cada uno a quien lo pida en su constructor. La idea es la misma; a mano en main es para programas pequeños.

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 implements Clock {
private LocalDate date;
public FakeClock(LocalDate date) {
this.date = date;
}
public void set(LocalDate date) {
this.date = date;
}
@Override
public LocalDate today() {
return date;
}
}

La prueba lo pasa en lugar del real:

FakeClock clock = new FakeClock(LocalDate.of(2026, 10, 30));
OrderService service = new OrderService(clock);
System.out.printf("Viernes: %.2f €%n", service.price(100));
clock.set(LocalDate.of(2026, 10, 27));
System.out.printf("Martes: %.2f €%n", service.price(100));
Salida
Viernes: 90.00 €
Martes: 100.00 €

FakeClock clock = new FakeClock(LocalDate.of(2026, 10, 30));
OrderService service = new OrderService(clock);
System.out.printf("Viernes: %.2f €%n", service.price(100));
clock.set(LocalDate.of(2026, 10, 27));
System.out.printf("Martes: %.2f €%n", service.price(100));
Paso a paso, en palabras
  1. Antes, price buscaba él mismo SystemClock.getInstance(): 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.

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
Clock tiene un método, así que un reloj de prueba para un día fijo también puede ser una lambda: new OrderService(() -> LocalDate.of(2026, 10, 30)). Una clase compensa cuando la prueba mueve la fecha, como aquí. Java también tiene un java.time.Clock integrado con Clock.fixed(...) para pruebas, y LocalDate.now(clock) le pregunta a él.

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?

FakeClock clock = new FakeClock(LocalDate.of(2026, 10, 30));
OrderService shop = new OrderService(clock);
clock.set(LocalDate.of(2026, 10, 31));
OrderService app = new OrderService(clock);
System.out.printf("%.2f €%n", shop.price(50));
System.out.printf("%.2f €%n", app.price(50));
Respuesta

Pruébalo

Encuentra el error

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

class Cart {
private static final Cart INSTANCE = new Cart();
private final List<String> items = new ArrayList<>();
private Cart() {
}
public static Cart getInstance() {
return INSTANCE;
}
public void add(String item) {
items.add(item);
}
public int size() {
return items.size();
}
}

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

CheckoutPage first = new CheckoutPage();
first.add("Café");
first.add("Taza");
check("Prueba 1", first.count(), 2);
CheckoutPage 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 main 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.
Una interfaz con una sola implementación «por si acaso» sobra. Aquí hay dos implementaciones, la real y la de prueba, y la interfaz compensa.

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 Clock, 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 ·