¿Qué es Dependency Injection en TypeScript y para qué sirve?
Dependency Injection (DI) es un patrón de diseño que implementa el principio de inversión de control (Inversion of Control, IoC). En lugar de que un objeto cree sus propias dependencias, estas se "inyectan" desde el exterior. En TypeScript, este patrón es especialmente útil gracias al tipado estricto y las capacidades de los decoradores.
Imagina que estás construyendo una aplicación para una tienda online. Tienes una clase OrderService que debe enviar notificaciones. Sin DI, estaría rígidamente vinculada a un método específico de envío (por ejemplo, Email). Con DI, puedes cambiar fácilmente de Email a SMS o notificaciones Push sin modificar el código de OrderService.
¿Por qué usar DI en TypeScript?
- Testeabilidad: Es fácil reemplazar servicios reales por objetos mock en pruebas unitarias.
- Flexibilidad: El sistema se vuelve modular, las dependencias se pueden cambiar sin modificar el código cliente.
- Simplificación del mantenimiento: Cada clase se encarga solo de su lógica de negocio, no de crear otros objetos.
- Seguridad de tipos: TypeScript verifica los tipos en tiempo de compilación, lo que reduce la cantidad de errores en tiempo de ejecución.
Implementación básica de DI en TypeScript sin frameworks
Comencemos con un ejemplo simple. Crearemos una interfaz para el servicio de notificaciones y dos implementaciones.
// Definimos la interfazinterface INotificationService { send(message: string): void;}
// Implementación para Emailclass EmailNotificationService implements INotificationService { send(message: string): void { console.log(`Enviando Email: ${message}`); }}
// Implementación para SMSclass SMSNotificationService implements INotificationService { send(message: string): void { console.log(`Enviando SMS: ${message}`); }}
// Clase que usa DIclass OrderService { constructor(private notificationService: INotificationService) {}
processOrder(orderId: number): void { // Lógica de procesamiento del pedido this.notificationService.send(`Pedido #${orderId} procesado`); }}
// Usoconst emailService = new EmailNotificationService();const orderService = new OrderService(emailService);orderService.processOrder(123); // Enviando Email: Pedido #123 procesadoEn este ejemplo, OrderService no sabe qué servicio de notificaciones se está utilizando. Simplemente llama al método send a través de la interfaz.
DI avanzado: Creación de un contenedor IoC simple
Para proyectos grandes, la inyección manual de dependencias se vuelve engorrosa. Aquí es donde entra en juego el contenedor IoC. Veamos cómo implementarlo usando decoradores de TypeScript.
Paso 1: Habilite el soporte para decoradores en tsconfig.json
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}Paso 2: Cree el contenedor DI
// di-container.tstype Constructor<T> = new (...args: any[]) => T;
class DIContainer { private services = new Map<string, any>();
register<T>(token: string, implementation: Constructor<T>): void { this.services.set(token, implementation); }
resolve<T>(token: string): T { const Implementation = this.services.get(token); if (!Implementation) { throw new Error(`El servicio "${token}" no está registrado`); }
// Obtenemos los tipos de los parámetros del constructor mediante reflexión const paramTypes: Constructor<any>[] = Reflect.getMetadata('design:paramtypes', Implementation) || [];
// Resolvemos las dependencias recursivamente const dependencies = param