Publicado el 2024-06-02
Escrito por C. V. Charco
Ya son bien conocidas las bondades de los principios SOLID en nuestro código para hacerlo más mantenible, testable y extendible. Uno de los patrones más habituales para conseguir cumplir parte de estos principios, dentro de la programación orientada a objetos, es la inyección de dependencias.
La inyección de dependencias es una técnica que consiste en proporcionar las instancias que necesita una clase de forma externa, en lugar de instanciarlas dentro de ella. Esto nos permite usar clases abstractas dentro de la propia clase, permitiendo una gran flexibilidad y adaptabilidad. Veamos el siguiente ejemplo donde tenemos un servicio que guarda datos en una base de datos Postgres y una clase que consume este servicio para su lógica de negocio.
// Sin inyección de dependencias
export class DBPostgres {
save() {...}
}
export class Foo {
dBPostgres!: DBPostgres;
constructor() {
this.dBPostgres = new DBPostgres();
}
bar() {
this.dBPostgres.save();
}
}
const foo = new Foo();
foo.bar();
// Con inyección de dependencias
export interface Storable {
save: () => void;
}
export class DBPostgres implements Storable {
save() {...}
}
export class Foo {
constructor(
private readonly storable: Storable
) {}
bar() {
this.storable.save();
}
}
const dbPostgres = new DBPostgres();
const foo = new Foo(dBPostgres);
foo.bar();
Si probamos ambos códigos veremos que hacen exactamente lo mismo. Tal vez nos preguntemos entonces por qué agregar complejidad a nuestro diseño si vamos a obtener lo mismo en retorno. La respuesta aquí no es qué hace el código, sino cómo lo hace para que pueda ser mantenible, testable y extendible.
Imagina que ahora queremos crear otra clase que en lugar de almacenar datos en una base de datos Postgres lo hace en una base de datos MySQL. Veamos los cambios:
// Sin inyección de dependencias
class DBPostgres {
save() {...}
}
class DBMySQL {
save() {...}
}
export class Foo {
dbPostgres!: DBPostgres;
dbMySQL!: DBMySQL;
static DB_TYPE = Object.freeze({
POSTGRES: Symbol(),
MYSQL: Symbol(),
});
constructor(
private readonly dbType: symbol
) {
this.dbPostgres = new DBPostgres();
this.dbMySQL = new DBMySQL();
}
bar() {
if(this.dbType === Foo.DB_TYPE.POSTGRES)
this.dbPostgres.save();
if(this.dbType === Foo.DB_TYPE.MYSQL)
this.dbMySQL.save();
}
}
const foo = new Foo(Foo.DB_TYPE.MYSQL);
foo.bar();
// Con inyección de dependencias
interface Storable {
save: () => void;
}
class DBPostgres implements Storable {
save() {...}
}
class DBMySQL implements Storable {
save() {...}
}
class Foo {
constructor(
private readonly storable: Storable
) {}
bar() {
this.storable.save();
}
}
const dbMySQL = new DBMySQL();
const foo = new Foo(dbMySQL);
foo.bar();
La comparación resulta bastante obvia. Sin inyección de dependencias la extensión de las clases resulta mucho más complicada y engorrosa, no quiero ni imaginar si tenemos que agregar también en el futuro otras bases de datos, como MongoDB, o almacenamiento local en SQLite. Además, si has observado, al trabajar con inyección de dependencias hemos conseguido aumentar la funcionalidad de la clase Foo (ahora también puede guardar datos en bases de datos MySQL) y no hemos tenido que modificar ni una sola línea de código y esto se alinea perfectamente con el principio abierto-cerrado de SOLID.