Introducción: ¿Qué es Dependency Injection y por qué es necesaria en Java?
En el desarrollo moderno de Java, la limpieza del código y su testabilidad son requisitos clave para un profesional. Dependency Injection (DI), o inyección de dependencias, es un patrón de diseño que permite que tu código sea débilmente acoplado, flexible y fácil de mantener. En lugar de que un objeto cree sus propias dependencias (por ejemplo, mediante new), DI las «inyecta» desde el exterior. Esta es una parte fundamental del principio de Inversión de Control (IoC).
En este artículo, analizaremos cómo funciona Dependency Injection en Java, veremos las tres formas principales de implementarlo (a través del constructor, setters e interfaz) y mostraremos cómo se aplica DI en proyectos reales, incluyendo el popular framework Spring.
¿Por qué sin DI tu código se vuelve frágil?
Imagina la clase Car, que crea un motor internamente:
public class Car { private Engine engine;
public Car() { this.engine = new PetrolEngine(); // Acoplamiento fuerte }
public void start() { engine.run(); }}Los problemas de este enfoque son evidentes:
- Imposibilidad de reutilización: No podrás reemplazar fácilmente
PetrolEngineporElectricEngine. - Dificultad para realizar pruebas: Para las pruebas unitarias, tendrías que crear un motor real, no un simulacro (mock).
- Violación del SRP: La clase
Carno solo se encarga de gestionar el coche, sino también de crear el motor.
Dependency Injection resuelve estos problemas delegando la creación de dependencias a un código externo (un contenedor u otra clase).
Tres formas de inyectar dependencias en Java
En Java, existen tres enfoques principales para implementar DI. Veamos cada uno con el ejemplo de un sistema de notificaciones.
1. Inyección a través del constructor (Constructor Injection)
Esta es la forma más preferida. Las dependencias se pasan a través de los parámetros del constructor. Esto garantiza que el objeto siempre se cree en un estado válido y que las dependencias no puedan modificarse después de la creación (inmutabilidad).
public interface MessageService { void sendMessage(String message, String recipient);}
public class EmailService implements MessageService { @Override public void sendMessage(String message, String recipient) { System.out.println("Email sent to " + recipient + ": " + message); }}
public class NotificationSender { private final MessageService messageService;
// Inyección de dependencia a través del constructor public NotificationSender(MessageService messageService) { this.messageService = messageService; }
public void notifyUser(String userEmail, String msg) { messageService.sendMessage(msg, userEmail); }}
// Usopublic class Main { public static void main(String[] args) { MessageService emailService = new EmailService(); NotificationSender sender = new NotificationSender(emailService); // DI sender.notifyUser("user@example.com", "Hello!"); }}2. Inyección a través de setters (Setter Injection)
Es adecuado para dependencias opcionales o cuando la dependencia puede cambiar durante la vida del objeto. Sin embargo, el objeto podría crearse en un estado incompleto si se olvida llamar al setter.
public class NotificationSender { private MessageService messageService;
// Setter para inyectar la dependencia public void setMessageService(MessageService messageService) { this.messageService = messageService; }
public void notifyUser(String userEmail, String msg) { if (messageService != null) { messageService.sendMessage(msg, userEmail); } else { System.o