¿Qué es la inyección de dependencias?

Este capítulo te introduce a las prácticas básicas de programación que debes seguir al escribir cualquier aplicación. Estos son los fundamentos necesarios para escribir código limpio, comprensible y mantenible.

Si aprendes y sigues estas reglas, el Nette estará ahí para ti en cada paso del camino. Se encargará de las tareas rutinarias por ti y te hará sentir lo más cómodo posible para que puedas centrarte en la propia lógica.

Los principios que mostraremos aquí son bastante simples. No tienes nada de qué preocuparte.

¿Recuerdas tu primer programa?

No tenemos idea en qué lenguaje lo escribiste, pero si fuera PHP, probablemente se vería algo como esto:

function suma(float $a, float $b): float
{
	return $a + $b;
}

echo suma(23, 1); // imprime 24

Unas pocas líneas de código triviales, pero en las que se esconden muchos conceptos clave. Vemos que hay variables. Que el código se descompone en unidades más pequeñas, que son funciones, por ejemplo. Que les pasamos argumentos de entrada y devuelven resultados. Lo único que falta son condiciones y bucles.

El hecho de que pasemos argumentos de entrada a una función y ésta devuelva un resultado es un concepto perfectamente comprensible que se utiliza en otros campos, como las matemáticas.

Una función tiene una firma, que consiste en su nombre, una lista de parámetros y sus tipos y, por último, el tipo de valor de retorno. Como usuarios, lo que nos interesa es la firma; normalmente no necesitamos saber nada sobre la implementación interna.

Ahora imagina que la firma de una función tiene este aspecto

function suma(float $x): float

¿Una suma con un solo parámetro? Eso es raro… ¿Qué tal esto?

function suma(): float

Eso sí que es raro, ¿no? ¿Cómo crees que se usa la función?

echo suma(); // ¿Qué imprime?

Mirando este código, estamos confundidos. No sólo un principiante no lo entendería, incluso un programador experto no entendería tal código.

¿Nos preguntamos cómo sería una función así por dentro? ¿De dónde sacaría los sumandos? Probablemente los obtendría de alguna manera por sí misma, así:

function suma(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

Resulta que hay enlaces ocultos a otras funciones (o métodos estáticos) en el cuerpo de la función, y para averiguar de dónde vienen realmente las sumas, tenemos que indagar más.

Así no!

El diseño que acabamos de mostrar es la esencia de muchas características negativas:

  • la firma de la función pretendía que no necesitaba sumandos, lo que nos confundió.
  • no tenemos ni idea de cómo hacer que la función calcule con otros dos números.
  • tuvimos que mirar en el código para ver de dónde toma los sumandos.
  • descubrimos ligaduras ocultas.
  • para entenderlo completamente, necesitamos explorar también estas ligaduras.

¿Y acaso la función de suma se encarga de obtener entradas? Por supuesto que no. Su única responsabilidad es sumar.

No queremos encontrarnos con un código así, y desde luego no queremos escribirlo. El remedio es simple: volver a lo básico y usar sólo parámetros:

function suma(float $a, float $b): float
{
	return $a + $b;
}

Regla nº 1: Déjalo que te lo pasen

La regla más importante es: todos los datos que necesiten las funciones o clases deben pasárseles.

En lugar de inventar mecanismos ocultos para que de alguna manera lleguen a ellos por sí mismos, simplemente pásales los parámetros. Ahorrarás el tiempo que lleva inventar mecanismos ocultos, que definitivamente no mejorarán tu código.

Si sigues esta regla siempre y en todas partes, estarás en camino hacia un código sin enlaces ocultos. Hacia un código que sea comprensible no sólo para el autor, sino también para cualquiera que lo lea después. Donde todo es comprensible desde las firmas de funciones y clases y no hay necesidad de buscar secretos ocultos en la implementación.

Esta técnica se denomina de forma experta inyección de dependencias. Y los datos se llaman dependencias. Pero es un simple paso de parámetros, nada más.

Por favor, no confundas la inyección de dependencias, que es un patrón de diseño, con un “contenedor de inyección de dependencias”, que es una herramienta, algo diametralmente distinto. Nos ocuparemos de los contenedores más adelante.

De las funciones a las clases

¿Y cómo se relacionan las clases con esto? Una clase es una entidad más compleja que una simple función, pero la regla #1 se aplica aquí también. Simplemente hay más formas de pasar argumentos. Por ejemplo, bastante similar al caso de una función:

class Matematicas
{
	public function suma(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Matematicas;
echo $math->suma(23, 1); // 24

O usando otros métodos, o el constructor directamente:

class Suma
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}

}

$suma = new Suma(23, 1);
echo $suma->calculate(); // 24

Ambos ejemplos se ajustan completamente a la inyección de dependencias.

Ejemplos de la vida real

En el mundo real, no escribirás clases para sumar números. Pasemos a ejemplos de la vida real.

Tengamos una clase Article que represente un artículo de blog:

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// guardar el artículo en la base de datos
	}
}

y el uso será el siguiente

$article = new Article;
$article->title = '10 cosas que debe saber sobre la pérdida de peso';
$article->content = 'Cada año, millones de personas en ...';
$article->save();

El método save() almacenará el artículo en una tabla de la base de datos. Implementarlo usando Nette Database sería pan comido, si no fuera por una pega: ¿de dónde saca Article la conexión a la base de datos, es decir, el objeto de clase Nette\Database\Connection?

Parece que tenemos muchas opciones. Puede tomarla de alguna variable estática. O heredar de la clase que proporcionará la conexión a la base de datos. O aprovechar un llamado singleton. O las llamadas facades que se usan en Laravel:

use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

Genial, hemos resuelto el problema.

¿O no?

Recordemos la regla nº 1: déjalo que te lo pasen: todas las dependencias que necesite la clase deben pasársele. Porque si no lo hacemos, y rompemos la regla, habremos empezado el camino hacia un código sucio lleno de bindings ocultos, incomprensibilidad, y el resultado será una aplicación que será un dolor de mantener y desarrollar.

El usuario de la clase Article no tiene ni idea de dónde almacena el método save() el artículo. ¿En una tabla de la base de datos? ¿En cuál, en producción o en desarrollo? ¿Y cómo se puede cambiar esto?

El usuario tiene que mirar cómo está implementado el método save() para encontrar el uso del método DB::insert(). Así que tiene que buscar más para averiguar cómo este método procura una conexión a la base de datos. Y los enlaces ocultos pueden formar una cadena bastante larga.

Los enlaces ocultos, las facades de Laravel o las variables estáticas nunca están presentes en un código limpio y bien diseñado. En código limpio y bien diseñado, los argumentos se pasan:

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Aún más práctico, como veremos a continuación, es utilizar un constructor:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Si usted es un programador experimentado, podría pensar que Article no debería tener un método save() en absoluto; debería representar un componente puramente de datos, y un repositorio separado debería encargarse de guardar. Eso tiene sentido. Pero eso nos llevaría mucho más allá del alcance del tema, que es la inyección de dependencias, y del esfuerzo por proporcionar ejemplos sencillos.

Si vas a escribir una clase que requiere una base de datos para funcionar, por ejemplo, no te imagines de dónde obtenerla, sino que te la pasen. Quizá como parámetro de un constructor u otro método. Declara las dependencias. Exponlas en la API de tu clase. Conseguirás un código comprensible y predecible.

Qué tal esta clase que registra mensajes de error:

class Logger
{
	public function log(string $message)
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

¿Qué le parece, hemos seguido la regla nº 1: déjalo que te lo pasen?

No lo hemos hecho.

La información clave, el directorio del archivo de registro, es obtenida por la clase a partir de la constante.

Vea el ejemplo de uso:

$logger = new Logger;
$logger->log('La temperatura es 23 °C');
$logger->log('La temperatura es 10 °C');

Sin conocer la implementación, ¿podrías responder a la pregunta de dónde se escriben los mensajes? ¿Te parecería necesaria la existencia de la constante LOG_DIR para que funcione? ¿Y sería capaz de crear una segunda instancia que escribiera en una ubicación diferente? Desde luego que no.

Arreglemos la clase:

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

La clase es ahora mucho más clara, configurable y por tanto más útil.

$logger = new Logger('/path/to/log.txt');
$logger->log('The temperature is 15 °C');

Pero no me importa!

“Cuando creo un objeto Article y llamo a save(), no quiero tratar con la base de datos, sólo quiero que se guarde en la que he establecido en la configuración.”

“Cuando uso Logger, sólo quiero que se escriba el mensaje, y no quiero ocuparme de dónde. Que se use la configuración global.”

Estos comentarios son correctos.

Como ejemplo, tomemos una clase que envía boletines y registra cómo ha ido:

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Se han enviado correos electrónicos');

		} catch (Exception $e) {
			$logger->log('Se ha producido un error durante el envío');
			throw $e;
		}
	}
}

La mejorada Logger, que ya no utiliza la constante LOG_DIR, requiere una ruta de archivo en el constructor. ¿Cómo resolver esto? A la clase NewsletterDistributor no le importa dónde se escriben los mensajes, sólo quiere escribirlos.

La solución es de nuevo la regla nº 1: déjalo que te lo pasen: pásale todos los datos que la clase necesite.

Así que pasamos la ruta al log al constructor, que luego usamos para crear el objeto Logger?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ ¡ASÍ NO!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Pues no. Porque la ruta no pertenece a los datos que la clase NewsletterDistributor necesita; necesita Logger. La clase necesita el propio logger. Y eso es lo que vamos a pasar:

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Se han enviado correos electrónicos');

		} catch (Exception $e) {
			$this->logger->log('Se ha producido un error durante el envío');
			throw $e;
		}
	}
}

Ahora está claro por las firmas de la clase NewsletterDistributor que el registro es parte de su funcionalidad. Y la tarea de reemplazar el logger por otro, quizás con fines de prueba, es bastante trivial. Además, si se cambia el constructor de la clase Logger, no tendrá ningún efecto en nuestra clase.

Regla nº 2: Toma lo que es tuyo

No te dejes engañar y no te dejes pasar las dependencias de tus dependencias. Sólo pasa tus propias dependencias.

Esto hará que el código que utilice otros objetos sea completamente independiente de los cambios en sus constructores. Su API será más verdadera. Y lo más importante, será trivial cambiar esas dependencias por otras.

Nuevo miembro de la familia

El equipo de desarrollo decidió crear un segundo registrador que escribe en la base de datos. Así que creamos una clase DatabaseLogger. Así que tenemos dos clases, Logger y DatabaseLogger, una escribe en un archivo, la otra en una base de datos … ¿no te parece extraña la nomenclatura? ¿No sería mejor renombrar Logger a FileLogger? Desde luego que sí.

Pero hagámoslo de forma inteligente. Creamos una interfaz con el nombre original:

interface Logger
{
	function log(string $message): void;
}

… que ambos registradores implementarán:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

Y debido a esto, no habrá necesidad de cambiar nada en el resto del código donde se utilice el logger. Por ejemplo, el constructor de la clase NewsletterDistributor seguirá conformándose con requerir Logger como parámetro. Y dependerá de nosotros qué instancia le pasemos.

Por eso nunca añadimos el sufijo Interface o el prefijo I a los nombres de las interfaces. De lo contrario, no sería posible desarrollar el código de forma tan agradable.

Houston, tenemos un problema

Mientras que podemos arreglárnoslas con una única instancia del registrador, ya sea basado en archivos o en bases de datos, a lo largo de toda la aplicación y simplemente pasarlo allí donde se registre algo, es bastante diferente para la clase Article. Creamos sus instancias según sea necesario, incluso varias veces. ¿Cómo tratar la dependencia de la base de datos en su constructor?

Un ejemplo puede ser un controlador que debe guardar un artículo en la base de datos después de enviar un formulario:

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Una posible solución es obvia: pasar el objeto de base de datos al constructor EditController y utilizar $article = new Article($this->db).

Al igual que en el caso anterior con Logger y la ruta del archivo, este no es el enfoque correcto. La base de datos no es una dependencia de EditController, sino de Article. Pasar la base de datos va en contra de la regla #2: toma lo que es tuyo. Si el constructor de la clase Article cambia (se añade un nuevo parámetro), tendrás que modificar el código allí donde se creen instancias. Ufff.

Houston, ¿qué sugieres?

Regla nº 3: Deje que se encargue la fábrica

Al eliminar las dependencias ocultas y pasar todas las dependencias como argumentos, hemos conseguido clases más configurables y flexibles. Y por lo tanto, necesitamos algo más para crear y configurar esas clases más flexibles para nosotros. Lo llamaremos fábricas.

La regla general es: si una clase tiene dependencias, deja la creación de sus instancias a la fábrica.

Las fábricas son un sustituto más inteligente del operador new en el mundo de la inyección de dependencias.

Por favor, no confundir con el patrón de diseño método de fábrica, que describe una forma específica de utilizar las fábricas y no está relacionado con este tema.

Fábrica

Una fábrica es un método o clase que produce y configura objetos. Llamamos Article a la clase productora ArticleFactory y podría tener este aspecto:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Su uso en el controlador sería el siguiente:

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// dejar que la fábrica cree un objeto
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

En este punto, cuando la firma del constructor de la clase Article cambia, la única parte del código que necesita responder es la propia fábrica ArticleFactory. Cualquier otro código que trabaje con objetos Article, como EditController, no se verá afectado.

Puede que ahora mismo te estés dando golpecitos en la frente preguntándote si nos hemos ayudado a nosotros mismos en algo. La cantidad de código ha crecido y todo empieza a parecer sospechosamente complicado.

No te preocupes, pronto llegaremos al contenedor Nette DI. Y tiene una serie de ases en la manga que harán que construir aplicaciones usando inyección de dependencias sea extremadamente sencillo. Por ejemplo, en lugar de la clase ArticleFactory, bastará con escribir una simple interfaz:

interface ArticleFactory
{
	function create(): Article;
}

Pero nos estamos adelantando, espera :-)

Resumen

Al principio de este capítulo, prometimos mostrarte una forma de diseñar código limpio. Basta con dar a las clases

A primera vista, puede que estas tres reglas no parezcan tener consecuencias de gran alcance, pero conducen a una perspectiva radicalmente distinta del diseño de código. ¿Merece la pena? Los desarrolladores que han abandonado viejos hábitos y han empezado a utilizar sistemáticamente la inyección de dependencias consideran este paso un momento crucial en su vida profesional. Les ha abierto el mundo de las aplicaciones claras y mantenibles.

Pero, ¿qué ocurre si el código no utiliza sistemáticamente la inyección de dependencias? ¿Y si se basa en métodos estáticos o singletons? ¿Causa problemas? Sí, los hay, y muy fundamentales.

versión: 3.x