Día 10 de 30Fase 2: De Vanilla a la Necesidad de Herramientas
Inyección de Dependencias Manual: El Nacimiento de Spring
El Operador new como Acoplamiento, Inversión de Control (IoC) y Ensamblado en el main()
Para dominar. Al terminar deberías poder hacerlo sin ayuda.
3.5h de estudio
5 conceptos
3 preguntas
1 katas
Java 17 LTSJava 21 LTSInversión de ControlIoCConstructor InjectionSOLID
Al terminar este día sabrás 4
Reconocer el acoplamiento que introduce new dentro de una clase de negocio.
Aplicar inversión de control e inyección por constructor sin ningún framework.
Construir a mano el grafo de objetos de una aplicación en su punto de entrada.
Diseñar contra interfaces para poder sustituir implementaciones en tests.
1
El Operador `new` como Fuente de Acoplamiento Rígido
Cuando una clase instancia a sus colaboradoras con new, queda atada a esa implementación concreta y no puede sustituirse sin modificar el código fuente.
En una línea Reemplazar el new por un parámetro en el constructor: public TareaServicio(TareaRepositorio repo).
Antes: la clase fabrica sus dependenciasjava
publicclassTareaService {
// La clase decide la implementación, su configuración y su ciclo de vidaprivatefinalTareaRepositoryJdbcrepo=newTareaRepositoryJdbc(
"jdbc:postgresql://localhost:5432/taskflow", "admin", "secreto");
privatefinalEmailSenderemail=newSmtpEmailSender("smtp.acme.com");
publicvoidcompletar(long id) {
vartarea= repo.buscar(id).orElseThrow();
tarea.completar(Clock.systemUTC()); // otra dependencia oculta: el reloj
repo.guardar(tarea);
email.enviar(tarea.responsable(), "Tarea completada");
}
}
// Para probar completar() necesitas PostgreSQL y un servidor SMTP reales.
Cómo funciona por dentro
Cuando una clase hace new RepositorioPostgres() dentro de sí misma, decide tres cosas que no le corresponden: qué implementación concreta usar, cómo construirla (con qué URL, usuario, pool) y cuánto vive ese objeto. Cambiar cualquiera de esas decisiones obliga a modificar la clase, y probarla sin una base de datos real se vuelve imposible.
Ese es el acoplamiento rígido: la clase depende de un detalle concreto en lugar de una capacidad. Se nota en los tests (no puedes sustituir el colaborador), en la configuración (las credenciales acaban dentro del código) y en los cambios (pasar de PostgreSQL a otro almacenamiento toca la lógica de negocio).
new no es malo en sí. Crear objetos de valor, DTOs, colecciones o entidades con new es perfectamente normal. El problema es usarlo para obtener servicios o recursos externos.
Impide probar la clase en aislamiento y dificulta cambiar de infraestructura (e.g. cambiar base de datos).
Hacer this.repositorio = new TareaJdbcRepositorio() dentro del constructor del servicio.
Universal en todas las versiones de Java.
Se conecta conPrincipios SOLIDInversión de DependenciasTesting
2
Inversión de Control (IoC): "No me llames, yo te llamo"
Principio en el cual el flujo de control se invierte: la clase no busca ni crea sus dependencias; alguien externo se las proporciona.
En una líneaPasar un repositorio mock en tests unitarios y el repositorio real en producción.
Inversión de control en código que ya conocesjava
// Tú no recorres la lista comparando: le das a sort() la regla y él te llama
tareas.sort(Comparator.comparing(Tarea::prioridad).reversed());
// Un "mini framework": controla el flujo y llama a tus pasosabstractclassImportador<T> {
publicfinalvoidimportar(Path fichero)throws IOException { // el flujo es suyotry (varlineas= Files.lines(fichero)) {
lineas.skip(1)
.map(this::parsear) // te llama a ti
.filter(this::esValido) // y aquí
.forEach(this::guardar); // y aquí
}
}
protectedabstractTparsear(String linea);
protectedbooleanesValido(T elemento) { returntrue; }
protectedabstractvoidguardar(T elemento);
}
Cómo funciona por dentro
Inversión de control significa que tu código deja de dirigir el flujo y la creación de objetos, y otra pieza lo hace por él. Es el principio de Hollywood: «no nos llames, ya te llamaremos». Lo usas a diario sin darte cuenta: un Comparator que le pasas a sort, un listener de un botón o un método @Test que JUnit invoca.
La inyección de dependencias es una forma concreta de inversión de control aplicada a la creación de objetos: la clase declara qué necesita y alguien externo se lo entrega ya construido. Ese «alguien» puede ser tu método main o un contenedor como Spring.
La diferencia con un framework y una librería está precisamente aquí: tú llamas a una librería; un framework te llama a ti. Spring es un framework porque es él quien crea tus objetos y decide cuándo invocar tus métodos.
Convierte el código en piezas modulares y fácilmente intercambiables.
Confundir Inversión de Control (principio general) con Inyección de Dependencias (técnica concreta).
Idéntico en 17 y 21.
Se conecta conInyección de DependenciasSpring FrameworkDesacoplamiento
3
Inyección por Constructor e Inmutabilidad con `final`
Declarar los colaboradores como campos private final y recibirlos en el constructor.
En una líneaprivate final Repositorio repo; public Servicio(Repositorio repo) { this.repo = repo; }
Con inyección por constructor, todas las dependencias obligatorias aparecen en la firma del constructor y se guardan en campos final. Eso garantiza tres cosas: el objeto nunca existe a medio configurar, sus dependencias no cambian después de construirlo (lo que lo hace seguro entre hilos) y cualquiera que lea la clase ve de un vistazo qué necesita.
Es también una alarma de diseño gratuita. Si un constructor necesita siete u ocho parámetros, la clase tiene demasiadas responsabilidades y conviene partirla. Con inyección por campo ese síntoma queda escondido.
Para dependencias opcionales, en lugar de setters suele ser mejor un valor por defecto razonable (por ejemplo, un Clock que por defecto es Clock.systemUTC()) con un segundo constructor para tests.
Garantiza que la clase nunca nazca en un estado incompleto y sea intrínsecamente segura ante hilos concurrentes.
Usar inyección por setter o por campo privado con @Autowired, permitiendo que el objeto se quede sin dependencias.
Idéntico en 17 y 21.
Se conecta conThread-SafetyClean CodeSpring Idiomático
4
El Árbol de Dependencias y el Método main()
En una app sin framework, el método main() actúa como el contenedor manual: crea los repositorios, luego los servicios pasándoles los repositorios, y luego los controladores.
En una líneavar repo = new Repo(); var serv = new Serv(repo); en la clase principal.
Raíz de composición escrita a manojava
publicclassTaskFlowApp {
publicstaticvoidmain(String[] args) {
// 1. Infraestructuravarconfig= Configuracion.desdeVariablesDeEntorno();
vardataSource=newHikariDataSource(config.hikari());
varreloj= Clock.systemUTC();
// 2. AdaptadoresTareaRepositoryrepo=newTareaRepositoryJdbc(dataSource);
Notificadornotificador= config.emailActivo()
? newNotificadorEmail(config.smtp())
: newNotificadorConsola();
// 3. Casos de usovartareaService=newTareaService(repo, notificador, reloj);
// 4. Entradavarservidor=newServidorHttp(config.puerto(), newTareaController(tareaService));
Runtime.getRuntime().addShutdownHook(newThread(() -> {
servidor.parar();
dataSource.close(); // los recursos se cierran en orden inverso
}));
servidor.arrancar();
}
}
Cómo funciona por dentro
Si ninguna clase crea sus propias dependencias, alguien tiene que hacerlo. Ese lugar se llama raíz de composición (composition root) y debe ser único y estar lo más cerca posible del punto de entrada: normalmente el método main.
Allí se crean primero los objetos sin dependencias (configuración, DataSource, reloj), luego los que dependen de ellos (repositorios), después los servicios y por último los controladores o el punto de entrada. El resultado es un árbol (técnicamente un grafo dirigido) de objetos.
Hacerlo a mano en un proyecto pequeño es un ejercicio excelente: verás exactamente qué hace un contenedor de inyección. También verás el coste: con decenas de clases, mantener el orden de creación, los singletons compartidos y el cierre de recursos se vuelve tedioso. Spring automatiza justamente este método main.
Visualizar este árbol permite entender exactamente qué hace Spring de forma automática mediante reflexión.
Dispersar la creación de instancias por toda la aplicación en lugar de centralizarla en el punto de entrada.
Idéntico en 17 y 21.
Se conecta conApplicationContextContenedor de BeansSpring Core
5
Programar contra Interfaces, no contra Implementaciones
El servicio debe depender de la interfaz TareaRepositorio, nunca de la clase concreta TareaJdbcRepositorio.
En una líneapublic interface TareaRepositorio { void guardar(Tarea t); }
Contrato, dos implementaciones y un doble de testjava
Programar contra interfaces significa que las clases de negocio dependen de un contrato (TareaRepository, Notificador) y no de una implementación (TareaRepositoryJdbc, NotificadorEmail). La interfaz describe qué se puede hacer, en términos del dominio, sin decir cómo.
El beneficio inmediato son los tests: puedes pasar una implementación en memoria o un doble de prueba sin tocar el servicio. El beneficio a medio plazo es la flexibilidad: cambiar de base de datos, de proveedor de correo o añadir una caché es escribir una nueva implementación, no modificar la lógica.
No hace falta una interfaz para cada clase. Tiene sentido cuando hay o puede haber varias implementaciones, cuando la dependencia toca el mundo exterior (red, disco, reloj, base de datos) o cuando quieres aislar un módulo. Una interfaz con una única implementación interna y sin intención de sustituirla solo añade ruido.
Permite cambiar la base de datos o simularla en pruebas con cero impacto en la lógica de negocio.
Declarar private TareaJdbcRepositorio repo; en el servicio.
Idéntico en 17 y 21.
Se conecta conPolimorfismoPrincipio de Inversión de DependenciasDIP
Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.
Bloque 1: El Acoplamiento del new (0 - 25 min)
Qué estudiar. Principio de Inversión de Dependencias (D de SOLID) e interfaces.
Pregunta a responder. ¿Cómo desacoplar un servicio de facturación de un notificador por email?
Resultado esperado. Interfaz Notificador y clase FacturacionServicio recibiéndola por constructor.
Bloque 2: Ensamblado Artesanal en main() (25 - 50 min)
Qué estudiar. Construcción manual del árbol de objetos en la clase principal.
Pregunta a responder. ¿En qué orden deben instanciarse las capas (repositorio -> servicio -> controlador)?
Resultado esperado. Método main() que ensambla 4 clases sin ningún framework.
Bloque 3: Testeabilidad Inmediata (50 - 80 min)
Qué estudiar. Escribir tests unitarios instanciando el servicio con new Servicio(fakeRepo).
Pregunta a responder. ¿Por qué este test corre en 3 milisegundos?
Resultado esperado. Test JUnit demostrando que no se requiere base de datos ni Spring para probar el servicio.
Cierre y Puente a Spring (80 - 90 min)
Qué estudiar. Preparación para el Día 7: ¿cómo Spring automatizará este cableado?
Pregunta a responder. ¿Qué anotaciones de Spring reemplazarán el código que escribimos hoy?
Resultado esperado. Anotación en el cuaderno de notas personal.
Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.
Explícalo desde cero
Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.
Por qué Spring nació por culpa del operador new
Lo que debes entender
Cuando un proyecto crece a 200 clases, ensamblar a mano el método main() requiere cientos de líneas de código frágil y repetitivo. Spring nació para ser un "ensamblador automático" de objetos.
Analogía para contarlo
Montar un coche a mano apretando cada tornillo (ensamblado manual en main) frente a una línea de fábrica robotizada que coloca el motor, luego el chasis y luego las ruedas automáticamente (Spring IoC).
Lo que suele explicarse mal
Presentar Spring como "un framework de anotaciones" sin explicar que su corazón es simplemente un contenedor que hace los new por ti en el orden correcto.
Señal de que aún no lo dominas
Si crees que necesitas Spring obligatoriamente para hacer inyección de dependencias.
Preguntas trampa
Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.
Nivel 1 (Básico)¿Por qué la inyección por constructor es superior a la inyección por atributos privados (@Autowired private Repo repo;)?Ver respuesta
Respuesta razonada
Varias razones que se refuerzan. Con constructor, los campos pueden ser final: el objeto es inmutable en sus dependencias y seguro entre hilos. Las dependencias obligatorias son explícitas y el objeto no puede existir sin ellas; con @Autowired en un campo, si creas la clase con new (por ejemplo en un test) el campo queda null y falla más tarde con un NullPointerException.
También facilita los tests: basta con new TareaService(repoFalso, notificadorFalso, relojFijo), sin contexto de Spring ni reflexión. Y destapa problemas de diseño: un constructor con muchos parámetros grita que la clase hace demasiado, y las dependencias circulares fallan al arrancar en lugar de esconderse. Por eso Spring recomienda la inyección por constructor y, desde Spring 4.3, no hace falta @Autowired si hay un único constructor.
Qué está evaluando la pregunta
Porque la inyección por constructor permite declarar los campos como final (inmutables), garantiza que la clase nunca se instancie en un estado incompleto y permite probarla con new Servicio(mock) en tests unitarios puros sin levantar Spring.
Nivel 2 (Intermedio)¿Qué es una dependencia circular y por qué el operador new en constructores la hace imposible de resolver?Ver respuesta
Respuesta razonada
Hay dependencia circular cuando A necesita a B para construirse y B necesita a A. Si cada clase hace new de la otra en su constructor, crear una A crea una B, que crea otra A, que crea otra B... hasta un StackOverflowError, porque nunca se termina de construir ninguna.
Con inyección por constructor el problema se hace visible: no hay orden posible para crear los objetos. Spring falla al arrancar con BeanCurrentlyInCreationException y, desde Spring Boot 2.6, prohíbe los ciclos por defecto incluso con inyección por campo. La solución casi siempre es de diseño: extraer la parte común a un tercer componente del que dependan ambos, o desacoplar con eventos para que uno publique y el otro escuche.
Qué está evaluando la pregunta
Si A necesita a B en su constructor y B necesita a A, para crear A necesitas B, pero para crear B necesitas A. Es un bloqueo mutuo irresoluble en instanciación por constructor.
Nivel 3 (Entrevista Senior)Si la inyección por constructor es tan buena, ¿por qué los frameworks de testing usan librerías como Mockito?Ver respuesta
Respuesta razonada
Porque la inyección por constructor permite sustituir dependencias, pero alguien tiene que escribir esos sustitutos. Para un repositorio sencillo, una implementación en memoria escrita a mano es a menudo la mejor opción: es legible y reutilizable. Para interfaces grandes, o cuando solo te interesa una interacción concreta, escribir la clase entera es tedioso.
Mockito genera esos dobles en tiempo de ejecución: when(repo.buscar(1L)).thenReturn(Optional.of(tarea)) para fijar respuestas y verify(notificador).avisar(eq("ana"), anyString()) para comprobar interacciones. No compite con la inyección por constructor, la necesita: new TareaService(mock(TareaRepository.class), ...) solo es posible porque las dependencias entran por el constructor. El riesgo es abusar de verify y acabar probando cómo está implementado el código en lugar de qué hace.
Qué está evaluando la pregunta
Mockito genera implementaciones dinámicas (proxies en tiempo de ejecución) de las interfaces de tus dependencias para que puedas inyectarlas por constructor en tus tests sin escribir clases falsas a mano.
Reto del tutor
Un desarrollador propone: "En lugar de pasar el repositorio por constructor, voy a crear un RepositorioSingleton.getInstance() estático accesible desde cualquier clase". Evalúa esta propuesta arquitectónica.
Lo que una respuesta suele tener bien
Reconoce que el patrón Singleton resuelve el problema de tener una sola instancia en memoria.
Lo que suele quedarse en la superficie
Decir que "los Singletons son malos".
El concepto de fondo que suele faltar
No entender que el acceso estático global introduce un acoplamiento invisible y oculta las dependencias de la clase, haciendo imposible sustituir el repositorio por un mock en tests unitarios.
Cómo lo respondería alguien senior en una entrevista
El patrón Singleton estático clásico es un anti-patrón en arquitecturas modernas: (1) Oculta las dependencias; (2) Introduce estado mutable global difícil de depurar; (3) Impide ejecutar tests unitarios en paralelo; (4) La solución idiomática es el Singleton gestionado por el contenedor (como hace Spring): la clase sigue teniendo dependencias explícitas en su constructor, pero el contenedor le inyecta una única instancia compartida.
Proyecto «Pedidos» · paso 10 de 30
Separar capas con inyección de dependencias
Que la lógica de negocio no sepa si los pedidos se guardan en memoria o en una base de datos.
De qué va el proyecto
El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.
Qué hacer
Define la interfaz PedidoRepository y haz que CatalogoEnMemoria y PedidoRepositoryJdbc la implementen.
Crea PedidoService (crear, confirmar, cancelar) que recibe sus dependencias por constructor en campos final.
Ensambla todas las piezas a mano en un método main.
Está listo cuando
Cambiar de repositorio en memoria a JDBC solo exige tocar el main.
PedidoService no contiene ningún new de sus dependencias.
Katas: ejercicios cortos
Reglas de oro
Programa siempre contra interfaces, nunca contra clases concretas.
Declara siempre tus dependencias como private final en el constructor.
Evita los Singletons estáticos globales (getInstance()); prefiere inyección de dependencias.