Introducción: ¿qué es la Inyección de Dependencias y para qué sirve?
\nImagine que está construyendo una casa. Necesita ladrillos, cemento y herramientas. En lugar de ir cada vez al almacén por ellos, simplemente solicita la entrega de todos los materiales en la obra. Dependency Injection (DI) o Inyección de Dependencias es el mismo principio, pero para su código.
\nLa Inyección de Dependencias es un patrón de diseño en el que un objeto recibe sus dependencias desde el exterior, en lugar de crearlas él mismo. Esto hace que el código sea más flexible, comprobable y fácil de mantener. En este artículo analizaremos DI con el ejemplo de Kotlin, un lenguaje de programación moderno que es perfecto para aprender este patrón.
\n\nEl problema: ¿por qué sin DI el código se vuelve frágil?
\nConsideremos un ejemplo típico. Supongamos que tenemos una clase UserService que envía notificaciones por email. Sin DI se ve así:
class EmailService {\n fun sendEmail(to: String, message: String) {\n println("Enviando email a $to: $message")\n }\n}\n\nclass UserService {\n private val emailService = EmailService() // ¡Acoplamiento rígido!\n\n fun notifyUser(email: String) {\n emailService.sendEmail(email, "¡Hola, usuario!")\n }\n}\n¿Cuál es el problema? UserService está rígidamente vinculado a la implementación concreta de EmailService. Si desea:
\n- \n
- Reemplazar EmailService por SmsService; \n
- Añadir registro de logs; \n
- Escribir pruebas unitarias con un objeto mock, \n
— tendrá que modificar el código de UserService. Esto viola el principio Open/Closed (abierto/cerrado) y dificulta las pruebas.
\n\nSolución: inyección manual de dependencias en Kotlin
\nLa forma más sencilla de implementar DI es pasar la dependencia a través del constructor. En Kotlin esto se hace de manera muy concisa:
\n// 1. Creamos una interfaz (abstracción)\ninterface NotificationService {\n fun send(to: String, message: String)\n}\n\n// 2. Implementamos servicios concretos\nclass EmailService : NotificationService {\n override fun send(to: String, message: String) {\n println("Email a $to: $message")\n }\n}\n\nclass SmsService : NotificationService {\n override fun send(to: String, message: String) {\n println("SMS a $to: $message")\n }\n}\n\n// 3. Inyectamos la dependencia a través del constructor\nclass UserService(private val notificationService: NotificationService) {\n fun notifyUser(email: String) {\n notificationService.send(email, "¡Hola!")\n }\n}\n\n// 4. Uso\nfun main() {\n val emailService = EmailService()\n val userService = UserService(emailService) // ¡Inyección!\n userService.notifyUser("user@example.com")\n}\nVentajas de este enfoque:
\n- \n
- Flexibilidad: se puede reemplazar fácilmente
EmailServiceporSmsService; \n - Capacidad de prueba: en las pruebas puede pasar un objeto mock: \n
class MockNotificationService : NotificationService {\n var lastMessage: String? = null\n override fun send(to: String, message: String) {\n lastMessage = message\n }\n}\n\nfun test() {\n val mock = MockNotificationService()\n val userService = UserService(mock)\n userService.notifyUser("test@test.com")\n assert(mock.lastMessage == "¡Hola!")\n}\n\nFrameworks DI: Koin y Dagger para Kotlin
\nCon DI manual, si el proyecto es grande, se escribe mucho código repetitivo. Aquí es donde los frameworks DI vienen al rescate. En el mundo de Kotlin, dos son populares: Koin (simple) y Dagger/Hilt (potente).
\n\nKoin: simplicidad y velocidad
\nKoin es un framework DI ligero escrito en Kotlin. No requiere generación de código y es