Simplificando Inversion of Control y Dependency Injection

Published: (December 10, 2025 at 09:00 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Inversión de Control (IoC)

Inversión de Control (IoC) es un principio fundamental en el que se apoyan todos los frameworks modernos. La idea central es simple: dejar que el framework lo haga por vos.

Cuando usamos un framework backend como Spring, este toma control de una enorme cantidad de responsabilidades que, sin él, tendrías que escribir a mano:

  • manejar el ciclo de vida del servidor
  • gestionar hilos y concurrencia
  • rutear cada request
  • leer y escribir sobre streams I/O
  • serializar y deserializar datos
  • manejar excepciones
  • proveer herramientas de testing sobre todo eso

Eso es IoC: el control deja de estar en tu código y pasa al framework.

Ejemplos de frameworks que aplican IoC

  • Django (Python)
  • Express.js (Node)
  • Gin / Gorilla (Go)

Inyección de Dependencias (DI)

A diferencia de IoC, la Inyección de Dependencias (DI) es un patrón de diseño. Podés usar un framework sin DI, o usar DI sin un framework. Su objetivo es resolver un problema puntual: la provisión de dependencias.

Cuando una clase necesita algo que está implementado en otra, aparece una dependencia. Por ejemplo:

public class EmailSender {
    private final EmailWriter emailWriter;

    public EmailSender(EmailWriter emailWriter) {
        this.emailWriter = emailWriter;
    }

    public void sendEmail(EmailContent emailContent) {
        var email = emailWriter.writeEmail(emailContent);
        // etc etc
    }
}

En este ejemplo EmailSender depende de EmailWriter; usarlo sin proporcionar la dependencia provocaría un NullPointerException.

Sin DI: dos soluciones manuales

Crear la dependencia desde afuera

public class SomeService {
    public void notifySomething(Destiny destiny) {
        var emailWriter = new EmailSender(new EmailWriter());
        EmailContent emailContent = buildEmailContent(destiny);
        emailWriter.sendEmail(emailContent);
    }
}

Crear la dependencia dentro del método

public class EmailSender {
    public void sendEmail(EmailContent emailContent) {
        var emailWriter = new EmailWriter();
        var email = emailWriter.writeEmail(emailContent);
        // etc etc
    }
}

Ambos enfoques presentan problemas puntuales:

  • Acople innecesario
  • Dificultad para testear
  • Imposibilidad de mockear dependencias
  • La responsabilidad de crear objetos se “mueve de lugar”

DI resuelve esto delegando la gestión de dependencias al framework.

Caso de uso: API de videos

Supongamos un API donde los usuarios suben videos. Debemos:

  • Encriptar
  • Publicar
  • Persistir metadata

Además:

  • Soportamos dos tipos de video: MP4 y FLV
  • En desarrollo queremos guardar localmente, pero en producción debemos usar un cloud provider
  • Queremos poder cambiar de proveedor sin tocar el servicio

En lugar de escribir condicionales como if (isProd()) { … } else { … }, usamos DI con perfiles.

Interfaz común

public interface VideoPublisher {
    URI publishContent(VideoPostRequest videoPostRequest);
}

Implementación local

@Component
@Profile("local")
public class LocalVideoPublisher implements VideoPublisher {
    private static final Logger log = LoggerFactory.getLogger(LocalVideoPublisher.class);

    @Override
    public URI publishContent(VideoPostRequest videoPostRequest) {
        log.info("We save the video locally");
        return URI.create("file:///local/test/video123");
    }
}

Implementación productiva

@Component
@Profile("prod")
public class CloudVideoPublisher implements VideoPublisher {
    private static final Logger log = LoggerFactory.getLogger(CloudVideoPublisher.class);

    @Override
    public URI publishContent(VideoPostRequest videoPostRequest) {
        log.info("Posting to cloud provider!");
        return URI.create("https://mycloudprovider.com/uploads/video123");
    }
}

Servicio que usa la interfaz

@Service
public class VideoService {
    private final VideoPublisher videoPublisher;
    private final VideoRepository videoRepository;

    public VideoService(VideoPublisher videoPublisher, VideoRepository videoRepository) {
        this.videoPublisher = videoPublisher;
        this.videoRepository = videoRepository;
    }

    public UUID postVideo(VideoRequest request) {
        URI link = videoPublisher.publishContent(request.toPostRequest());
        // persist metadata with the link …
        return videoRepository.save(new Video(link, …));
    }
}

Tests con perfiles

Test en entorno local

@SpringBootTest
@ActiveProfiles("local")
class LocalVideoServiceTest {
    @Autowired VideoService videoService;
    @Autowired VideoRepository videoRepository;

    @Test
    void shouldSaveLocally() {
        var input = new VideoRequest(
            VideoType.MP4,
            new byte[]{},
            "My title",
            "My description",
            1L
        );
        var result = videoService.postVideo(input);

        var actual = videoRepository.findById(result);

        assertThat(actual).get()
            .extracting(Video::link)
            .isEqualTo(URI.create("file:///local/test/video123"));
    }
}

Test en entorno productivo

@SpringBootTest
@ActiveProfiles("prod")
class ProdVideoServiceTest {
    @Autowired VideoService videoService;
    @Autowired VideoRepository videoRepository;

    @Test
    void shouldSaveOnCloudProvider() {
        var input = new VideoRequest(
            VideoType.MP4,
            new byte[]{},
            "Some other video",
            "Significant description",
            2L
        );
        var result = videoService.postVideo(input);

        var actual = videoRepository.findById(result);

        assertThat(actual).get()
            .extracting(Video::link)
            .isEqualTo(URI.create("https://mycloudprovider.com/uploads/video123"));
    }
}
Back to Blog

Related posts

Read more »