CodeGym /Cursos /Módulo 5. Spring /Principios básicos de Dependency Injection (DI)

Principios básicos de Dependency Injection (DI)

Módulo 5. Spring
Nivel 2 , Lección 1
Disponible

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:

  1. 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.
  2. Fácil de probar: puedes sustituir fácilmente las dependencias por "mocks" para escribir tests.
  3. 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:

  1. OrderService depende de una implementación concreta de PaymentService. Si necesitamos usar otro PaymentService, tendremos que cambiar el código.
  2. Probar OrderService se complica porque no podemos reemplazar PaymentService por 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:

  1. Por constructor
  2. Por setter
  3. 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.

  1. Microservicios: en microservicios DI se usa para inyectar servicios, repositorios, configuraciones y clientes de API.
  2. Aplicaciones web: DI permite mantener una sola instancia de un servicio que usan todos los controllers para manejar las peticiones.
  3. 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.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION