Día 10 de 30 Fase 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.

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 dependencias java
public class TareaService {
    // La clase decide la implementación, su configuración y su ciclo de vida
    private final TareaRepositoryJdbc repo = new TareaRepositoryJdbc(
        "jdbc:postgresql://localhost:5432/taskflow", "admin", "secreto");
    private final EmailSender email = new SmtpEmailSender("smtp.acme.com");

    public void completar(long id) {
        var tarea = 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).

Se conecta con Principios SOLIDInversión de DependenciasTesting

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. Define la interfaz PedidoRepository y haz que CatalogoEnMemoria y PedidoRepositoryJdbc la implementen.
  2. Crea PedidoService (crear, confirmar, cancelar) que recibe sus dependencias por constructor en campos final.
  3. 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.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown