Implementando un Contendor de Inversión de Control

Publicado el 2024-06-02

Escrito por C. V. Charco

Contexto

En otro post hablaba de las ventajas del patrón de inyección de dependencias, que si no lo habéis visto todavía y no tenéis claro qué es la inyección de dependencias ya estáis tardando en pinchar aquí antes de seguir leyendo este post.

No todo sale siempre a pedir de Milhouse

Ya hemos visto la gran ventaja de usar inyección de dependencias pero, cuál es el problema y por qué necesitamos lo que se conoce como un Contenedor de Inversión de Control.

Pues bien, imagina un típico escenario en el que estamos construyendo una API REST siguiendo (o intentándolo) una arquitectura limpia, con una sólida separación de responabilidades. Veamos un ejemplo muy resumido y obviando otros detalles como validaciones, sanitizaciones, tratamiento de errores, implementación de interfaces, etc.

class MyRepository implements IMyRepository {
    findAll() {...}
}

class MyService implements IMyService {
    constructor(myRepository: IMyRepository){}

    findAll() {
        return this.myRepository.findAll();
    }
}

class MyController extends Routeable {
    constructor(
        private readonly myService: MyService;
    )

    // implementar endpoints
}

class MyModule {
    register(): Routeable {
        const myRepository = new MyRepository();
        const myService = new MyService(myRepository);
        const myController = new MyController(myService);
        return myController;
    }
}

Al final esta implementación resulta algo farragosa y nos obliga a crear clases adicionales para gestionar la inyección de las dependencias. La solución es apoyarnos en un Contenedor de Inversión de Control que nos gestione la inyección de dependencias por nosotros y, con ayuda de los decoradores de Typescript, podamos obtener lo mismo con el siguiente código.

@Injectable()
class MyRepository implements IMyRepository {
    findAll(){...}
}

@Injectable()
class MyService implements IMyService {
    @Inject()
    myRepository!: MyRepository

    findAll() {
        return this.myRepository.findAll();
    }
}

class MyController {
    @Inject()
    myService!: MyService;

    // implementar endpoints
}

Implementación del Contenedor de Inversión de Control

  1. Creamos la clase que manejará las dependencias, la llamaremos IoCContainer.ts. Esta clase deberá contener un diccionario con el nombre de las clases que registra, que utilizará como clave para registrar las dependencias, y una referencia a la propia clase, que más adelante, una vez se necesite, se instanciará de forma perezosa. También necesitaremos dos métodos, uno para registrar nuevas dependencias y otra para resolverlas.
export class IoCContainer {
  private static objects = new Map<string, any>();

  // Recibe una clase y la registra
  static register() {}

  // Retorna la clase ya instanciada
  static resolve() {}
}

Resultado final

// Inject.decorator.ts

import "reflect-metadata";
import { IoCContainer } from "./Injectable.decorator";

export function Inject() {
  return (target: any, propertyKey: string) => {
    const type = Reflect.getMetadata("design:type", target, propertyKey);
    const typeName = type.name;
    const dependency = IoCContainer.resolve(typeName);

    if (dependency) {
      target[propertyKey] = dependency;
    } else {
      throw new Error(`Dependency ${type.name} not found.`);
    }
  };
}

// Injectable.decorator.ts

import { IoCContainer } from "./IoCContainer";

export function Injectable<T extends { new (...args: any[]): {} }>() {
  return (target: T) => {
    IoCContainer.register(target.name, target);
  };
}
export { IoCContainer };

// IoCContainer.ts

export class IoCContainer {
  private static objects = new Map<string, any>();

  static register<T>(name: string, obj: { new (...args: any[]): T }) {
    this.objects.set(name, obj);
  }

  static resolve<T>(name: string): T | undefined {
    const object = this.objects.get(name);
    if (object) {
      if (typeof object === "function") {
        const instance = new object();
        this.objects.set(name, instance);
        return instance;
      } else {
        return object;
      }
    } else {
      return undefined;
    }
  }
}