Introdução à Dependency Injection em Swift
Dependency Injection (DI) é um dos princípios-chave de design que ajuda a criar aplicações flexíveis, testáveis e de fácil manutenção em Swift. No contexto do desenvolvimento iOS e macOS, a DI permite separar a criação de objetos do seu uso, o que simplifica significativamente os testes e a modificação do código.
A ideia principal é que uma classe não deve criar suas próprias dependências. Em vez disso, as dependências são "injetadas" externamente. Esta é uma implementação do Princípio da Inversão de Dependência (Dependency Inversion Principle) do SOLID.
Por que a Dependency Injection é importante para o desenvolvimento iOS/macOS?
Em aplicações nativas em Swift, o problema de acoplamento rígido (tight coupling) é comum. Vejamos um exemplo típico:
class UserService { let networkManager = NetworkManager() let database = Database() func fetchUser(id: Int) -> User { // lógica }}Aqui, UserService está rigidamente acoplado às implementações específicas de NetworkManager e Database. Isso cria vários problemas:
- Dificuldade de teste — é impossível substituir os serviços reais por mocks ou stubs
- Baixa reutilização — a classe não pode ser usada com outras implementações
- Violação do SRP — a classe não lida apenas com a lógica de negócios, mas também com a criação de dependências
A Dependency Injection resolve esses problemas, permitindo injetar dependências através do construtor, propriedades ou métodos.
Formas de implementar Dependency Injection em Swift
1. Injeção via construtor (Constructor Injection)
A forma mais comum e recomendada. As dependências são passadas através do inicializador:
protocol NetworkService { func request(endpoint: String) -> Data}
protocol DatabaseService { func save(user: User)}
class UserService { private let networkService: NetworkService private let databaseService: DatabaseService init(networkService: NetworkService, databaseService: DatabaseService) { self.networkService = networkService self.databaseService = databaseService } func fetchUser(id: Int) -> User? { let data = networkService.request(endpoint: "users/\\(id)") // parsing dos dados return User(id: id, name: "John") }}Vantagens: as dependências são explicitamente visíveis, obrigatórias para o funcionamento da classe e fáceis de testar.
2. Injeção via propriedade (Property Injection)
As dependências são definidas após a criação do objeto:
class AuthManager { var networkService: NetworkService? func login(username: String, password: String) -> Bool { guard let service = networkService else { return false } // lógica de autenticação return true }}
// Usolet authManager = AuthManager()authManager.networkService = RealNetworkService()Desvantagem: as dependências podem não ser definidas, o que leva a tipos opcionais e potenciais crashes.
3. Injeção via método (Method Injection)
A dependência é passada diretamente no método:
class DataProcessor { func process(data: Data, using service: NetworkService) -> ProcessedData { let result = service.request(endpoint: "process") // processamento return ProcessedData() }}Adequado para casos onde a dependência é necessária apenas para um método específico.
Exemplo prático: aplicativo de gerenciamento de tarefas
Vamos criar uma aplicação simples para iOS usando DI:
// Protocolos de dependênciasprotocol TaskRepository { func fetchTasks() ->
```