Introdução: o que é Injeção de Dependência e para que serve?
\nImagine que você está construindo uma casa. Você precisa de tijolos, cimento e ferramentas. Em vez de ir ao depósito buscá-los toda vez, você simplesmente pede a entrega de todos os materiais no canteiro de obras. Dependency Injection (DI) ou Injeção de Dependência é o mesmo princípio, mas para o seu código.
\nInjeção de Dependência é um padrão de projeto no qual um objeto recebe suas dependências de fora, em vez de criá-las ele mesmo. Isso torna o código mais flexível, testável e fácil de manter. Neste artigo, vamos explorar DI com exemplos em Kotlin — uma linguagem de programação moderna que é excelente para aprender esse padrão.
\n\nO problema: por que sem DI o código se torna frágil?
\nVamos considerar um exemplo típico. Suponha que temos uma classe UserService que envia notificações por email. Sem DI, ela se parece com isso:
class EmailService {\n fun sendEmail(to: String, message: String) {\n println("Enviando email para $to: $message")\n }\n}\n\nclass UserService {\n private val emailService = EmailService() // Acoplamento rígido!\n\n fun notifyUser(email: String) {\n emailService.sendEmail(email, "Olá, usuário!")\n }\n}\nQual é o problema? UserService está rigidamente acoplado a uma implementação específica de EmailService. Se você quiser:
\n- \n
- Substituir EmailService por SmsService; \n
- Adicionar logging; \n
- Escrever testes unitários com um objeto mock, \n
— você terá que alterar o código de UserService. Isso viola o princípio Open/Closed (aberto/fechado) e dificulta os testes.
\n\nSolução: injeção manual de dependências em Kotlin
\nA maneira mais simples de implementar DI é passar a dependência pelo construtor. Em Kotlin, isso é feito de forma muito concisa:
\n// 1. Criamos uma interface (abstração)\ninterface NotificationService {\n fun send(to: String, message: String)\n}\n\n// 2. Implementamos serviços concretos\nclass EmailService : NotificationService {\n override fun send(to: String, message: String) {\n println("Email para $to: $message")\n }\n}\n\nclass SmsService : NotificationService {\n override fun send(to: String, message: String) {\n println("SMS para $to: $message")\n }\n}\n\n// 3. Injetamos a dependência pelo construtor\nclass UserService(private val notificationService: NotificationService) {\n fun notifyUser(email: String) {\n notificationService.send(email, "Olá!")\n }\n}\n\n// 4. Uso\nfun main() {\n val emailService = EmailService()\n val userService = UserService(emailService) // Injeção!\n userService.notifyUser("user@example.com")\n}\nVantagens dessa abordagem:
\n- \n
- Flexibilidade: você pode facilmente substituir
EmailServiceporSmsService; \n - Testabilidade: nos testes, você pode passar um 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 == "Olá!")\n}\n\nFrameworks DI: Koin e Dagger para Kotlin
\nCom DI manual, se o projeto for grande, você escreve muito código boilerplate. É aí que os frameworks DI entram em ação. No mundo Kotlin, dois são populares: Koin (simples) e Dagger/Hilt (poderoso).
\n\nKoin: simplicidade e velocidade
\nKoin é um framework DI leve, escrito em Kotlin. Ele não requer geração de código e é