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:
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
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);InicioCreo el relojtrueMás precisamente, para curiosos
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
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));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
- Antes, price buscaba él mismo SystemClock.getInstance(): un vínculo escondido con un objeto global que la prueba no puede cambiar.
- La prueba crea su propio reloj: un FakeClock que dice 2026-10-30, viernes.
- El reloj entra desde fuera, por el constructor. OrderService solo guarda una referencia a él.
- price(100): el servicio le pregunta a su reloj. Es viernes, así que el importe es 90.00.
- La prueba mueve la fecha al 2026-10-27, un martes. En OrderService no cambia nada.
- 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));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);Prueba 1: OKPrueba 2: FAIL, esperaba 1, recibí 3Cada prueba pasa por separado. Juntas, la segunda falla. ¿En qué línea está el error?
Elige la línea con el error.
La página toma por sí misma el único carrito. Las dos páginas, y las dos pruebas, llenan el mismo objeto: la segunda prueba ve el café y la taza de la primera. En la tienda es aún peor: todos los clientes compran en un solo carrito. Un carrito no es algo de lo que haya uno en el programa; hay uno por cliente. La solución: el carrito es una clase normal con constructor público, y la página lo recibe en el suyo: new CheckoutPage(new Cart()). Cada prueba crea su propio carrito, y las dos pasan.
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.roundno 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.
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.
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
OrderServicey 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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Clases anidadas · Lambdas · Patrones de diseño · Strategy
- Continúa en: Adapter y Facade · Repository · La tienda: qué patrón dónde · SOLID sin dogma
- Ejercicios sobre el tema: Patrones de diseño