¿Qué es la inyección de dependencias?

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

Si adopta y sigue estas reglas, Nette le apoyará a cada paso. Se ocupará por usted de las tareas rutinarias y le dará la máxima comodidad para que pueda concentrarse en la lógica misma.

Los principios que mostraremos aquí son bastante sencillos. No hay nada que temer.

¿Recuerda su primer programa?

No sabemos en qué lenguaje lo escribió, pero si fue en PHP, probablemente tenía este aspecto:

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

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

Unas pocas líneas de código triviales y, sin embargo, esconden un montón de conceptos clave. Que existen las variables. Que el código se divide en unidades más pequeñas, como las funciones. Que les pasamos argumentos de entrada y devuelven resultados. Solo faltan las condiciones y los bucles.

Que a una función le pasemos datos de entrada y nos devuelva un resultado es un concepto perfectamente comprensible, usado también en otros campos, como las matemáticas.

Una función tiene su firma, formada por su nombre, la lista de parámetros y sus tipos y, por último, el tipo del valor devuelto. Como usuarios nos interesa la firma; de la implementación interna no solemos necesitar saber nada.

Ahora imagine que la firma de la función tuviera este aspecto:

function addition(float $x): float

¿Una suma con un solo parámetro? Qué raro… ¿Y qué tal esta?

function addition(): float

Ahora sí que es raro, ¿verdad? ¿Cómo se usa esta función?

echo addition(); // ¿qué imprimirá?

Al ver un código así nos quedaríamos desconcertados. No solo no lo entendería un principiante: tampoco lo entendería un programador experto.

¿Se pregunta qué aspecto tendría por dentro una función así? ¿De dónde sacaría los números que hay que sumar? Probablemente se los procuraría de alguna manera ella misma, quizá así:

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

En el cuerpo de la función hemos descubierto dependencias ocultas de otras funciones globales o métodos estáticos. Para averiguar de dónde salen realmente los números hay que seguir investigando.

¡Por ahí no!

El diseño que acabamos de mostrar es la esencia de muchas propiedades negativas:

  • La firma de la función fingía que no necesitaba los números que sumar, lo que nos desconcertó.
  • No tenemos ni idea de cómo hacer que la función sume dos números distintos.
  • Hemos tenido que mirar el código para averiguar de dónde saca los números.
  • Hemos descubierto dependencias ocultas.
  • Para entenderlo del todo hay que examinar también esas dependencias.

¿Y es acaso tarea de la función de suma obtener las entradas? Por supuesto que no. Su responsabilidad es solo la suma misma.

No queremos toparnos con un código así, y desde luego no queremos escribirlo. La solución es sencilla: volver a lo básico y usar simplemente parámetros:

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

Regla n.º 1: deje que se lo pasen

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

En lugar de inventar formas ocultas de que ellas mismas obtengan los datos, pase simplemente los parámetros. Ahorrará el tiempo dedicado a inventar caminos ocultos que desde luego no mejorarán su código.

Si sigue esta regla siempre y en todas partes, va camino de un código sin dependencias ocultas. Camino de un código comprensible no solo para su autor, sino también para cualquiera que lo lea después. Donde todo se entiende a partir de las firmas de las funciones y las clases, y no hace falta buscar detalles ocultos en la implementación.

A esta técnica se la llama profesionalmente inyección de dependencias. Y a los datos se los llama dependencias. Es simplemente pasar parámetros, nada más.

No confunda, por favor, 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 esencialmente distinto. De los contenedores hablaremos más adelante.

De las funciones a las clases

¿Y cómo se aplica esto a las clases? Una clase es una entidad más compleja que una simple función, pero la regla n.º 1 vale aquí igualmente. Solo que hay más maneras de pasar los argumentos. Por ejemplo, de forma bastante parecida al caso de la función:

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

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

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

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

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

$sum = new Sum(23, 1);
echo $sum->calculate(); // 24

Ambos ejemplos cumplen plenamente con la inyección de dependencias.

Ejemplos de la vida real

En el mundo real no escribirá clases para sumar números. Pasemos a ejemplos prácticos.

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

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

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

y su uso será el siguiente:

$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();

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

Parece que tenemos muchas opciones. Podría tomarlo de una variable estática. O heredando de una clase que proporcione la conexión a la base de datos. O usar un singleton. O las llamadas fachadas, como 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: deje que se lo pasen: todas las dependencias que la clase necesita deben serle pasadas. Porque si rompemos la regla, hemos tomado el camino hacia un código sucio, lleno de dependencias ocultas y falta de claridad, y el resultado será una aplicación que costará un mundo mantener y desarrollar.

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

El usuario tiene que mirar cómo está implementado el método save() y encuentra el uso del método DB::insert(). Así que tiene que seguir investigando cómo obtiene ese método la conexión a la base de datos. Y las dependencias ocultas pueden formar una cadena bastante larga.

En un código limpio y bien diseñado nunca hay dependencias ocultas, fachadas de Laravel ni variables estáticas. En un código limpio y bien diseñado se pasan argumentos:

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 más adelante, es usar el 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 es usted un programador con experiencia, quizá piense que Article no debería tener el método save() en absoluto; que debería representar puramente una estructura de datos y que del guardado debería ocuparse un repositorio aparte. Tiene sentido. Pero eso nos llevaría mucho más allá del alcance del tema, que es la inyección de dependencias, y del objetivo de dar ejemplos sencillos.

Si escribe una clase que necesita para funcionar, por ejemplo, una base de datos, no invente de dónde sacarla: haga que se la pasen. Quizá como parámetro del constructor o de otro método. Reconozca las dependencias. Reconózcalas en la API de su clase. Obtendrá un código comprensible y previsible.

¿Y qué tal esta clase, que registra mensajes de error?

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

¿Qué opina, hemos seguido la Regla n.º 1: deje que se lo pasen?

No la hemos seguido.

La información clave, el directorio con el archivo de registro, se la procura la propia clase a partir de una constante.

Mire el ejemplo de uso:

$logger = new Logger;
$logger->log('Temperature is 23 °C');
$logger->log('Temperature is 10 °C');

Sin conocer la implementación, ¿sabría decir dónde se escriben los mensajes? ¿Se le habría ocurrido que para su funcionamiento hace falta que exista la constante LOG_DIR? ¿Y podría crear una segunda instancia que escribiera en otro sitio? 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 comprensible, configurable y, por tanto, más útil.

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

¡Pero a mí eso me da igual!

“Cuando creo un objeto Article y llamo a save(), no quiero ocuparme de la base de datos; solo quiero que se guarde en la que tengo configurada.”

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

Son observaciones válidas.

Como ejemplo, mostremos una clase que distribuye boletines y registra el resultado:

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Emails have been sent out');

		} catch (Exception $e) {
			$logger->log('An error occurred during sending');
			throw $e;
		}
	}
}

El Logger mejorado, que ya no usa la constante LOG_DIR, requiere la ruta al archivo en el constructor. ¿Cómo se resuelve esto? A la clase NewsletterDistributor no le interesa dónde se escriben los mensajes; simplemente quiere registrarlos.

La solución es de nuevo la Regla n.º 1: deje que se lo pasen: le pasamos todos los datos que la clase necesita.

¿Significa eso que pasamos la ruta del registro por el constructor y la usamos después al crear el objeto Logger?

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

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

¡Así no! Porque la ruta no es un dato que necesite la clase NewsletterDistributor; lo necesita el Logger. ¿Percibe la diferencia? La clase NewsletterDistributor necesita el logger en sí. Así que le pasaremos el logger:

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

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Emails have been sent out');

		} catch (Exception $e) {
			$this->logger->log('An error occurred during sending');
			throw $e;
		}
	}
}

Ahora queda claro por la firma de la clase NewsletterDistributor que el registro forma parte de su funcionamiento. Y la tarea de sustituir el logger por otro, quizá para las pruebas, es del todo directa. Además, si cambiara el constructor de la clase Logger, no tendría ningún impacto en nuestra clase.

Regla n.º 2: tome lo que es suyo

No se líe y no acepte las dependencias de sus dependencias. Acepte solo las suyas propias.

Gracias a eso, el código que usa otros objetos será completamente independiente de los cambios en sus constructores. Su API será más precisa. Y, sobre todo, será directo sustituir esas dependencias por otras.

Un nuevo miembro de la familia

El equipo de desarrollo ha decidido crear un segundo logger, uno que escriba en la base de datos. Así que creamos la clase DatabaseLogger. Ahora tenemos dos clases, Logger y DatabaseLogger; una escribe en un archivo, la otra en la base de datos… ¿no le parecen los nombres un poco raros? ¿No sería mejor renombrar Logger a FileLogger? Desde luego.

Pero hagámoslo con cabeza. Creamos una interfaz con el nombre original:

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

… que ambos loggers implementarán:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

Y gracias a eso no hará falta modificar nada en el resto del código donde se usa el logger. Por ejemplo, el constructor de la clase NewsletterDistributor seguirá contentándose con exigir Logger como parámetro. Y de nosotros dependerá qué instancia le proporcionamos.

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

Houston, tenemos un problema

Mientras que en toda la aplicación nos las apañamos con una única instancia del logger, sea de archivo o de base de datos, y basta con pasarla allí donde se registra algo, la situación es bien distinta con la clase Article. Sus instancias las creamos según haga falta, incluso muchas veces. ¿Cómo gestionamos la dependencia de la base de datos en su constructor?

Un ejemplo podría ser un controlador que debe guardar un artículo en la base de datos tras 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 parece obvia: pasemos el objeto de la base de datos por el constructor a EditController y usemos $article = new Article($this->db).

Igual que en el caso anterior con Logger y la ruta al 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 infringe, por tanto, la Rule #2: Take What's Yours. Si cambia el constructor de la clase Article (se añade un nuevo parámetro), tendrá que modificar el código en todos los lugares donde se crean instancias. Uf.

Houston, ¿qué propone?

Regla n.º 3: déjelo en manos de la factory

Al eliminar las dependencias ocultas y pasar todas las dependencias como argumentos, hemos ganado clases más configurables y flexibles. Por eso necesitamos algo más que cree y configure por nosotros esas clases más flexibles. Lo llamaremos factories.

La regla es: si una clase tiene dependencias, delegue la creación de sus instancias en una factory.

Las factories son una alternativa más inteligente al operador new en el mundo de la inyección de dependencias.

No lo confunda, por favor, con el patrón de diseño factory method, que describe una forma concreta de usar las factories y no está relacionado con este tema.

Factory

Una factory es un método o una clase que crea y configura objetos. A la clase que produce Article la llamaremos 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á el siguiente:

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

	public function formSubmitted($data)
	{
		// dejamos que la factory cree el objeto
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Llegados a este punto, si cambia la firma del constructor de la clase Article, la única parte del código que tiene que reaccionar es la propia ArticleFactory. Todo el demás código que trabaja con objetos Article, como EditController, quedará intacto.

Puede que ahora se esté rascando la cabeza preguntándose si de verdad hemos mejorado la situación. La cantidad de código ha crecido y todo el asunto empieza a parecer sospechosamente complejo.

Tranquilo, enseguida llegaremos al contenedor DI de Nette. Y tiene varios ases en la manga que simplificarán enormemente la construcción de aplicaciones con inyección de dependencias. Por ejemplo, en lugar de la clase ArticleFactory bastará con escribir solo una interfaz:

interface ArticleFactory
{
	function create(): Article;
}

Pero nos estamos adelantando, siga atento :-)

Resumen

Al principio de este capítulo prometimos mostrar un procedimiento para diseñar código limpio. Basta con asegurarse de que las clases:

  1. reciban las dependencias que necesitan
  2. y, por el contrario, no reciban lo que no necesitan directamente
  3. y que los objetos con dependencias se creen mejor en factories

A primera vista puede no parecerlo, pero estas tres reglas tienen consecuencias de largo alcance. Llevan a una perspectiva radicalmente distinta sobre el diseño del código. ¿Merece la pena? Los programadores que han abandonado los viejos hábitos y han empezado a usar la inyección de dependencias de forma consecuente consideran ese paso un momento decisivo en su carrera profesional. Les abrió un mundo de aplicaciones claras y mantenibles.

¿Y qué pasa si el código no usa la inyección de dependencias de forma consecuente? ¿Si está construido sobre métodos estáticos o singletons? ¿Trae eso problemas? Sí, y muy grandes.

versión: 3.x