El Principio de Inversión de Dependencias (DIP, Dependency Inversion Principle) busca desacoplar módulos de software promoviendo dependencias en abstracciones en lugar de implementaciones concretas. Gracias a DIP, los componentes de alto nivel no necesitan conocer los detalles de bajo nivel para colaborar entre sí.
Este principio fue popularizado por Robert C. Martin y es clave para construir sistemas flexibles, testables y fáciles de extender. De él se desprende la idea de inyección de dependencias y el uso de contenedores IoC en Java.
Esto implica redirigir las dependencias de tal forma que las clases concretas se adapten a contratos previamente definidos.
Veamos una clase de servicio que graba eventos directamente en un motor de persistencia concreto. El módulo de alto nivel conoce demasiado sobre la infraestructura.
class RegistroEventosService {
private final JdbcEventoRepository jdbcEventoRepository = new JdbcEventoRepository();
void registrar(Evento evento) {
jdbcEventoRepository.guardar(evento);
}
}
El servicio no puede reutilizarse con otro mecanismo de almacenamiento sin modificar su código. Además, es difícil testearlo porque la dependencia concreta se instancia dentro de la clase.
Introducimos una abstracción que representa un repositorio de eventos. El servicio depende de la interfaz y la implementación concreta se provee desde afuera, mediante inyección de dependencias.
interface EventoRepository {
void guardar(Evento evento);
}
class RegistroEventosService {
private final EventoRepository eventoRepository;
RegistroEventosService(EventoRepository eventoRepository) {
this.eventoRepository = eventoRepository;
}
void registrar(Evento evento) {
eventoRepository.guardar(evento);
}
}
Ahora la clase está cerrada a cambios cuando se introduce un nuevo mecanismo de persistencia. Las implementaciones dependen del contrato EventoRepository
y el servicio se mantiene estable.
class JdbcEventoRepository implements EventoRepository {
public void guardar(Evento evento) {
// usa JDBC para persistir
}
}
class InMemoryEventoRepository implements EventoRepository {
private final List<Evento> eventos = new ArrayList<>();
public void guardar(Evento evento) {
eventos.add(evento);
}
}
El servicio se puede inicializar con cualquiera de estas implementaciones, lo cual permite probarlo en memoria y en producción usando JDBC sin modificar el código de alto nivel.
DIP se complementa con la inversión de control (IoC), donde un contenedor como Spring Framework administra la creación e inyección de dependencias. En lugar de instanciar las clases manualmente, se declara qué contrato necesita cada servicio y el contenedor resuelve la implementación concreta en tiempo de ejecución.
Consideremos un flujo que envía alertas al cliente. Aquí se observa cómo DIP facilita agregar nuevos canales sin modificar la lógica principal.
interface Notificador {
void notificar(Alerta alerta);
}
class AlertaService {
private final Notificador notificador;
AlertaService(Notificador notificador) {
this.notificador = notificador;
}
void enviarAlerta(Alerta alerta) {
notificador.notificar(alerta);
}
}
La abstracción Notificador
permite implementar correo electrónico, SMS, notificaciones push, etc. Cada canal depende del contrato y puede evolucionar de forma independiente.
Para adoptar DIP a gran escala, conviene agrupar interfaces por capas: repositorios, servicios, gateways externos. Las clases concretas viven en módulos independientes que se conectan mediante las abstracciones compartidas. Esto habilita arquitecturas hexagonales o limpias, donde la lógica de negocio se mantiene aislada de los detalles técnicos.
El Principio de Inversión de Dependencias permite crear software adaptable y resistente al cambio. En los próximos temas veremos cómo combinar los principios aprendidos para formar un diseño coherente en toda la base de código.