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{ public static readonly SystemClock Instance = new SystemClock();
private SystemClock() { }
public DateTime Today => DateTime.Today;}
class OrderService{ public decimal Price(decimal amount) { if (SystemClock.Instance.Today.DayOfWeek == DayOfWeek.Friday) { return amount * 0.9m; } return amount; }}Alguien escribe una prueba para asegurarse de que el descuento funciona:
var service = new OrderService();Console.WriteLine($"Descuento de viernes: esperaba 90.00 €, recibí {service.Price(100):F2} €");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 está en un campo static readonly. El entorno de ejecución lo crea una vez, antes de usar la clase, y eso es seguro también con muchos hilos. Si crearlo es caro y debe ocurrir solo en la primera petición, existe Lazy<T>: el objeto se crea en el primer .Value, también una sola vez:class SystemClock{ private static readonly Lazy<SystemClock> instance = new Lazy<SystemClock>(() => new SystemClock());
public static SystemClock Instance => instance.Value;
private SystemClock() { Console.WriteLine("Creo el reloj"); }}Console.WriteLine("Inicio");var a = SystemClock.Instance;var b = SystemClock.Instance;Console.WriteLine(a == b);InicioCreo el relojTrueMás precisamente, para curiosos
static readonly y Lazy<T> solo protegen de una segunda creación. No protegen de la dependencia escondida: SystemClock.Instance se puede seguir llamando desde cualquier parte.Míralo
El reloj entra desde fuera
IClock: una interfaz con una propiedad. SystemClock del problema solo añade : IClock:interface IClock{ DateTime Today { get; }}
class OrderService{ private readonly IClock clock;
public OrderService(IClock clock) { this.clock = clock; }
public decimal Price(decimal amount) { if (clock.Today.DayOfWeek == DayOfWeek.Friday) { return amount * 0.9m; } return amount; }}En el programa, el reloj real se pasa una vez, al crear el servicio:
var service = new OrderService(SystemClock.Instance);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 ASP.NET Core, pasar las dependencias lo hace el contenedor del framework. Registras el reloj una vez, builder.Services.AddSingleton<IClock>(SystemClock.Instance), y todo controlador que pida un IClock en su constructor lo recibe. Así funciona IProductService en las lecciones MVC, Vistas con Razor y WebAPI. AddSingleton significa ahí «un objeto para toda la aplicación», pero nadie lo toma por su cuenta: el framework lo pasa.
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 : IClock{ public DateTime Today { get; set; }
public FakeClock(DateTime today) { Today = today; }}La prueba lo pasa en lugar del real:
var clock = new FakeClock(new DateTime(2026, 10, 30));var service = new OrderService(clock);Console.WriteLine($"Viernes: {service.Price(100):F2} €");
clock.Today = new DateTime(2026, 10, 27);Console.WriteLine($"Martes: {service.Price(100):F2} €");Viernes: 90.00 €Martes: 100.00 €var clock = new FakeClock(new DateTime(2026, 10, 30));var service = new OrderService(clock);Console.WriteLine($"Viernes: {service.Price(100):F2} €");
clock.Today = new DateTime(2026, 10, 27);Console.WriteLine($"Martes: {service.Price(100):F2} €");Paso a paso, en palabras
- Antes, Price buscaba él mismo SystemClock.Instance: 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
TimeProvider con GetLocalNow(): el programa pasa TimeProvider.System, y las pruebas su propia subclase. La idea es la misma que con IClock.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?
var clock = new FakeClock(new DateTime(2026, 10, 30));var shop = new OrderService(clock);clock.Today = new DateTime(2026, 10, 31);var app = new OrderService(clock);
Console.WriteLine($"{shop.Price(50):F2} €");Console.WriteLine($"{app.Price(50):F2} €");Pruébalo
Encuentra el error
El carrito es un Singleton, «para que haya uno»:
class Cart{ public static readonly Cart Instance = new Cart(); private readonly List<string> items = new List<string>();
private Cart() { }
public void Add(string item) { items.Add(item); }
public int Count => items.Count;}Dos pruebas, cada una con su propia página de pago. check imprime OK o lo que la prueba esperaba:
var first = new CheckoutPage();first.Add("Café");first.Add("Taza");Check("Prueba 1", first.Count, 2);
var 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 IClock, y cuántas pruebas necesitas para estar seguro?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Clases anidadas · Lambdas y delegados · 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