DEV Community

Cover image for Simplificando Inversion of Control y Dependency Injection
Federico Herrera
Federico Herrera

Posted on

Simplificando Inversion of Control y Dependency Injection

Inversión de Control (IoC) y Inyección de Dependencias (DI) son dos conceptos que aparecen en prácticamente cualquier entrevista de desarrollo web. Y aun así, muchos desarrolladores con años de experiencia no siempre pueden explicarlos con claridad. No porque no los entiendan, al contrario, sino porque están tan asimilados en el día a día que dejan de ser visibles.

Un pequeño repaso nunca viene mal.

Inversion of 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,
  • y proveer herramientas de testing sobre todo eso.

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

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

Dependency Injection

A diferencia de IoC, 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
    }
}
Enter fullscreen mode Exit fullscreen mode

En éste ejemplo podemos ver que EmailSender depende de EmailWriter, usarlo arrojaría NullPointerException en caso de que no se proporcione.

En éste caso si no tuvieramos DI tendríamos 2 soluciones.

  1. Crear la dependencia desde afuera
public class SomeService {
     public void notifySomething(Destiny destiny) {
         var emailWriter = new EmailSender(new EmailWriter());
         EmailContent emailContent = buildEmailContent(destiny);
         emailSender.sendEmail(emailContent);
     }
}
Enter fullscreen mode Exit fullscreen mode
  1. Crear la dependencia adentro del método
public class EmailSender {

    public void sendEmail(EmailContent emailContent) {
        var emailWriter = new EmailWriter();
        var email = emailWriter.writeEmail(emailContent);
        // etc etc
    }
}
Enter fullscreen mode Exit fullscreen mode

Ambos enfoques tienen problemas puntuales:

  • Acople innecesario
  • Dificultad para testear
  • Imposibilidad de mockear dependencias
  • La responsabilidad de crear objetos termina “moviéndose de lugar”

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

Demo

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

Diagrama de flujo de Video Service, request http a POST /api/video hacia un paso de encriptación, luego publicación y finalmente persistencia antes de retornar a cliente

Intentaremos evitar hacer:

if (isProd()) {
  // usar cloud
} else {
  // guardar en local
}
Enter fullscreen mode Exit fullscreen mode

Tenemos entonces:
Diagrama de flujo del servicio de video, mostrando los pasos de encriptación, publicación y persistencia, con un proceso de bifurcación basado en el ambiente (producción o local), y la leyenda indica los diferentes tipos de bloques.

Aplicando IoC y DI

Para el publisher podemos hacer uso de una interfaz común y profiles, permitiendonos tener 2 implementaciones distintas y solo le indicamos al framework caundo usar cada una y que se encargue del resto.

La interfaz común:

public interface VideoPublisher {
    URI publishContent(VideoPostRequest videoPostRequest);
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Y nuestro service puede simplemente hacer uso de la interfaz, sin involucrarse en detalles:

var link = videoPublisher.publishContent(...)
Enter fullscreen mode Exit fullscreen mode

DI nos permite testearlo de forma tan simple como dandole un perfil contextual al test y validando que la URI es la esperada:

@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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Profile = prod

@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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)