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
}
}
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.
- 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);
}
}
- 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
}
}
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
Intentaremos evitar hacer:
if (isProd()) {
// usar cloud
} else {
// guardar en local
}
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);
}
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");
}
}
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");
}
}
Y nuestro service puede simplemente hacer uso de la interfaz, sin involucrarse en detalles:
var link = videoPublisher.publishContent(...)
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"));
}
}
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"));
}
}


Top comments (0)