Dependency Injection (inyección de dependencias) suena complicado, pero es simplemente un término de moda para pasar un objeto (dependencia) a otro objeto. Si lo decimos más sencillo, DI es la manera en la que un objeto (llamémoslo consumidor) obtiene todas las dependencias que necesita para hacer su trabajo.
Imagina un restaurante. Cocinar los platos es responsabilidad del chef. Claro, él no va a salir al mercado a buscar las mejores verduras, ni a criar vacas para el filete. Eso lo hacen otros especialistas, y el restaurante le proporciona al chef todos los ingredientes necesarios. En programación funciona de forma parecida: un objeto no debería crear sus propias dependencias — eso debe hacerse desde fuera.
La inyección de dependencias (DI) hace el código:
- Flexible: si de repente necesitas cambiar una dependencia (por ejemplo, cambiar de una base de datos a otra), puedes hacerlo sin reescribir toda la lógica.
- Fácil de probar: puedes sustituir fácilmente las dependencias por "mocks" para escribir tests.
- Fácil de mantener: los cambios en una dependencia afectan mínimamente al resto del código.
El problema del enfoque tradicional
Veamos un ejemplo sin DI:
public class OrderService {
private final PaymentService paymentService;
public OrderService() {
this.paymentService = new PaymentService(); // Nosotros mismos creamos la instancia de la dependencia
}
public void placeOrder() {
paymentService.processPayment();
}
}
¿Qué está mal aquí? Bueno, al menos:
OrderServicedepende de una implementación concreta dePaymentService. Si necesitamos usar otroPaymentService, tendremos que cambiar el código.- Probar
OrderServicese complica porque no podemos reemplazarPaymentServicepor un stub o mock.
¿Cómo resuelve DI el problema?
DI soluciona esto pasando las dependencias desde fuera:
public class OrderService {
private final PaymentService paymentService;
// La dependencia se pasa por el constructor desde afuera
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
Ahora podemos pasar cualquier implementación de PaymentService (por ejemplo, CreditCardPaymentService o PayPalPaymentService), lo que hace el código más flexible.
Ventajas de usar DI
Testabilidad
Con DI puedes reemplazar fácilmente dependencias reales por mocks para las pruebas. Por ejemplo:
PaymentService mockPaymentService = Mockito.mock(PaymentService.class);
OrderService orderService = new OrderService(mockPaymentService);
Mantenibilidad
Cuando los objetos no crean sus dependencias por sí mismos, es más fácil reemplazarlas en el futuro. Por ejemplo, si necesitas añadir una nueva implementación de PaymentService, puedes hacerlo sin tocar OrderService.
Escalabilidad
DI ayuda a escalar las aplicaciones. Imagínate que en lugar de 10 clases tienes 1000, y todas crean sus dependencias "a mano". Gestionarlo se volvería una pesadilla. DI lo maneja bien y te libera de la rutina de gestionar dependencias.
Formas de inyectar dependencias
Spring Framework soporta tres formas principales de DI:
- Por constructor
- Por setter
- Por campo
Las veremos con más detalle en la siguiente clase, pero aquí tienes un resumen.
Inyección por constructor
Con este enfoque las dependencias se pasan como parámetros del constructor:
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
En la mayoría de los casos es mejor usar este enfoque porque:
- Hace las dependencias obligatorias de inicializar (el compilador exige el constructor).
- Permite crear dependencias inmutables (
final).
Inyección por setter
Las dependencias se pasan mediante setters. Esto las hace opcionales:
@Component
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
Este enfoque encaja mejor para configuraciones por defecto o cuando las dependencias no siempre son necesarias.
Inyección por campo
Este método usa la anotación @Autowired directamente sobre el campo. Aunque es el más conciso, es menos preferible porque rompe la encapsulación:
@Component
public class OrderService {
@Autowired
private PaymentService paymentService;
public void placeOrder() {
paymentService.processPayment();
}
}
DI en Spring Framework
En Spring Framework hay herramientas para usar DI. Y eso es genial, porque ya no tenemos que gestionar las dependencias "a mano". Principios básicos de DI en Spring:
Vinculación automática de dependencias (Autowired)
La anotación @Autowired se usa para inyectar automáticamente las dependencias necesarias. Spring encuentra la dependencia adecuada basándose en el tipo.
Ejemplo:
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
Spring encontrará automáticamente un bean del tipo PaymentService y lo pasará al constructor.
Configuración basada en Java con la anotación @Bean
También puedes definir dependencias manualmente mediante clases de configuración:
@Configuration
public class AppConfig {
@Bean
public PaymentService paymentService() {
return new PayPalPaymentService();
}
@Bean
public OrderService orderService(PaymentService paymentService) {
return new OrderService(paymentService);
}
}
DI en proyectos reales
Bueno, podríamos repetirlo otra vez, pero en esencia ya sabes que DI ayuda al menos en cualquier aplicación web, en proyectos de microservicios y en las pruebas.
- Microservicios: en microservicios DI se usa para inyectar servicios, repositorios, configuraciones y clientes de API.
- Aplicaciones web: DI permite mantener una sola instancia de un servicio que usan todos los controllers para manejar las peticiones.
- Pruebas: DI facilita sustituir dependencias reales por objetos mock al escribir tests.
Ejemplo real
Si desarrollas una REST API para procesar pedidos, DI permite conectar fácilmente controllers, servicios y repositorios:
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<String> createOrder() {
orderService.placeOrder();
return ResponseEntity.ok("¡Pedido realizado con éxito!");
}
}
Aquí OrderController está totalmente "desacoplado": no le importa cómo está hecho OrderService — simplemente lo usa.
Errores comunes y cómo solucionarlos
Dependencias cíclicas
Si dos beans se refieren el uno al otro, Spring no podrá inyectarlos y lanzará un error. Por ejemplo:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
Para resolver esto es mejor revisar la arquitectura de la aplicación y convertir las dependencias en unidireccionales.
Ambigüedad de dependencias
Si tienes varios beans del mismo tipo, Spring no sabrá cuál inyectar:
@Component
public class CreditCardPaymentService implements PaymentService {}
@Component
public class PayPalPaymentService implements PaymentService {}
@Component
public class OrderService {
@Autowired
private PaymentService paymentService; // Error: ¿qué bean elegir?
}
La solución — utilizar la anotación @Qualifier:
@Component
public class OrderService {
@Autowired
@Qualifier("creditCardPaymentService")
private PaymentService paymentService;
}
Ahora que entiendes qué es DI, por qué sirve y cómo aplicarlo, estamos listos para estudiar las distintas formas de inyección y elegir la herramienta adecuada para cada situación.
GO TO FULL VERSION