Introducción: ¿Por qué su código necesita Inyección de Dependencias?
Cada desarrollador de iOS se enfrenta a una situación en la que una clase se vuelve "gorda" y está rígidamente vinculada a implementaciones específicas. Cambia una clase y tiene que reescribir docenas de otras. ¿Le suena familiar? Este es el problema clásico del acoplamiento fuerte (tight coupling).
Dependency Injection (DI) es un patrón de diseño que resuelve este problema. En lugar de que el objeto cree sus propias dependencias, estas se "inyectan" desde el exterior. Esto hace que el código sea flexible, comprobable y que cumpla con los principios SOLID (en particular, el principio de inversión de dependencias).
En este artículo, analizaremos qué es DI en Swift, qué formas de implementarlo existen y cómo simplifica la vida del desarrollador de iOS/macOS. Usaremos ejemplos simples, comprensibles incluso para un principiante.
¿Qué es la Inyección de Dependencias? Un ejemplo simple
Imagine que está horneando una pizza. La opción sin DI: usted mismo cultiva el trigo, hace la harina, ordeña la vaca para el queso. Esto es acoplamiento fuerte. La opción con DI: simplemente pide los ingredientes (dependencias) a un proveedor. No sabe cómo se hizo la harina, solo le importa el resultado.
En programación es lo mismo. Supongamos que tenemos una clase OrderService que envía pedidos por correo electrónico:
// ❌ Sin DI — acoplamiento fuerteclass OrderService { let mailer = Mailer() // Crea la dependencia él mismo
func process(order: Order) { mailer.send(email: "Nuevo pedido: \\(order.id)") }}Problema: no podemos reemplazar Mailer por una versión de prueba. La clase OrderService "sabe" demasiado.
// ✅ Con DI — la dependencia se inyectaclass OrderService { let mailer: MailerProtocol // Dependemos de una abstracción, no de un concreto
init(mailer: MailerProtocol) { // Inyección a través del inicializador self.mailer = mailer }
func process(order: Order) { mailer.send(email: "Nuevo pedido: \\(order.id)") }}Ahora podemos pasar cualquier objeto que cumpla con el protocolo MailerProtocol — un cliente de correo real o un "simulacro" para pruebas.
Tres formas principales de inyectar dependencias en Swift
Existen tres formas populares de pasar una dependencia a una clase. Veamos cada una.
1. Constructor Injection (a través del inicializador)
La forma más común y preferida. La dependencia se pasa al crear el objeto. Esto garantiza que el objeto siempre esté en un estado válido.
protocol NetworkServiceProtocol { func fetchData() -> String}
class RealNetworkService: NetworkServiceProtocol { func fetchData() -> String { return "Datos reales del servidor" }}
class MockNetworkService: NetworkServiceProtocol { func fetchData() -> String { return "Datos de prueba" }}
class DataManager { private let network: NetworkServiceProtocol
// La dependencia se pasa a través de init init(network: NetworkServiceProtocol) { self.network = network }
func load() -> String { return network.fetchData() }}
// Usolet realManager = DataManager(network: RealNetworkService())let mockManager = DataManager(network: MockNetworkService())Ventajas: Se ve claramente qué dependencias se necesitan. El objeto está listo para funcionar de inmediato. Desventajas: Si hay muchas dependencias, el inicializador se vuelve voluminoso.
2. Property Injection (a través de una propiedad)
La dependencia se establece después de crear el objeto. A menudo se usa cuando la dependencia es opcional o difícil de obtener en el momento de la inicialización.
class UserViewModel { var apiService: NetworkServiceProtocol? // Dependencia opcional
func loadUser() { apiServi