Apuntes DAM

Patrón Inyección de dependencias

Una clase recibe los objetos que necesita por el constructor en vez de crearlos con new, y así se pueden cambiar por otros (dobles en las pruebas). Spring, Hilt y ASP.NET Core lo hacen solos.

nivel intermedioTambién: dependency injection, DI, inversión de control, IoC, contenedor

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Inyección de dependencias

Elige qué notificador inyecta el ensamblador y los correos que se matriculan, separados por comas.

Paso 1

El ensamblador (el main, o Spring en una aplicación de verdad) elige la pieza de producción: el notificador que envía correos.

1Notificador n = new NotificadorEmail();              // el ensamblador elige  // matriculados = 0, avisos = 0
2ServicioMatricula s = new ServicioMatricula(n);
3
4ServicioMatricula(Notificador notificador) {
5    this.notificador = notificador;
6}
7
8boolean matricular(String email) {
9    if (!matriculados.add(email)) return false;
10    notificador.avisar(email, "Bienvenido al ciclo");
11    return true;
12}

Variables

matriculados
0
avisos
0

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Si ServicioMatricula hace new NotificadorEmail() dentro, está atado a enviar correos de verdad: no se puede probar sin mandar emails, ni cambiar a SMS sin tocar la clase. La clase decide qué usa y además lo crea, dos responsabilidades que no le tocan.

Con inyección de dependencias, la clase declara lo que necesita (una interfaz Notificador) y lo recibe desde fuera, normalmente en el constructor. Quien la crea elige la pieza: el correo de verdad en producción, un NotificadorFalso que apunta los avisos en una prueba. La clase no cambia; cambia lo que se le pasa.

En una aplicación grande no se ensambla a mano: un contenedor (Spring, Hilt en Android, el de ASP.NET Core o el de Laravel) sabe cómo crear cada pieza, si se comparte una sola o se crea una cada vez, y las pasa a los constructores. Es la «inversión de control»: la clase ya no controla cómo se crean sus dependencias.

Cuándo usarlo

  • Casi siempre que una clase usa servicios externos: bases de datos, correo, APIs, el reloj del sistema, ficheros.
  • Quieres probar una clase sin sus dependencias reales (sin base de datos, sin enviar correos).
  • La misma clase debe funcionar con piezas distintas según la configuración (correo o SMS, MySQL o memoria).

Cuándo no

  • Para objetos de datos simples (un record Alumno): se crean con new y ya está.
  • Para utilidades sin estado ni efectos (Math.max): no hay nada que sustituir.

Participantes

  1. Dependencia (interfaz). Lo que la clase necesita, descrito como interfaz (Notificador).
  2. Implementaciones. Las piezas reales y los dobles (NotificadorEmail, NotificadorFalso).
  3. Cliente. La clase que recibe la dependencia en el constructor y la guarda (ServicioMatricula).
  4. Ensamblador. Quien crea los objetos y decide qué pieza va con cada uno: el main, una fábrica o un contenedor como Spring.

Diagrama de clases

1notificadorcrea e inyectaelige«interface»Notificador+avisar(destino : String, texto : String) : voidNotificadorEmailNotificadorFalso+enviados : List<String>ServicioMatricula-notificador : Notificador+ServicioMatricula(notificador : Notificador)+matricular(email : String) : booleanMain
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2interface Notificador {
3  +avisar(destino : String, texto : String) : void
4}
5class NotificadorEmail
6class NotificadorFalso {
7  +enviados : List<String>
8}
9class ServicioMatricula {
10  -notificador : Notificador
11  +ServicioMatricula(notificador : Notificador)
12  +matricular(email : String) : boolean
13}
14class Main
15Notificador <|.. NotificadorEmail
16Notificador <|.. NotificadorFalso
17ServicioMatricula --> "1" Notificador : notificador
18Main ..> ServicioMatricula : crea e inyecta
19Main ..> NotificadorEmail : elige
20@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

El mismo servicio en producción y en una prueba

ServicioMatricula no cambia: en producción recibe el notificador de correo y en la prueba uno falso que apunta lo que habría enviado.

Java
1import java.util.*;
2
3interface Notificador {
4    void avisar(String destino, String texto);
5}
6
7class NotificadorEmail implements Notificador {
8    public void avisar(String destino, String texto) { System.out.println("  [email a " + destino + "] " + texto); }
9}
10
11/** Un doble para las pruebas: no envía nada, apunta lo que habría enviado. */
12class NotificadorFalso implements Notificador {
13    final List<String> enviados = new ArrayList<>();
14    public void avisar(String destino, String texto) { enviados.add(destino + ": " + texto); }
15}
16
17/** Recibe lo que necesita en el constructor: no hace new de ninguna de sus dependencias. */
18class ServicioMatricula {
19    private final Notificador notificador;
20    private final Set<String> matriculados = new HashSet<>();
21
22    ServicioMatricula(Notificador notificador) {      // inyección por constructor
23        this.notificador = notificador;
24    }
25
26    boolean matricular(String email) {
27        if (!matriculados.add(email)) return false;     // ya estaba
28        notificador.avisar(email, "Bienvenido al ciclo");
29        return true;
30    }
31}
32
33public class Main {
34    public static void main(String[] args) {
35        // El «ensamblador» (aquí el main; en Spring, el contenedor) elige qué piezas se usan
36        System.out.println("En producción:");
37        ServicioMatricula real = new ServicioMatricula(new NotificadorEmail());
38        real.matricular("ana@correo.es");
39
40        System.out.println("En una prueba:");
41        NotificadorFalso falso = new NotificadorFalso();
42        ServicioMatricula probado = new ServicioMatricula(falso);
43        probado.matricular("luis@correo.es");
44        probado.matricular("luis@correo.es");
45        System.out.println("  Avisos que habría enviado: " + falso.enviados.size() + " → " + String.join(", ", falso.enviados));
46    }
47}

Salida al ejecutarlo (la misma en los 5 lenguajes)

En producción:
  [email a ana@correo.es] Bienvenido al ciclo
En una prueba:
  Avisos que habría enviado: 1 → luis@correo.es: Bienvenido al ciclo

Spring y las pruebas con Mockito

El contenedor de Spring inyecta las dependencias por el constructor; en las pruebas se inyecta un doble.

Java
1// Spring crea los objetos (beans) y les pasa sus dependencias por el constructor
2@Component
3public class NotificadorEmail implements Notificador { /* ... */ }
4
5@Service
6public class ServicioMatricula {
7    private final Notificador notificador;
8    private final AlumnoRepository alumnos;
9
10    // Con un solo constructor, Spring lo usa para inyectar; no hace falta ni @Autowired
11    public ServicioMatricula(Notificador notificador, AlumnoRepository alumnos) {
12        this.notificador = notificador;
13        this.alumnos = alumnos;
14    }
15}
16
17// En una prueba con JUnit y Mockito se inyecta un doble en lugar del real
18@Test
19void avisaAlMatricular() {
20    Notificador falso = mock(Notificador.class);
21    new ServicioMatricula(falso, repositorioEnMemoria).matricular("ana@correo.es");
22    verify(falso).avisar(eq("ana@correo.es"), anyString());
23}

En la práctica

  • Spring y Spring Boot (@Service, @Component, @Repository e inyección por constructor).
  • Hilt y Dagger en Android; el contenedor de ASP.NET Core (builder.Services.AddScoped<...>()).
  • El contenedor de servicios de Laravel y Symfony, que inyecta en los constructores de los controladores.
  • Las pruebas unitarias con dobles (Mockito, Moq, unittest.mock): solo son posibles si las dependencias se inyectan.

Errores típicos

  • Hacer new de la dependencia dentro de la clase «por comodidad»: se pierde todo lo ganado.
  • Inyectar la clase concreta (NotificadorEmail) en vez de la interfaz: no se puede sustituir.
  • Pasar el contenedor entero a las clases para que pidan lo que quieran (service locator): las dependencias dejan de verse en el constructor.
  • Constructores con diez dependencias: es una señal de que la clase hace demasiadas cosas.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Una calculadora que no elige sus piezas

CalculadoraPrecio recibe el impuesto y el descuento en el constructor, pero los ignora y crea los suyos (IVA de España y sin descuento). Por eso cambiar de zona o de descuento en el main no sirve de nada. Arréglala para que use las dependencias que le inyectan.

  • Órdenes: zona es|pt|canarias, descuento ninguno|joven|cuponN (por ejemplo cupon5 quita 5 €) y precio base.
  • Salida de precio: base € → final € (impuesto, descuento). Primero se aplica el impuesto y después el descuento.
JavaUna calculadora que no elige sus piezasFácil

Ejemplo

Entrada (lo que se escribe por teclado)
precio 100
zona pt
precio 100
descuento joven
precio 100
zona canarias
descuento cupon5
precio 20
Salida esperada
100,00 € → 121,00 € (IVA 21 %, sin descuento)
100,00 € → 123,00 € (IVA 23 %, sin descuento)
100,00 € → 104,55 € (IVA 23 %, carné joven -15 %)
20,00 € → 16,40 € (IGIC 7 %, cupón -5,00 €)
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.math.*;
2import java.util.*;
3
4interface Impuesto {
5    String nombre();
6    double aplicar(double base);
7}
8
9class IvaEspana implements Impuesto {
10    public String nombre() { return "IVA 21 %"; }
11    public double aplicar(double base) { return base * 1.21; }
12}
13
14class IvaPortugal implements Impuesto {
15    public String nombre() { return "IVA 23 %"; }
16    public double aplicar(double base) { return base * 1.23; }
17}
18
19class IgiCanarias implements Impuesto {
20    public String nombre() { return "IGIC 7 %"; }
21    public double aplicar(double base) { return base * 1.07; }
22}
23
24interface Descuento {
25    String nombre();
26    double aplicar(double precio);
27}
28
29class SinDescuento implements Descuento {
30    public String nombre() { return "sin descuento"; }
31    public double aplicar(double precio) { return precio; }
32}
33
34class DescuentoJoven implements Descuento {
35    public String nombre() { return "carné joven -15 %"; }
36    public double aplicar(double precio) { return precio * 0.85; }
37}
38
39class DescuentoFijo implements Descuento {
40    private final double euros;
41    DescuentoFijo(double euros) { this.euros = euros; }
42    public String nombre() { return "cupón -" + Main.euros(euros); }
43    public double aplicar(double precio) { return Math.max(0, precio - euros); }
44}
45
46/** Calcula precios con el impuesto y el descuento que le inyectan: no elige ninguno. */
47class CalculadoraPrecio {
48    private final Impuesto impuesto;
49    private final Descuento descuento;
50
51    CalculadoraPrecio(Impuesto impuesto, Descuento descuento) {
52        this.impuesto = impuesto;
53        this.descuento = descuento;
54    }
55
56    double precio(double base) {
57        return descuento.aplicar(impuesto.aplicar(base));       // primero el impuesto, después el descuento
58    }
59
60    String detalle() { return impuesto.nombre() + ", " + descuento.nombre(); }
61}
62
63public class Main {
64    static String euros(double x) {
65        return BigDecimal.valueOf(x).setScale(2, RoundingMode.HALF_UP).toPlainString().replace('.', ',') + " €";
66    }
67
68    public static void main(String[] args) {
69        Impuesto impuesto = new IvaEspana();
70        Descuento descuento = new SinDescuento();
71        Scanner sc = new Scanner(System.in);
72        while (sc.hasNextLine()) {
73            String[] p = sc.nextLine().trim().split("\\s+");
74            if (p[0].isEmpty()) continue;
75            try {
76                switch (p[0] + "/" + (p.length - 1)) {
77                    case "zona/1" -> impuesto = switch (p[1]) {
78                        case "es" -> new IvaEspana();
79                        case "pt" -> new IvaPortugal();
80                        case "canarias" -> new IgiCanarias();
81                        default -> throw new IllegalArgumentException("Zona desconocida: " + p[1]);
82                    };
83                    case "descuento/1" -> descuento = switch (p[1]) {
84                        case "ninguno" -> new SinDescuento();
85                        case "joven" -> new DescuentoJoven();
86                        default -> {
87                            if (!p[1].startsWith("cupon")) throw new IllegalArgumentException("Descuento desconocido: " + p[1]);
88                            yield new DescuentoFijo(Double.parseDouble(p[1].substring(5)));
89                        }
90                    };
91                    case "precio/1" -> {
92                        // El main es el «ensamblador»: crea la calculadora con las piezas elegidas
93                        CalculadoraPrecio c = new CalculadoraPrecio(impuesto, descuento);
94                        System.out.println(euros(Double.parseDouble(p[1])) + " → " + euros(c.precio(Double.parseDouble(p[1]))) + " (" + c.detalle() + ")");
95                    }
96                    default -> System.out.println("Orden no válida: " + String.join(" ", p));
97                }
98            } catch (NumberFormatException e) {
99                System.out.println("Orden no válida: " + String.join(" ", p));
100            } catch (IllegalArgumentException e) {
101                System.out.println(e.getMessage());
102            }
103        }
104    }
105}

La calculadora ya no decide nada: la configuración la elige quien la crea. Para Andorra bastaría con otra clase Impuesto.

Probarla es fácil: se le inyecta un impuesto o un descuento de prueba y se comprueba el resultado.

2. Un contenedor de dependencias

Escribe un contenedor de dependencias mínimo, como el de Spring: registrar guarda cómo crear un tipo (un objeto nuevo cada vez), singleton guarda cómo crearlo pero lo comparte (se crea la primera vez que se pide) y obtener devuelve el objeto que toca o lanza una excepción si el tipo no está registrado.

  • El main registra un registro de mensajes, un reloj y un servicio de avisos que recibe los dos en su constructor, y comprueba si el contenedor devuelve el mismo objeto o uno nuevo. Al final cuenta cuántos registros y servicios se han creado.
  • obtener de un tipo sin registrar lanza IllegalStateException con No hay nada registrado para Tipo (el nombre simple de la clase). Volver a registrar un tipo sustituye lo anterior.
JavaUn contenedor de dependenciasMedio

Ejemplo

Entrada (lo que se escribe por teclado)
singleton registro
singleton reloj
registrar servicio
mismo registro
mismo servicio
avisar Examen el lunes
avisar Entrega del proyecto
registro
Salida esperada
¿Mismo registro? sí
¿Mismo servicio? no
Avisado: Examen el lunes
Avisado: Entrega del proyecto
Registro: 10:00 Examen el lunes | 10:01 Entrega del proyecto
Registros creados: 1 · servicios creados: 4
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2import java.util.function.Supplier;
3
4/** Un contenedor de dependencias mínimo: sabe cómo crear cada tipo y si se comparte. */
5class Contenedor {
6    private final Map<Class<?>, Supplier<?>> fabricas = new HashMap<>();
7    private final Map<Class<?>, Object> compartidos = new HashMap<>();
8    private final Set<Class<?>> unicos = new HashSet<>();
9
10    /** Cada obtener crea un objeto nuevo. */
11    <T> void registrar(Class<T> tipo, Supplier<? extends T> fabrica) {
12        fabricas.put(tipo, fabrica);
13        unicos.remove(tipo);
14        compartidos.remove(tipo);
15    }
16
17    /** Se crea la primera vez que se pide y después siempre se devuelve el mismo. */
18    <T> void singleton(Class<T> tipo, Supplier<? extends T> fabrica) {
19        fabricas.put(tipo, fabrica);
20        unicos.add(tipo);
21        compartidos.remove(tipo);
22    }
23
24    <T> T obtener(Class<T> tipo) {
25        Supplier<?> f = fabricas.get(tipo);
26        if (f == null) throw new IllegalStateException("No hay nada registrado para " + tipo.getSimpleName());
27        if (!unicos.contains(tipo)) return tipo.cast(f.get());
28        return tipo.cast(compartidos.computeIfAbsent(tipo, t -> f.get()));
29    }
30}
31
32// ───── Las piezas de una aplicación pequeña ─────
33interface Registro { void escribir(String s); List<String> lineas(); }
34class RegistroMemoria implements Registro {
35    static int creados = 0;
36    private final List<String> lineas = new ArrayList<>();
37    RegistroMemoria() { creados++; }
38    public void escribir(String s) { lineas.add(s); }
39    public List<String> lineas() { return lineas; }
40}
41
42interface Reloj { String ahora(); }
43class RelojFijo implements Reloj {
44    private int tic = 0;
45    public String ahora() { return String.format("10:%02d", tic++); }
46}
47
48/** Recibe sus dependencias en el constructor: el contenedor se las da. */
49class ServicioAvisos {
50    static int creados = 0;
51    private final Registro registro;
52    private final Reloj reloj;
53    ServicioAvisos(Registro registro, Reloj reloj) { this.registro = registro; this.reloj = reloj; creados++; }
54    void avisar(String texto) { registro.escribir(reloj.ahora() + " " + texto); }
55}
56
57public class Main {
58    public static void main(String[] args) {
59        Contenedor c = new Contenedor();
60        Scanner sc = new Scanner(System.in);
61        while (sc.hasNextLine()) {
62            String linea = sc.nextLine().trim();
63            if (linea.isEmpty()) continue;
64            try {
65                switch (linea) {
66                    case "registrar registro" -> c.registrar(Registro.class, RegistroMemoria::new);
67                    case "singleton registro" -> c.singleton(Registro.class, RegistroMemoria::new);
68                    case "singleton reloj" -> c.singleton(Reloj.class, RelojFijo::new);
69                    case "registrar servicio" -> c.registrar(ServicioAvisos.class, () -> new ServicioAvisos(c.obtener(Registro.class), c.obtener(Reloj.class)));
70                    case "mismo registro" -> System.out.println("¿Mismo registro? " + (c.obtener(Registro.class) == c.obtener(Registro.class) ? "sí" : "no"));
71                    case "mismo servicio" -> System.out.println("¿Mismo servicio? " + (c.obtener(ServicioAvisos.class) == c.obtener(ServicioAvisos.class) ? "sí" : "no"));
72                    case "registro" -> {
73                        List<String> l = c.obtener(Registro.class).lineas();
74                        System.out.println("Registro: " + (l.isEmpty() ? "(vacío)" : String.join(" | ", l)));
75                    }
76                    default -> {
77                        if (!linea.startsWith("avisar ")) throw new IllegalArgumentException();
78                        c.obtener(ServicioAvisos.class).avisar(linea.substring(7));
79                        System.out.println("Avisado: " + linea.substring(7));
80                    }
81                }
82            } catch (IllegalStateException e) {
83                System.out.println(e.getMessage());
84            } catch (IllegalArgumentException e) {
85                System.out.println("Orden no válida: " + linea);
86            }
87        }
88        System.out.println("Registros creados: " + RegistroMemoria.creados + " · servicios creados: " + ServicioAvisos.creados);
89    }
90}

El servicio no sabe de dónde salen su registro y su reloj: el contenedor los resuelve al crearlo.

Compartir o no una pieza es una decisión de configuración: el mismo código funciona con un registro por servicio o con uno para todos.

Test

Test: Inyección de dependencias

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Qué significa que una clase recibe sus dependencias por inyección?

  2. 2.¿Qué ventaja da para las pruebas?

  3. 3.¿De qué tipo debería ser el parámetro del constructor?

  4. 4.¿Qué hace un contenedor como Spring?

  5. 5.Un constructor necesita diez dependencias. ¿Qué indica?

Relacionado