Introducción: Por qué la Inyección de Dependencias es una necesidad
En el desarrollo moderno para iOS y macOS, la limpieza y testabilidad del código pasan a primer plano. Uno de los principios clave que permiten alcanzar estos objetivos es la Inyección de Dependencias (DI). Sin DI, las clases a menudo se vuelven "fuertemente acopladas" (tightly coupled), lo que dificulta su reutilización, modificación y, lo más importante, la escritura de pruebas unitarias.
En la práctica, la Inyección de Dependencias en Swift significa que un objeto recibe sus dependencias desde el exterior, en lugar de crearlas él mismo. Esto puede parecer un cambio menor, pero transforma radicalmente la arquitectura de la aplicación. En este artículo, analizaremos los patrones principales de DI en Swift: Constructor Injection, Property Injection y el uso de un contenedor DI con el ejemplo de Swinject. Cada método se ilustrará con ejemplos prácticos para iOS/macOS.
¿Listo para hacer tu código más flexible y testeable? Entonces comencemos.
1. Constructor Injection: La forma más confiable
Constructor Injection es la inyección de dependencias a través del inicializador (init) de la clase. Este método se considera el más preferible, ya que garantiza que el objeto siempre esté en un estado válido: todas sus dependencias son obligatorias e inmutables.
Consideremos un ejemplo. Supongamos que tenemos una clase NetworkService que maneja solicitudes HTTP, y una clase UserViewModel que utiliza este servicio.
// Protocolo para abstracciónprotocol NetworkServiceProtocol { func fetchUsers() async throws -> [User]}
// Implementación realclass RealNetworkService: NetworkServiceProtocol { func fetchUsers() async throws -> [User] { // Solicitud HTTP real return [] }}
// ViewModel con Constructor Injectionclass UserViewModel { private let networkService: NetworkServiceProtocol
// La dependencia se pasa a través del inicializador init(networkService: NetworkServiceProtocol) { self.networkService = networkService }
func loadUsers() async { do { let users = try await networkService.fetchUsers() print("Cargados \\(users.count) usuarios") } catch { print("Error: \\(error)") } }}
// Usolet service = RealNetworkService()let viewModel = UserViewModel(networkService: service)Ventajas:
- La dependencia está explícitamente declarada y es obligatoria.
- El objeto no puede crearse sin dependencias.
- Ideal para pruebas unitarias: es fácil reemplazar la implementación real por un Mock.
Desventajas:
- Si hay muchas dependencias, el inicializador se vuelve voluminoso.
- No es adecuado para dependencias opcionales.
Este método es el "estándar de oro" de DI. Úsalo siempre que sea posible.
2. Property Injection: Flexibilidad para dependencias opcionales
Property Injection es la inyección de dependencias a través de propiedades del objeto. Generalmente se usa cuando la dependencia no es obligatoria para el funcionamiento del objeto o cuando no se puede pasar a través del inicializador (por ejemplo, en Storyboard/UIKit).
Un ejemplo clásico es un ViewController en iOS que se crea desde un Storyboard. No puedes modificar su init, por lo que usas una propiedad.
protocol LoggerProtocol { func log(message: String)}
class ConsoleLogger: LoggerProtocol { func log(message: String) { print("LOG: \\(message)") }}
class MyViewController: UIViewController { // Dependencia opcional var logger: LoggerProtocol?
override func viewDidLoad() { super.viewDidLoad() logger?.log(message: "Vista cargada") // Si logger no está configurado, no ocurre nada }}
// Configuración (por ejem