wandres.dev
DE LA IDEA A LA APP STORE · el panorama completo

Inyección de dependencias

Haz tu código flexible y testeable dependiendo de protocolos, no de implementaciones concretas. Inyección por init, por environment, y por qué importa.

⏱ 12 min

Inyección de dependencias suena grande, pero la idea es simple: en vez de que un objeto cree lo que necesita, se lo pasas desde fuera. Eso te deja cambiar esa pieza —por una falsa en tests, por otra implementación en producción— sin tocar el objeto. Es la base de un código testeable.

🎯 Al terminar esta lección sabrás
  • Qué es y por qué inyectar dependencias.
  • Depender de protocolos, no de tipos concretos.
  • Inyección por inicializador y por environment.
  • Sustituir dependencias en tests.

El problema: dependencias fijas

Si un modelo crea directamente su servicio de red, está atado a él: no puedes probarlo sin internet, ni cambiarlo:

@Observable
class Feed {
    private let servicio = ServicioReal()   // ❌ dependencia rígida
    func cargar() async { /* usa servicio */ }
}

La solución: depende de un protocolo

Define un contrato (protocolo) y haz que el modelo dependa de él, recibiéndolo desde fuera:

protocol ServicioFeed {
    func publicaciones() async throws -> [Publicacion]
}

@Observable
class Feed {
    private let servicio: ServicioFeed
    init(servicio: ServicioFeed) {          // ✅ inyectado
        self.servicio = servicio
    }
    func cargar() async { /* usa self.servicio */ }
}

Ahora Feed no sabe ni le importa qué servicio es, solo que cumple el contrato. En producción le pasas el real; en tests, uno falso.

Programa contra contratos, no contra implementaciones

Esta es una de las ideas más rentables de toda la ingeniería de software, y en Swift encaja perfecto con los protocolos del Nivel 1. Cuando tu código depende de “algo que sabe traer publicaciones” (un protocolo) en vez de “esta clase concreta que llama a esta URL”, ganas tres cosas de golpe: puedes probarlo sin red, puedes cambiar la implementación sin tocar quien la usa, y el diseño te obliga a pensar en las fronteras entre piezas. Es la diferencia entre un código rígido que se rompe al tocarlo y uno flexible que evoluciona contigo.

Inyección por environment

Para dependencias transversales, combina lo del Nivel 3: inyecta un servicio en el environment y las vistas lo toman de ahí. En iOS moderno, con @Entry defines una clave de environment para tu dependencia y la lees con @Environment. Útil cuando muchas vistas comparten el mismo servicio sin pasarlo por parámetros.

Sustituir en tests

Aquí se cobra la inversión. En un test, le pasas una implementación falsa que devuelve datos controlados:

struct ServicioFalso: ServicioFeed {
    func publicaciones() async throws -> [Publicacion] {
        [Publicacion(id: 1, title: "Test", body: "…")]   // datos fijos
    }
}

// en el test:
let feed = Feed(servicio: ServicioFalso())
await feed.cargar()
// ahora compruebas el resultado, sin depender de internet
💡
No necesitas un framework de DI

En muchos ecosistemas, la inyección de dependencias viene con librerías pesadas. En Swift, casi siempre basta con lo que ya sabes: protocolos para los contratos e inicializadores para pasar las dependencias. Empieza así de simple. Los frameworks de DI son para necesidades muy concretas de apps grandes, no un requisito de entrada.

⚔️ Desata tus dependencias
  1. Extrae un servicio a un protocolo (p. ej. ServicioFeed).
  2. Haz que un modelo lo reciba por su init en vez de crearlo.
  3. Escribe una implementación real y una falsa que conformen al protocolo.
  4. Crea el modelo con la falsa y verifica su comportamiento sin red.