¿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 el „contenedor de inyección de dependencias“, que es una herramienta, algo completamente diferente. Hablaremos de 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 eres un programador experimentado, podrías estar pensando que Article
no debería tener un
método save()
en absoluto, debería ser un componente de datos puro, y un repositorio separado debería encargarse
del almacenamiento. Esto tiene sentido. Pero eso nos llevaría mucho más allá del tema, que es la inyección de dependencias, e
intentar dar 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 dejes que te pasen los parámetros de tus dependencias. Pasa las dependencias directamente.
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.
Un 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 escribe en una base de datos … ¿no crees que hay algo extraño en ese nombre? ¿No sería mejor renombrar
Logger
a FileLogger
? Claro que sí.
Pero hagámoslo de forma inteligente. Crearemos 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 de esta forma, no será necesario cambiar nada en el resto del código donde se utilice el logger. Por ejemplo, el
constructor de la clase NewsletterDistributor
seguirá contentándose con requerir Logger
como
parámetro. Y dependerá de nosotros qué instancia le pasemos.
Esta es la razón por la que nunca damos a los nombres de interfaz el sufijo Interface
o el prefijo
I
. De lo contrario, sería imposible desarrollar código de esta forma tan agradable.
Houston, tenemos un problema
Mientras que en toda la aplicación podemos estar contentos con una única instancia de un logger, ya sea de fichero o de base
de datos, y simplemente pasarlo allí donde se registre algo, es bastante diferente en el caso de la clase Article
.
De hecho, creamos instancias de ella según sea necesario, posiblemente varias veces. ¿Cómo tratar el enlace a la base de datos
en su constructor?
Como ejemplo, podemos utilizar 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();
}
}
Se ofrece directamente una posible solución: hacer que el objeto de base de datos sea pasado por el constructor a
EditController
y utilizar $article = new Article($this->db)
.
Como 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
. Así que pasar la base de datos va en contra de la regla nº 2: toma lo que es tuyo. Cuando se cambia el constructor de la clase
Article
(se añade un nuevo parámetro), también habrá que modificar el código en todos los lugares donde se crean
instancias. Ufff.
Houston, ¿qué sugieres?
Regla nº 3: Deje que se encargue la fábrica
Al eliminar los enlaces ocultos y pasar todas las dependencias como argumentos, obtenemos clases más configurables y flexibles. Y por lo tanto necesitamos algo más para crear y configurar esas clases más flexibles. 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
- las dependencias que necesitan
- y no lo que no necesitan directamente
- y que los objetos con dependencias se hacen mejor en fábricas
Puede no parecerlo a primera vista, pero estas tres reglas tienen implicaciones de gran alcance. Conducen a una visión radicalmente distinta del diseño de código. ¿Merece la pena? Los programadores que han desechado viejos hábitos y han empezado a utilizar sistemáticamente la inyección de dependencias lo consideran un momento crucial en su vida profesional. Les ha abierto un mundo de aplicaciones claras y sostenibles.
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? ¿Supone algún problema? Lo hace, y es muy significativo.