¿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
- pasen las dependencias que necesitan
- a la inversa, no pasen lo que no necesitan directamente
- y que los objetos con dependencias se creen mejor en fábricas
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.