Simplificando Inversion of Control y Dependency Injection
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"));
}
}