Diseño de software

Inyección de dependencias (DI): sacar new de la clase

A menudo creamos los objetos necesarios justo donde se usan en el código. Pero en cuanto intentas escribir pruebas, encuentras un muro. El código de creación está incrustado en la clase y no puedes sustituirlo.

5 min de lectura
Imagen de portada de Inyección de dependencias (DI): sacar new de la clase

A menudo creamos los objetos necesarios justo donde se usan en el código. Pero en cuanto intentas escribir pruebas, encuentras un muro. El código de creación está incrustado en la clase y no puedes sustituirlo.

Aunque busques qué es la inyección de dependencias (DI), suelen aparecer términos como “inversión de control” o “contenedor IoC”, lo que puede resultar aún más confuso.

Por eso hoy voy a explicarlo de la forma más sencilla posible. Empecemos por la conclusión.

La inyección de dependencias (DI) consiste en crear fuera de la clase el objeto que antes se creaba dentro y pasarlo a la clase. Ese único desplazamiento es la clave: sacar la creación del objeto fuera de la clase. Eso es prácticamente todo lo que implica la DI.

Al terminar este artículo entenderás por qué hace falta la DI, cómo cambia el código y por qué bibliotecas como Swinject se encargan de ello.

¿Qué problema hay si el objeto se crea dentro de la clase?

Supongamos que tenemos una clase que procesa pedidos. Necesita un módulo de pago para procesarlos.

El código de principiante más habitual tiene este aspecto.

class OrderService {
    // Crear directamente el objeto de pago dentro de la clase
    private let pay = KakaoPay()

    func order() { pay.pay() }
}

El problema es que OrderService queda estrechamente acoplado a KakaoPay.

¿Y si quieres cambiar KakaoPay por NaverPay? Tienes que entrar en la clase y modificar la parte de creación.

Aunque quieras introducir un objeto de pago falso durante las pruebas, no hay forma de hacerlo. La clase ya ha creado internamente un KakaoPay real.

En otras palabras, colocar la creación del objeto dentro de la clase la ata a una implementación concreta.


Por eso sacamos la creación del objeto fuera

La solución es más sencilla de lo que parece. No lo crees directamente: créalo fuera y limítate a recibirlo.

class OrderService {
    private let pay: Pay
    // Limitarse a recibir lo creado y pasado desde fuera
    init(pay: Pay) { self.pay = pay }

    func order() { pay.pay() }
}

Solo cambió una cosa: desapareció la creación directa y ahora se recibe mediante el inicializador.

OrderService ya no necesita saber si el pago lo gestiona KakaoPay o NaverPay. Solo sabe que recibe y usa algo llamado Pay.

La esencia de la DI no tiene nada de grandioso. Simplemente mueve la creación del objeto fuera de la clase y delega fuera la decisión de qué introducir.

Introducir un objeto desde fuera se denomina “inyectar”. Se inyecta el objeto del que depende la clase; de ahí, inyección de dependencias.

El nombre puede parecer difícil, pero eso es todo lo que hace.


¿Qué ventajas ofrece la DI?

Decir que es útil no basta, así que he resumido qué cambia realmente.

  • Es más fácil sustituir componentes: aunque cambies KakaoPay por NaverPay, OrderService no requiere modificar ni una línea. Solo cambias el objeto que introduces.
  • Las pruebas son más sencillas: puedes introducir un objeto de pago falso (Mock) en lugar del real y validar solo la lógica sin realizar cargos.
  • Las responsabilidades se separan: OrderService se centra únicamente en “procesar pedidos”, mientras que el exterior decide “qué método de pago usar”.

El tercer punto es el que más noto. Cuando una clase se concentra solo en su trabajo, el código resulta mucho más fácil de leer.

En resumen:

Categoría Creación dentro Creación fuera (DI)
Sustitución del pago Hay que modificar la clase Solo se sustituye el objeto introducido
Pruebas No se puede introducir un objeto falso Se puede inyectar un Mock
Responsabilidades Creación y lógica mezcladas Centrarse solo en la lógica
Al sacar new fuera, el código queda así de limpio
Al sacar new fuera, el código queda así de limpio

Entonces, ¿por qué hacen falta bibliotecas de DI como Swinject?

Aquí surge una pregunta natural: “Entiendo que se introduce desde fuera, pero ¿quién gestiona ese ‘fuera’?”

Si solo hay unos pocos objetos, puedes crearlos e introducirlos manualmente. Pero los proyectos reales tienen cientos de objetos relacionados entre sí.

Que una persona los cree y los introduzca uno a uno, en el orden correcto, es un infierno.

Por eso aparecieron herramientas que realizan esa “creación e introducción desde fuera”. Los contenedores de DI del ecosistema Swift, como Swinject y Factory, cumplen exactamente esa función.

El contenedor crea los objetos y los coloca donde hacen falta
El contenedor crea los objetos y los coloca donde hacen falta
Cuando hay cientos de objetos, agradeces que un contenedor los gestione por ti
Cuando hay cientos de objetos, agradeces que un contenedor los gestione por ti

Registra los objetos que necesitas y el contenedor los crea y los introduce donde corresponde. Casi nunca tendrás que crearlos directamente.

Aquí aparece también la conocida “inversión de control (IoC)”. Significa que el control de crear y conectar objetos pasa de tu código al contenedor.

En pocas palabras, la DI es el concepto y el contenedor de DI es el trabajador que lo ejecuta por ti.


Preguntas frecuentes

P. ¿DI e IoC son lo mismo? No. IoC (inversión de control) es el principio amplio de ceder el control, mientras que la DI es una forma concreta de llevarlo a cabo. La DI es un tipo de IoC.

P. ¿Hay métodos de inyección además del constructor? Sí. Existen la inyección por constructor, por propiedad y por método. Sin embargo, en Swift se recomienda la inyección por constructor. Las dependencias pueden fijarse con let, no cambian y las pruebas resultan más sencillas.

P. ¿Hace falta una biblioteca para que sea DI? No. Pasar manualmente un objeto al constructor, como acabamos de hacer, también es DI. Las bibliotecas solo lo automatizan.


Hoy hemos explicado la inyección de dependencias como un cambio muy pequeño: modificar el lugar donde se crea un objeto.

Si la DI te parece difícil, recuerda solo esto: “No lo crees dentro de la clase; créalo fuera e introdúcelo”. Todo lo demás se deduce de forma natural.

Si alguna vez te has quedado bloqueado al escribir pruebas, te recomiendo cambiar manualmente el ejemplo de hoy. Lo entenderás mucho mejor que mirándolo sin más.

Seguir leyendo