Lookup de serviços injetáveis no Angular

por Marcos Koch Salvador em 20/09/2017

No episódio 16 do Hipsters.tech ouvi a seguinte expressão “Angular é coisa de Javeiro”. De fato, qualquer desenvolvedor Java vai se sentir em casa com o Angular 2+, fazendo uso de classes, orientação a objeto e injeção de dependências.

Quem já usou o CDI do Java EE, sabe que é possível solicitar a injeção de dependências em atributos de subclasses e superclasses, construtores e até como parâmetro de métodos específicos. Inclusive é possível pedir o lookup de Java Beans em objetos não gerenciados pelo CDI. Por outro lado, o Angular só oferece a injeção no construtor dos componentes e serviços, não sendo permitido utilizar essa funcionalidade em superclasses (creio eu que seja devido a limitações do JavaScript). O código abaixo ilustra essa deficiência que eu mencionei:

export class CrudService {
    // Injeção não disponível, a subclasse é quem deve prover essa referencia
    constructor(private restangular: Restangular) {}

    public save() {
        // implementacao
    }
}

@Injectable()
export class ProdutoService extends CrudService {

    constructor(restangular: Restangular) {
        super(restangular); // Subclasse passando a referência para Superclasse
    }
}

No Angular 1.x (ou AngularJS para os íntimos), podemos fazer este lookup através da seguinte mandinga:

var injector = angular.element(document).injector(); // Forma genérica para obter o $injector
var ServicoDesejado = injector.get('ServicoDesejado');

No Angular 2+, devido ao redesign total do framework e sua API, não conseguimos acessar o Injector dessa forma. Felizmente, é possível implementarmos uma classe/serviço que faça o lookup de uma maneira bem simples.

Vamos criar um serviço injetável chamado “InjectorUtils” conforme o código abaixo. Note que nele possuímos um método não-estático para atribuirmos a referência o Injector do Angular e outro método estático que usaremos para fazer o lookup da nossa dependência.

import { Injectable, Injector, Type, InjectionToken } from '@angular/core';

@Injectable()
export class InjectorUtils {

    private static instance: Injector;

    public static lookup<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T): T {
        if (!InjectorUtils.instance) {
            throw new Error('You must set an Injector instance before try lookup a dependency!');
        }
        return InjectorUtils.instance.get(token, notFoundValue);
    }

    public setInstance(instance: Injector): void {
        if (!instance) {
            throw new Error('You cannot set a null instance of Injector!');
        }

        if (!!InjectorUtils.instance) {
            throw new Error('Another instance of Injector is already registred!');
        }

        InjectorUtils.instance = instance;
    }
}

Após isto, precisamos registrar ele no módulo raiz/principal da aplicação e também fornecer ao nosso serviço uma instancia de Injector:

import { NgModule, Injector } from '@angular/core';

import { AppComponent } from './app.component';
import { InjectorUtils } from './utils/injector-utils';

@NgModule({
    declarations: [
        AppComponent
    ],
    providers: [
        InjectorUtils
    ],
    bootstrap: [AppComponent]
})
export class AppModule {

    constructor(injectorUtils: InjectorUtils, injector: Injector) {
        injectorUtils.setInstance(injector);
    }
}

Agora a classe “CrudService” que citei no início do post, pode fazer o lookup dos serviços sem depender da subclasse. E mais: o construtor dele fica livre para receber parâmetros que customizem seu funcionamento.

import { InjectorUtils } from '../utils/injector-utils';
import { Restangular } from 'ngx-restangular';

export class CrudService {

    private _endpoint: Restangular;

    constructor(subResource: string) {
        const restangular = InjectorUtils.lookup(Restangular);
        this._endpoint = restangular.all(subResource);
    }

    // Os demais métodos foram omitidos para simplificar a classe
}
import { Injectable } from '@angular/core';
import { CrudService } from './crud.service';

@Injectable()
export class ProdutoService extends CrudService {

    constructor() {
        super('produto'); // Nome do subpath do resource a ser consumido
    }
}

Se você quiser baixar o código-fonte deste exemplo, disponibilizei um projeto baseado no Angular CLI no GitHub.