¿Qué es la inyección de dependencias?

Este capítulo te introduce a las prácticas básicas de programación que sustentan todo el framework Nette y que deberías seguir cuando escribas tus propias aplicaciones. Estos son los fundamentos necesarios para escribir código limpio, comprensible y mantenible.

Si aprendes y sigues estas reglas, el framework 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 addition(float $a, float $b): float
{
	return $a + $b;
}

echo addition(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 addition(float $x): float

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

function addition(): float

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

echo addition(); // ¿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 addition(): 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 addition(float $a, float $b): float
{
	return $a + $b;
}

Regla #1: Utiliza parámetros

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

Si rompemos esta regla, será imposible hacer el código comprensible, limpio y sostenible.

Si la seguimos, iremos hacia un código sin restricciones ocultas. Hacia un código comprensible no sólo para el autor, sino para cualquiera que lo lea después. Donde todo es comprensible a partir de las firmas de funciones y clases y no hay necesidad de buscar secretos ocultos en la implementación.

Esta técnica de pasar argumentos se llama técnicamente inyección de dependencia.

(No confundas la inyección de dependencias con un “contenedor de inyección de dependencias”; es algo radicalmente diferente, y cubriremos los contenedores en próximo capítulo).

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 Math
{
	public function addition(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->addition(23, 1); // 24

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

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

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

}

$addition = new Addition(23, 1);
echo $addition->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 regla #1: usar parámetros: tenemos que pasarles todos los datos que necesite la clase. Porque si no lo hacemos, y rompemos la regla, hemos empezado el camino hacia un código sucio lleno de enlaces ocultos, incomprensibilidad, y el resultado será una aplicación que es 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 vas a escribir una clase que requiera una base de datos, por ejemplo, no averigües de dónde obtenerla, sino deja que te la pasen. Quizás como parámetro a un constructor u otro método. Declara dependencias. Exponlas en la API de tu clase. Obtendrás un código comprensible y predecible.

Qué tal esta clase que registra mensajes de error:

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

¿Qué piensas, hemos seguido la regla #1: usar parámetros?

No lo hemos hecho.

La clase obtiene la información clave, el directorio que contiene el fichero log, de una constante.

Vea un 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 LogDir 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)
	{
		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;
		}
	}
}

Sin embargo, el nuevo Logger, que ya no usa la constante LogDir, requiere la ruta al fichero en el constructor. ¿Cómo solucionarlo? A la clase NewsletterDistributor no le importa dónde se escriben los mensajes, sólo quiere escribirlos.

La solución es de nuevo regla #1: usar parámetros: pasarle todos los datos que la clase necesite.

¿Así que pasamos la ruta al log al constructor, que luego usamos al crear el objeto Logger? No. Porque la ruta no es el dato que necesita la clase NewsletterDistributor; eso es lo que necesita Logger. La clase necesita el propio logger. Y se lo 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 tienes la opción de reemplazar el logger por otro.

Mientras que en toda la aplicación podemos estar contentos con una única instancia del logger y pasarlo allí donde se registre algo, es diferente en el caso de la clase Article. Querremos crear múltiples instancias de ella. ¿Cómo tratar la dependencia de la base de datos en el constructor? Como ejemplo, tomemos un controlador que se supone que guarda un artículo en la base de datos después de enviar un formulario:

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

Se sugiere una posible solución: pasar el objeto base de datos al UserController por el constructor y utilizar $article = new Article($this->db).

Como en el caso anterior, esta no es la práctica correcta. La base de datos no es una dependencia de UserController, sino de Article. Además, en el momento en que el constructor de la clase Article se modifique de alguna forma (se añada un nuevo parámetro), tendremos que modificar el código en todos los lugares donde se creen instancias.

La solución son las fábricas.

Regla #2: Usar fábricas

Al eliminar los enlaces ocultos y pasar todos los datos como argumentos, obtenemos clases más configurables y flexibles. Por lo tanto, todavía necesitamos algo para crear y configurar esas clases más flexibles. Lo llamaremos una fábrica.

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.

Fábrica

Una fábrica es una clase que crea y configura objetos. La fábrica que produce Article se llamará ArticleFactory y su uso en el controlador será el siguiente:

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

	public function formSubmitted($data)
	{
		// let the factory create an object
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Una implementación de fábrica podría tener este aspecto

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

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

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

Puede que ahora te estés dando golpecitos en la frente preguntándote cómo nos hemos ayudado a nosotros mismos. La cantidad de código ha crecido y se ha movido del controlador a una clase separada. Sin embargo, Nette DI tiene un as escondido en la manga. Entiende el concepto de fábricas e incluso puede escribir un servicio de este tipo para nosotros. Así que en lugar de la clase ArticleFactory, podríamos simplemente crear una interfaz:

interface ArticleFactory
{
	function create(): Article;
}

Pero nos estamos adelantando un poco, llegaremos a eso en un minuto.

Resumen

Al principio de este capítulo, prometimos demostrar un principio simple de cómo diseñar aplicaciones. Aunque el principio en sí es simple (dar a las clases los datos que necesitan), lo que sigue de él requiere más reflexión. Siéntete libre de leer este capítulo varias veces.

Los programadores que han desechado viejos hábitos y han empezado a utilizar la inyección de dependencias lo consideran sistemáticamente un momento crucial en su vida profesional. Abrió un mundo de aplicaciones claras y sostenibles.

Ahora veremos qué es Contenedor de Inyección de Dependencias.