DIP (Dependency Inversion Principle) — el quinto principio SOLID que define las reglas para construir dependencias entre módulos: los módulos de alto nivel no deben depender de módulos de bajo nivel, ambos deben depender de abstracciones. Las abstracciones no deben depender de los detalles — los detalles deben depender de las abstracciones. Este principio, descrito por Robert Martin en Clean Architecture (2017), está en la base de la arquitectura débilmente acoplada. Según este libro, el principio de inversión de dependencias elimina los acoplamientos rígidos entre las capas de la aplicación.
Puntos clave
DIP (Dependency Inversion Principle) es el principio de inversión de dependencias que invierte la visión tradicional sobre la dirección de las dependencias entre módulos. Los módulos de alto nivel (lógica de negocio) no deben depender directamente de módulos de bajo nivel (base de datos, red, UI). En su lugar, ambos niveles dependen de abstracciones definidas en el módulo de alto nivel.
La formulación formal de DIP incluye dos reglas: A — los módulos de alto nivel no deben depender de módulos de bajo nivel, ambos deben depender de abstracciones. B — las abstracciones no deben depender de los detalles, los detalles deben depender de las abstracciones. La segunda regla se deriva de la primera: si una abstracción depende de los detalles, no puede ser una base estable para un módulo de alto nivel.
Sin DIP, una arquitectura típica se ve así: BusinessLogic → DatabaseRepository — la lógica de negocio depende directamente de un repositorio concreto. Con DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic no sabe de la existencia de DatabaseRepository, solo conoce la interfaz DatabaseService, que se implementa fuera de la lógica de negocio.
Inversión significa que el flujo de control y el flujo de dependencias van en direcciones opuestas. El flujo de control va de arriba abajo: UI → ViewModel → UseCase → Repository. El flujo de dependencias va de abajo arriba: Repository implementa una interfaz definida en UseCase. Repository (bajo nivel) depende de UseCase (alto nivel).
Esta inversión es la diferencia clave entre DIP y la separación habitual en capas. En la arquitectura tradicional por capas, cada capa depende de la capa inferior. En la arquitectura con DIP, todas las capas dependen de abstracciones, mientras que la implementación de estas abstracciones reside en la capa de infraestructura, que se “enchufa” a las capas superiores mediante mecanismos de DI.
El mecanismo de DIP se implementa definiendo abstracciones en los módulos de alto nivel y realizándolas en los módulos de bajo nivel. El módulo de alto nivel declara una interfaz para la funcionalidad que necesita. El módulo de bajo nivel implementa esa interfaz. El ensamblaje (wiring) ocurre en la raíz de composición de la aplicación.
El proceso de introducir DIP en código existente: extraer una interfaz para el módulo de bajo nivel, mover esta interfaz al módulo de alto nivel (o a una capa separada de abstracciones), reescribir la dependencia del módulo de alto nivel para que use la interfaz, hacer que el módulo de bajo nivel implemente esta interfaz. Después de estos pasos, la dirección de la dependencia se ha invertido.
DIP requiere un mecanismo de raíz de composición — un punto en la aplicación donde se crean y conectan todas las dependencias. En Android es Application.get() o el componente Hilt, en iOS — AppDelegate o SceneDelegate. La raíz de composición es el único lugar donde el código conoce las implementaciones concretas.
DIP crea límites arquitectónicos entre las capas de la aplicación. Cuando ViewModel depende de la interfaz UserRepository, se forma un límite entre las capas de presentación y dominio: ViewModel (presentación) no sabe de dónde vienen los datos. Este límite permite cambiar la implementación de UserRepository (Room → REST → Mock) sin afectar a ViewModel. Cuantos más límites de este tipo, más resistente es la aplicación a los cambios de frameworks y bibliotecas.
En la arquitectura Android recomendada por Google, DIP se implementa mediante UseCases que residen en la capa de dominio y dependen de interfaces Repository. RepositoryImpl están en la capa de datos e implementan estas interfaces. La capa de presentación (ViewModel) depende de UseCases. La dirección de las dependencias va de presentación a dominio, de dominio a datos — pero ninguna capa conoce las implementaciones concretas de otra capa.
DIP y DI a menudo se confunden, pero son conceptos diferentes. DIP es un principio arquitectónico (QUÉ hacer: depender de abstracciones). DI es un patrón de implementación (CÓMO hacerlo: pasar dependencias mediante el constructor). DIP responde a la pregunta “¿en qué deben basarse los módulos?”, DI responde a “¿cómo obtienen los objetos sus dependencias?”.
Dependency Injection es una forma de inyectar dependencias en un objeto a través del constructor, método o propiedad. Cuando una clase Kotlin recibe una interfaz Repository mediante su constructor — eso es DI. El hecho de que una clase ViewModel dependa de la interfaz Repository y no de una implementación concreta de RoomRepository — eso es DIP. DI es la herramienta, DIP es el objetivo.
Se puede seguir DIP sin un framework DI: el ensamblaje manual de dependencias en la raíz de composición también es DI (manual DI). Se puede usar un framework DI (Dagger, Hilt, Koin) violando DIP: si ViewModel crea directamente un objeto Repository mediante new() — DIP se viola, incluso si el framework está instalado. DIP es una decisión arquitectónica, DI es un detalle técnico.
Veamos un ejemplo en Android de aplicación de DIP a la capa de datos. Sin DIP, una ViewModel crea directamente una RoomDatabase y DAO. Con DIP — la ViewModel depende de la interfaz UserRepository, y la implementación concreta RoomUserRepository se proporciona externamente.
// La abstracción pertenece a la capa de dominio (alto nivel)
interface UserRepository {
fun getUser(id: Int): User
}
// La capa de dominio depende solo de la abstracción
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// La implementación en la capa de datos depende de la abstracción de la capa de dominio
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Raíz de composición
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
Un ejemplo en iOS con un Application Coordinator y un protocolo de navegación:
// Abstracción de navegación en la capa de dominio
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel depende de la abstracción, no de UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (capa UIKit) implementa el protocolo de la capa de dominio
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// Código de navegación UIKit
}
func navigateToLogin() {
// Código de navegación UIKit
}
}
El punto clave: AuthViewModel (dominio) no sabe de la existencia de AppCoordinator (UIKit). Solo conoce el protocolo AuthNavigation. Si UIKit se reemplaza por SwiftUI mañana — AuthViewModel no requiere cambios. DIP hace que la capa de dominio sea independiente de los frameworks y bibliotecas de UI.
Hilt es la herramienta DI estándar para Android, recomendada por Google. Está integrada en Jetpack, soporta ViewModel, Fragment, Service y otros componentes de Android. Hilt automatiza la creación de la raíz de composición mediante anotaciones @Module, @Provides, @Inject. Usar Hilt no garantiza el cumplimiento de DIP — la interfaz UserRepository debe definirse en la capa de dominio, no en la capa de datos.
Koin es un framework DI ligero para Kotlin sin generación de código ni procesamiento de anotaciones. El DSL de Koin (module, single, factory) es más fácil de aprender, pero la verificación de dependencias ocurre en tiempo de ejecución, no en tiempo de compilación. Koin es popular en proyectos multiplataforma (KMP) gracias al soporte de iOS.
Dagger 2 es el predecesor de Hilt, todavía usado en proyectos grandes. Dagger genera código DI en tiempo de compilación, lo que proporciona el máximo rendimiento y diagnóstico de errores en tiempo de compilación. Hilt está construido sobre Dagger y proporciona una API simplificada. Para proyectos nuevos, Google recomienda Hilt como framework DI principal.
Los módulos DI deben corresponder a las capas arquitectónicas y separarse en DomainModule, DataModule, PresentationModule. DomainModule proporciona solo abstracciones y UseCases. DataModule proporciona implementaciones para las abstracciones. PresentationModule conecta ViewModels con UseCases. Esta organización garantiza que la capa de dominio permanezca independiente de las bibliotecas de infraestructura.
Al migrar entre frameworks DI (por ejemplo, de Koin a Hilt), la estructura de DomainModule no cambia — solo cambian los métodos de conexión en DataModule y PresentationModule. DIP asegura el aislamiento de la lógica de dominio, mientras que el framework DI es un mecanismo técnico de conexión.
Preguntas frecuentes
DIP es necesario en los límites arquitectónicos — entre las capas de la aplicación (dominio → datos, presentación → dominio). Dentro de una misma capa, DIP puede ser excesivo. Por ejemplo, una clase utilitaria StringFormatter dentro de la capa de dominio no requiere una interfaz — si no hay razón para reemplazarla.
No. DIP es un principio: los módulos deben depender de abstracciones. DI es un patrón: un objeto recibe dependencias desde el exterior en lugar de crearlas él mismo. DI es una forma de implementar DIP, pero se puede seguir DIP sin DI (mediante fábricas o service locator). DI sin DIP es posible pero no tiene valor arquitectónico.
Las interfaces pertenecen al módulo que las usa, no al módulo que las implementa. UserRepository se declara en la capa de dominio y se implementa en la capa de datos. Esta es la regla clave de DIP: el propietario de la abstracción es el consumidor, no el proveedor de la implementación.
DIP hace posible las pruebas en capas aisladas. Un ViewModel que depende de UserRepository (interfaz) se puede probar con una implementación mock sin base de datos. Sin DIP, el ViewModel dependería de RoomUserRepository y requeriría configurar la base de datos para cada prueba. DIP + DI proporcionan un aislamiento completo de módulos durante las pruebas.
Hilt es la opción estándar para proyectos Android, recomendada por Google. Koin es una alternativa para proyectos Kotlin Multiplatform. Dagger 2 es para proyectos existentes donde la migración a Hilt no está justificada. Elegir un framework no elimina la necesidad de seguir DIP a nivel arquitectónico.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también