¿Qué es la Inyección de Dependencias?
Este capítulo le introducirá en las prácticas básicas de programación que debe seguir al escribir todas las aplicaciones. Estos son los fundamentos necesarios para escribir código limpio, comprensible y mantenible.
Si adopta estas reglas y las sigue, Nette le ayudará en cada paso del camino. Se encargará de las tareas rutinarias por usted y le proporcionará la máxima comodidad, para que pueda centrarse en la lógica en sí.
Los principios que mostraremos aquí son bastante simples. No tiene que preocuparse por nada.
¿Recuerda su primer programa?
No sabemos en qué lenguaje lo escribió, pero si fue PHP, probablemente se parecía a esto:
function soucet(float $a, float $b): float
{
return $a + $b;
}
echo soucet(23, 1); // imprime 24
Unas pocas líneas triviales de código, pero contienen muchos conceptos clave. Que existen variables. Que el código se divide en unidades más pequeñas, como funciones. Que les pasamos argumentos de entrada y devuelven resultados. Solo faltan las condiciones y los bucles.
El hecho de que pasemos datos de entrada a una función y esta devuelva un resultado es un concepto perfectamente comprensible que se utiliza en otros campos, como las matemáticas.
Una función tiene su firma, que consiste en su nombre, una lista de parámetros y sus tipos, y finalmente el tipo de valor de retorno. Como usuarios, nos interesa la firma, normalmente no necesitamos saber nada sobre la implementación interna.
Ahora imagine que la firma de la función fuera así:
function soucet(float $x): float
¿Una suma con un solo parámetro? Eso es extraño… ¿Y qué tal así?
function soucet(): float
Esto ya es realmente muy extraño, ¿verdad? ¿Cómo se usaría la función?
echo soucet(); // ¿qué imprimirá?
Al ver tal código, estaríamos confundidos. No solo un principiante no lo entendería, sino que incluso un programador experimentado no comprendería este código.
¿Se pregunta cómo sería realmente tal función por dentro? ¿De dónde obtendría los sumandos? Aparentemente, los obtendría de alguna manera por sí misma, tal vez así:
function soucet(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
En el cuerpo de la función, descubrimos dependencias ocultas a otras funciones globales o métodos estáticos. Para averiguar de dónde provienen realmente los sumandos, debemos investigar más a fondo.
¡Por aquí no!
El diseño que acabamos de mostrar es la esencia de muchas características negativas:
- la firma de la función pretendía no necesitar sumandos, lo que nos confundió
- no sabemos en absoluto cómo hacer que la función sume otros dos números
- tuvimos que mirar el código para averiguar de dónde obtenía los sumandos
- descubrimos dependencias ocultas
- para una comprensión completa, también es necesario examinar estas dependencias
¿Y es siquiera tarea de la función de suma obtener las entradas? Por supuesto que no. Su responsabilidad es únicamente la suma en sí.
No queremos encontrarnos con tal código, y definitivamente no queremos escribirlo. La solución es simple: volver a lo básico y simplemente usar parámetros:
function soucet(float $a, float $b): float
{
return $a + $b;
}
Regla nº 1: deja que te lo pasen
La regla más importante es: todos los datos que las funciones o clases necesitan deben serles pasados.
En lugar de inventar formas ocultas para que puedan obtenerlos por sí mismos, simplemente pase los parámetros. Ahorrará tiempo necesario para inventar caminos ocultos, que definitivamente no mejorarán su código.
Si sigue esta regla siempre y en todas partes, estará en camino hacia un código sin dependencias ocultas. Hacia un código que sea comprensible no solo para el autor, sino también para cualquiera que lo lea después. Donde todo es comprensible a partir de las firmas de funciones y clases y no es necesario buscar secretos ocultos en la implementación.
Esta técnica se llama técnicamente inyección de dependencias. Y esos datos se llaman dependencias. Sin embargo, es simplemente pasar parámetros, nada más.
Por favor, no confunda la inyección de dependencias, que es un patrón de diseño, con el “contenedor de inyección de dependencias”, que es una herramienta, es decir, algo diametralmente diferente. Hablaremos de los contenedores más adelante.
De funciones a clases
¿Y cómo se relacionan las clases con esto? Una clase es una unidad más compleja que una simple función, sin embargo, la regla nº 1 se aplica aquí sin excepción. Solo que hay más formas de pasar argumentos. Por ejemplo, de manera bastante similar al caso de una función:
class Matematika
{
public function soucet(float $a, float $b): float
{
return $a + $b;
}
}
$math = new Matematika;
echo $math->soucet(23, 1); // 24
O mediante otros métodos, o directamente el constructor:
class Soucet
{
public function __construct(
private float $a,
private float $b,
) {
}
public function spocti(): float
{
return $this->a + $this->b;
}
}
$soucet = new Soucet(23, 1);
echo $soucet->spocti(); // 24
Ambos ejemplos están completamente en línea con la inyección de dependencias.
Ejemplos reales
En el mundo real, no escribirá clases para sumar números. Pasemos a ejemplos prácticos.
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
{
// guardamos el artículo en la base de datos
}
}
y el 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()
guarda el artículo en una tabla de la base de datos. Implementarlo usando Nette Database sería pan comido, si no fuera por un pequeño inconveniente: ¿de
dónde obtiene Article
la conexión a la base de datos, es decir, el objeto de la clase
Nette\Database\Connection
?
Parece que tenemos muchas opciones. Puede tomarla de alguna variable estática. O heredar de una clase que proporcione la conexión a la base de datos. O usar el 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: deja que te 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, incomprensibilidad, y el resultado será una aplicación que será doloroso mantener y desarrollar.
El usuario de la clase Article
no tiene idea de dónde guarda el artículo el método save()
. ¿En
una tabla de base de datos? ¿En cuál, la de producción o la de prueba? ¿Y cómo se puede cambiar eso?
El usuario debe mirar cómo está implementado el método save()
y encuentra el uso del método
DB::insert()
. Así que debe investigar más a fondo cómo este método obtiene 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, facades de Laravel o 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, será mediante 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 un programador experimentado, quizás piense 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 del almacenamiento. Eso tiene sentido. Pero eso nos llevaría mucho más allá del alcance del tema, que es la
inyección de dependencias, y el esfuerzo por dar ejemplos simples.
Si escribe una clase que requiere, por ejemplo, una base de datos para su funcionamiento, no piense de dónde obtenerla, sino deje que se la pasen. Tal vez como parámetro del constructor u otro método. Admita las dependencias. Admítalas en la API de su clase. Obtendrá un código comprensible y predecible.
¿Y 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é piensa, hemos seguido la #Regla nº 1: deja que te lo pasen?
No lo hemos hecho.
La información clave, es decir, el directorio con el archivo de registro, la clase la obtiene por sí misma de una constante.
Mire 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ía responder a la pregunta de dónde se escriben los mensajes? ¿Se le ocurriría que
para funcionar es necesaria la existencia de la constante LOG_DIR
? ¿Y podría crear una segunda instancia que
escriba en otro lugar? Seguramente no.
Corrijamos 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 ahora es mucho más comprensible, configurable y, por lo tanto, más útil.
$logger = new Logger('/ruta/al/log.txt');
$logger->log('La temperatura es 15 °C');
¡Pero eso no me interesa!
„Cuando creo un objeto Article y llamo a save(), no quiero ocuparme de la base de datos, simplemente quiero que se guarde en la que tengo configurada.“
„Cuando uso Logger, simplemente quiero que el mensaje se escriba, y no quiero preocuparme por dónde. Que se use la configuración global.“
Estos son comentarios válidos.
Como ejemplo, mostraremos una clase que envía boletines informativos y registra cómo fue:
class NewsletterDistributor
{
public function distribute(): void
{
$logger = new Logger(/* ... */);
try {
$this->sendEmails();
$logger->log('Los correos electrónicos fueron enviados');
} catch (Exception $e) {
$logger->log('Ocurrió un error al enviar');
throw $e;
}
}
}
El Logger
mejorado, que ya no usa la constante LOG_DIR
, requiere que se especifique la ruta del
archivo en el constructor. ¿Cómo resolver esto? A la clase NewsletterDistributor
no le importa en absoluto dónde
se escriben los mensajes, solo quiere escribirlos.
La solución es nuevamente la #Regla nº 1: deja que te lo pasen: todos los datos que la clase necesita, se los pasamos.
Entonces, ¿eso significa que pasamos la ruta del registro a través del constructor, que luego usamos 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! La ruta no pertenece a los datos que la clase NewsletterDistributor
necesita; esos los necesita
Logger
. ¿Percibe la diferencia? La clase NewsletterDistributor
necesita el logger como tal. Así que
eso es lo que pasaremos:
class NewsletterDistributor
{
public function __construct(
private Logger $logger, // ✅
) {
}
public function distribute(): void
{
try {
$this->sendEmails();
$this->logger->log('Los correos electrónicos fueron enviados');
} catch (Exception $e) {
$this->logger->log('Ocurrió un error al enviar');
throw $e;
}
}
}
Ahora está claro a partir de las firmas de la clase NewsletterDistributor
que el registro es parte de su
funcionalidad. Y la tarea de cambiar el logger por otro, por ejemplo, para pruebas, es completamente trivial. Además, si el
constructor de la clase Logger
cambiara, no tendría ningún efecto en nuestra clase.
Regla nº 2: toma lo que es tuyo
No se deje engañar y no deje que le pasen las dependencias de sus dependencias. Deje que le pasen solo sus dependencias.
Gracias a esto, el código que utiliza otros objetos será completamente independiente de los cambios en sus constructores. Su API será más veraz. Y, sobre todo, será trivial reemplazar estas dependencias por otras.
Nuevo miembro de la familia
En el equipo de desarrollo, se decidió crear un segundo logger que escriba en la base de datos. Por lo tanto, crearemos la
clase DatabaseLogger
. Así que tenemos dos clases, Logger
y DatabaseLogger
, una escribe en
un archivo, la otra en la base de datos… ¿no le parece algo extraño en el nombre? ¿No sería mejor renombrar
Logger
a FileLogger
? Definitivamente sí.
Pero lo haremos inteligentemente. Crearemos 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 esto, no será necesario cambiar nada en el resto del código donde se utiliza el logger. Por ejemplo, el
constructor de la clase NewsletterDistributor
seguirá estando satisfecho con requerir Logger
como
parámetro. Y dependerá de nosotros qué instancia le pasemos.
Por eso nunca damos a los nombres de las interfaces el sufijo Interface
o el prefijo I
. De lo
contrario, no sería posible desarrollar el código de esta manera tan agradable.
Houston, tenemos un problema
Mientras que en toda la aplicación podemos arreglárnoslas con una única instancia de logger, ya sea de archivo o de base de
datos, y simplemente pasarla a donde sea que algo se registre, la situación es bastante diferente en el caso de la clase
Article
. Creamos sus instancias según sea necesario, incluso varias veces. ¿Cómo lidiar con la dependencia de la
base de datos en su constructor?
Como ejemplo puede servir un controlador que, después de enviar un formulario, debe guardar el artículo en la base de datos:
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 se presenta directamente: dejamos que el objeto de la base de datos se pase mediante el constructor a
EditController
y usamos $article = new Article($this->db)
.
Al igual que en el caso anterior con Logger
y la ruta al archivo, este no es el procedimiento 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 nº 2: toma lo que es tuyo. Cuando cambie el constructor de la clase
Article
(se agregue un nuevo parámetro), será necesario modificar también el código en todos los lugares donde se
creen instancias. Ufff.
Houston, ¿qué sugieres?
Regla nº 3: déjalo en manos de la fábrica
Al eliminar las dependencias ocultas y pasar todas las dependencias como argumentos, hemos obtenido clases más configurables y flexibles. Y, por lo tanto, necesitamos algo más que cree y configure esas clases más flexibles para nosotros. Lo llamaremos fábricas.
La regla es: si una clase tiene dependencias, deja la creación de sus instancias en manos de una fábrica.
Las fábricas son un reemplazo más inteligente del operador new
en el mundo de la inyección de dependencias.
Por favor, no confunda con el patrón de diseño factory method, que describe una forma específica de usar 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. La clase que produce Article
la llamaremos
ArticleFactory
y podría verse así, por ejemplo:
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 fábrica cree el objeto
$article = $this->articleFactory->create();
$article->title = $data->title;
$article->content = $data->content;
$article->save();
}
}
Si en este momento cambia la firma del constructor de la clase Article
, la única parte del código que debe
reaccionar es la propia fábrica ArticleFactory
. Todo el código adicional que trabaja con objetos
Article
, como EditController
, no se verá afectado de ninguna manera.
Quizás ahora se esté golpeando la frente, preguntándose si realmente hemos mejorado algo. La cantidad de código ha aumentado y todo comienza a parecer sospechosamente complicado.
No se preocupe, pronto llegaremos al contenedor DI de Nette. Y este tiene varios ases bajo la manga que simplificarán
enormemente la construcción de aplicaciones que utilizan inyección de dependencias. Por ejemplo, en lugar de la clase
ArticleFactory
, será suficiente escribir solo una
interfaz:
interface ArticleFactory
{
function create(): Article;
}
Pero nos estamos adelantando, espere un poco más :-)
Resumen
Al comienzo de este capítulo, prometimos mostrar un método para diseñar código limpio. Basta con que las clases
- reciban las dependencias que necesitan
- y, por el contrario, no reciban lo que no necesitan directamente
- y que los objetos con dependencias se fabriquen mejor en fábricas
Puede que no lo parezca a primera vista, pero estas tres reglas tienen consecuencias de gran alcance. Conducen a una visión radicalmente diferente del diseño de código. ¿Vale la pena? Los programadores que abandonaron viejos hábitos y comenzaron a usar consistentemente la inyección de dependencias consideran este paso un momento crucial en sus vidas profesionales. Se les abrió un mundo de aplicaciones claras y mantenibles.
Pero, ¿qué pasa si el código no utiliza consistentemente la inyección de dependencias? ¿Qué pasa si se basa en métodos estáticos o singletons? ¿Trae algún problema? Sí, y muy fundamentales.