Introdução: Por que a Injeção de Dependência é uma Necessidade
No desenvolvimento moderno para iOS e macOS, a limpeza e a testabilidade do código estão em primeiro plano. Um dos princípios-chave para alcançar esses objetivos é a Injeção de Dependência (DI). Sem DI, as classes frequentemente se tornam "fortemente acopladas" (tightly coupled), o que dificulta sua reutilização, modificação e, principalmente, a escrita de testes unitários.
Na prática, a Injeção de Dependência em Swift significa que um objeto recebe suas dependências de fora, em vez de criá-las ele mesmo. Isso pode parecer uma mudança pequena, mas muda radicalmente a arquitetura do aplicativo. Neste artigo, vamos abordar os principais padrões de DI em Swift: Constructor Injection, Property Injection e o uso de um container DI com o exemplo do Swinject. Cada método será ilustrado com exemplos práticos para iOS/macOS.
Pronto para tornar seu código mais flexível e testável? Então vamos começar.
1. Constructor Injection: A Maneira Mais Confiável
Constructor Injection é a injeção de dependências através do inicializador (init) da classe. Este método é considerado o mais preferível, pois garante que o objeto esteja sempre em um estado válido: todas as suas dependências são obrigatórias e imutáveis.
Vamos considerar um exemplo. Suponha que temos uma classe NetworkService, que lida com requisições HTTP, e uma classe UserViewModel, que usa este serviço.
// Protocolo para abstraçãoprotocol NetworkServiceProtocol { func fetchUsers() async throws -> [User]}
// Implementação realclass RealNetworkService: NetworkServiceProtocol { func fetchUsers() async throws -> [User] { // Requisição HTTP real return [] }}
// ViewModel com Constructor Injectionclass UserViewModel { private let networkService: NetworkServiceProtocol
// A dependência é passada através do inicializador init(networkService: NetworkServiceProtocol) { self.networkService = networkService }
func loadUsers() async { do { let users = try await networkService.fetchUsers() print("Carregados \\(users.count) usuários") } catch { print("Erro: \\(error)") } }}
// Usolet service = RealNetworkService()let viewModel = UserViewModel(networkService: service)Vantagens:
- A dependência é explicitamente declarada e obrigatória.
- O objeto não pode ser criado sem dependências.
- Ideal para testes unitários: é fácil substituir a implementação real por um Mock.
Desvantagens:
- Se houver muitas dependências, o inicializador se torna extenso.
- Não é adequado para dependências opcionais.
Este método é o "padrão ouro" da DI. Use-o sempre que possível.
2. Property Injection: Flexibilidade para Dependências Opcionais
Property Injection é a injeção de dependências através de propriedades do objeto. Geralmente é usado quando a dependência não é obrigatória para o funcionamento do objeto ou quando não é possível passá-la pelo inicializador (por exemplo, em Storyboard/UIKit).
Um exemplo clássico é um ViewController no iOS que é criado a partir de um Storyboard. Você não pode alterar seu init, então usa uma propriedade.
protocol LoggerProtocol { func log(message: String)}
class ConsoleLogger: LoggerProtocol { func log(message: String) { print("LOG: \\(message)") }}
class MyViewController: UIViewController { // Dependência opcional var logger: LoggerProtocol?
override func viewDidLoad() { super.viewDidLoad() logger?.log(message: "View did load") // Se logger não estiver definido, nada acontece }}
// Configuração (exemp