Preguntas frecuentes sobre DI (FAQ)

¿Es DI otro nombre para IoC?

Inversion of Control (IoC) es un principio que describe el flujo de control de un programa: ¿es su código el que llama a código externo o es el código externo (como un framework) el que llama al suyo? IoC es un concepto amplio que incluye los eventos, el llamado principio de Hollywood y otros aspectos. Este concepto abarca también las factories, de las que habla la Rule #3: Let the Factory Handle It, y que representan una inversión del operador new.

Dependency Injection (DI) se centra en cómo obtienen los objetos sus dependencias (es decir, los otros objetos con los que necesitan trabajar). Es un patrón de diseño que aboga por pasar las dependencias explícitamente a los objetos en lugar de que estos las creen o las busquen.

Por tanto, la DI se puede considerar una forma concreta de IoC. Pero no todas las formas de IoC fomentan un código limpio. Son antipatrones, por ejemplo, las técnicas basadas en el estado global o en el patrón Service Locator.

¿Qué es un Service Locator?

Es un enfoque alternativo a la inyección de dependencias. Consiste en un objeto central (el locator) en el que se registran todos los servicios (dependencias) disponibles. Cuando un objeto necesita una dependencia, se la pide al Service Locator.

Comparado con la DI, sin embargo, le falta transparencia. Las dependencias quedan escondidas dentro del código del objeto (las llamadas al locator) en lugar de ser explícitas en su API (constructor o métodos), lo que obliga a inspeccionar el código para entender las conexiones. Las pruebas también son más complicadas, porque no puede simplemente pasar dependencias simuladas al crear el objeto; a menudo hay que manipular el propio Service Locator. Además, el Service Locator introduce una dependencia innecesaria: los objetos quedan acoplados al locator, a diferencia de lo que ocurre con la DI, donde lo ideal es que los objetos ni siquiera sepan del contenedor.

¿Cuándo es mejor no usar DI?

No se conocen inconvenientes significativos de usar correctamente el patrón de diseño de la inyección de dependencias. Al contrario, obtener las dependencias de lugares accesibles globalmente (como propiedades estáticas o singletons) trae numerosas complicaciones, igual que usar un Service Locator. Por eso usar DI es en general siempre recomendable. No es un dogma; simplemente no se ha impuesto ninguna alternativa mejor para gestionar las dependencias de forma limpia.

Hay, sin embargo, situaciones concretas y limitadas en las que acceder a los objetos globalmente puede ser aceptable. Por ejemplo, durante la depuración, cuando necesita volcar el valor de una variable, medir el tiempo de ejecución o registrar un mensaje en un punto concreto. En esos casos, que implican acciones temporales que después se eliminarán del código, usar un dumper, un cronómetro o un logger accesible globalmente puede ser legítimo. Esas herramientas no forman parte del diseño esencial de la aplicación.

¿Tiene inconvenientes usar DI?

¿Usar la inyección de dependencias trae desventajas, como más esfuerzo al escribir código o peor rendimiento? ¿Qué perdemos cuando empezamos a escribir código conforme a la DI?

La DI en sí tiene un impacto insignificante en el rendimiento en tiempo de ejecución o en el consumo de memoria. El rendimiento del contenedor DI sí puede influir, pero Nette DI compila el contenedor a código PHP puro, con lo que la sobrecarga durante la ejecución de la aplicación es prácticamente nula.

Al escribir código siguiendo los principios de la DI, a menudo hay que crear constructores que aceptan dependencias. Aunque en el pasado eso pudiera parecer tedioso, los IDE modernos y características como la promoción de propiedades del constructor de PHP 8 lo hacen muy rápido. Las factories las puede generar a menudo Nette DI automáticamente, lo que reduce aún más el código repetitivo. Por otro lado, desaparece la necesidad de escribir singletons y puntos de acceso estáticos.

En conjunto, una aplicación bien diseñada que usa DI no suele ser ni notablemente más corta ni más larga que una que se apoya en singletons o en el acceso global. El código relacionado con la creación y la conexión de las dependencias simplemente se traslada de las clases individuales a lugares dedicados: la configuración del contenedor DI y las factories.

¿Cómo reescribir una aplicación heredada para que use DI?

Migrar una aplicación heredada a la inyección de dependencias puede ser un proceso exigente, sobre todo en aplicaciones grandes y complejas. Es importante abordar el proceso de forma sistemática.

  • Al pasar a la inyección de dependencias es importante que todos los miembros del equipo entiendan los principios y las prácticas que se van a usar.
  • Primero, analice la aplicación existente para identificar los componentes clave y sus dependencias. Cree un plan sobre qué partes se refactorizarán y en qué orden.
  • Implemente un contenedor DI o, mejor aún, use una biblioteca existente como Nette DI.
  • Refactorice poco a poco las partes de la aplicación para que usen la inyección de dependencias. Eso puede implicar modificar constructores o métodos para que acepten las dependencias como parámetros.
  • Actualice el código donde se crean los objetos para obtenerlos del contenedor o usar las factories que este proporciona.

Recuerde que pasar a la inyección de dependencias es una inversión en la calidad del código y en la mantenibilidad a largo plazo de la aplicación. Aunque hacer estos cambios pueda resultar exigente, el resultado debería ser un código más limpio, más modular y fácilmente testeable, preparado para futuras ampliaciones y para su mantenimiento.

¿Por qué se prefiere la composición a la herencia?

Usar la composición se prefiere en general a la herencia para reutilizar código, porque lleva a un acoplamiento más laxo. Con la composición es menos probable que se encuentre con problemas en los que cambiar una clase base rompe las subclases dependientes. Un ejemplo típico es la situación conocida como infierno del constructor.

¿Se puede usar el contenedor DI de Nette fuera de Nette?

Por supuesto. El contenedor DI de Nette forma parte de Nette, pero está diseñado como una biblioteca independiente que se puede usar al margen de las demás partes del framework. Basta con instalarlo con Composer, crear un archivo de configuración que defina sus servicios y después usar unas pocas líneas de código PHP para crear el contenedor DI. Y puede empezar de inmediato a aprovechar la inyección de dependencias en sus proyectos.

El capítulo sobre el contenedor DI de Nette describe un caso de uso concreto con ejemplos de código.

¿Por qué la configuración está en archivos NEON?

NEON es un lenguaje de configuración sencillo y fácil de leer, desarrollado dentro de Nette para configurar aplicaciones, servicios y sus dependencias. Comparado con JSON o YAML ofrece para ese fin opciones mucho más intuitivas y flexibles. En NEON puede describir con naturalidad definiciones de servicios y relaciones que en JSON o YAML sería difícil o imposible expresar con la misma claridad.

¿Ralentiza la aplicación el análisis de los archivos NEON?

Aunque los archivos NEON se analizan muy rápido, su velocidad de análisis es en gran medida irrelevante en producción. Y es que los archivos de configuración se analizan una sola vez, la primera vez que la aplicación se ejecuta (o cuando cambian). Tras el análisis se genera el código del contenedor DI, se guarda en caché (en disco) y ese código PHP compilado se ejecuta en cada petición posterior, con lo que no hace falta ningún análisis más.

Así funciona en un entorno de producción. Durante el desarrollo, los archivos NEON se analizan cada vez que cambia su contenido, lo que garantiza que el desarrollador tenga siempre un contenedor DI actualizado. Como ya se ha dicho, el análisis en sí es muy rápido.

¿Cómo accedo desde mi clase a los parámetros del archivo de configuración?

Tenga presente la Rule #1: Let It Be Passed to You. Si una clase necesita información del archivo de configuración, no intente averiguar cómo puede la clase conseguirla. Simplemente pídala, por ejemplo mediante el constructor de la clase. Y después proporcione ese valor en el archivo de configuración.

En este ejemplo, %myParameter% es un marcador del valor del parámetro myParameter, que se pasará al constructor de MyClass:

# config.neon
parameters:
	myParameter: Some value

services:
	- MyClass(%myParameter%)

Si quiere pasar varios parámetros o usar autowiring, conviene envolver los parámetros en un objeto.

¿Soporta Nette la interfaz Container de PSR-11?

El contenedor DI de Nette no soporta PSR-11 directamente. Pero si necesita interoperabilidad entre el contenedor DI de Nette y bibliotecas o frameworks que esperan la interfaz Container de PSR-11, puede crear un adaptador sencillo que sirva de puente entre el contenedor DI de Nette y PSR-11.

¿Qué significan los términos container, compiler, definition, etc.?

Un breve vocabulario de las palabras que aparecen una y otra vez alrededor de Nette DI, la mayoría al escribir extensiones:

  • Container (contenedor): el objeto compilado (Nette\DI\Container) que crea los servicios cuando se piden y los mantiene en tiempo de ejecución. Se genera una vez como código PHP optimizado.
  • Compiler (compilador): la maquinaria que convierte los archivos de configuración y las extensiones en esa clase del contenedor.
  • ContainerBuilder: el modelo mutable del contenedor usado durante la compilación; contiene las definiciones de los servicios antes de que exista ningún servicio real. Véase Creación de extensiones.
  • Service (servicio): un objeto gestionado por el contenedor, normalmente creado una vez y compartido (un singleton): una conexión a la base de datos, un mailer, un logger.
  • Definition (definición): la receta de un servicio: su tipo, cómo crearlo y qué hacer después. Nette convierte las definiciones en los métodos factory del contenedor; existen varios tipos (véase tipos de definición).
  • Type (tipo): la clase o interfaz de un servicio, que el autowiring usa para casar los servicios con los lugares que los requieren.
  • Autowiring: pasar automáticamente los servicios a los constructores y métodos según su tipo, para que no tenga que conectar las dependencias a mano.
  • Tag (etiqueta): una marca adjunta a una definición (opcionalmente con un valor); una extensión puede después encontrar con findByTag() todos los servicios que la llevan.
  • Setup: llamadas adicionales que se realizan sobre un servicio justo después de crearlo: llamadas a métodos o asignaciones a propiedades, añadidas con addSetup().
  • Alias: un nombre alternativo para un servicio existente.
versión: 3.x