Primeiro DI: Primeiro DI em Interfaces para Aplicações Datilografadas

Eu compartilho uma das minhas bibliotecas chamada First DI. Há muitos anos que me ajuda a resolver o problema da injeção de dependência em aplicativos de navegador para bibliotecas como React, Preact, Mithril e outras. Ao escrever o First DI, a ideologia das bibliotecas de DI das linguagens C # e Java, como autofac, java spring autowired, ninject e outros, foi tomada como base. E, assim como as bibliotecas dessas linguagens, o First DI trabalha com base na reflexão e nas interfaces Typescript.

O que é DI?


Em resumo, a Injeção Dependecy (a seguir denominada DI) é uma das partes principais para a construção de uma Arquitetura Limpa. O que permite substituir qualquer parte do programa por outra implementação desta parte do programa. E o DI é uma ferramenta para implementar e substituir partes do programa.

Em particular, ele permite:

  • Substitua implementações lógicas individualmente para cada plataforma. Por exemplo, no navegador, mostre notificações por meio da API de Notificações e, ao montar aplicativos móveis, substitua por notificações pela API da plataforma móvel.
  • Substitua a implementação da lógica em vários ambientes. Por exemplo, um gateway de pagamento ou um sistema de distribuição de notificações inacessível da máquina local do desenvolvedor pode ser usado no circuito do produto. Assim, em vez de pagamentos reais ou correspondência no estágio de desenvolvimento, a lógica pode ser substituída por um esboço.
  • Substitua a fonte de dados. Por exemplo, em vez de uma solicitação real ao servidor, remova dados pré-preparados para testar a lógica de negócios ou o layout quanto à conformidade com os layouts.
  • e muitos usos mais úteis, mas não sobre isso agora ...

Para começar, prepare as ferramentas para o trabalho.

Preparação para uso


Para usar, você precisa fazer apenas 3 coisas simples:

  1. Conecte o polyfill de refletir-metadados .
  2. No arquivo tsconfig, ative as opções emitDecoratorMetadata e experimentalDecorators. O primeiro permite gerar reflexão, o segundo inclui suporte para decoradores
  3. Crie qualquer decorador vazio, por exemplo const reflection = (..._ params: Object []): void => {} ou use o pronto da biblioteca.

Agora vou demonstrar o uso em sua forma mais simples.

Uso simples


Por exemplo, considere um programa escrito usando Pure Architecture:

import { autowired, override, reflection } from "first-di";

@reflection // typescript  
class ProdRepository { //   

    public async getData(): Promise<string> {
        return await Promise.resolve("production");
    }

}

@reflection
class MockRepository { //       

    public async getData(): Promise<string> {
        return await Promise.resolve("mock");
    }

}

@reflection
class ProdService {

    constructor(private readonly prodRepository: ProdRepository) { }

    public async getData(): Promise<string> {
        return await this.prodRepository.getData();
    }

}

class ProdController { //   React, Preact, Mithril  .

    @autowired() //  
    private readonly prodService!: ProdService;

    // constructor  ,   React, Preact, Mithril
    //      

    public async getData(): Promise<string> {
        return await this.prodService.getData();
    }

}

if (process.env.NODE_ENV === "test") { //    
    override(ProdRepository, MockRepository);
}

const controllerInstance = new ProdController(); //   
const data = await controllerInstance.getData();

if (process.env.NODE_ENV === "test") {
    assert.strictEqual(data, "mock"); //  
} else {
    assert.strictEqual(data, "production"); //  
}

Na Pure Architecture, você precisa adicionar apenas 2 linhas de código, com conexão automática e substituição para que o DI comece a funcionar. E agora o programa já possui uma implementação para produção e uma implementação para teste.

Como o First DI foi gravado principalmente para aplicativos clientes, por padrão as dependências serão implementadas como Singleton. Para alterar globalmente esse comportamento, existem opções padrão, bem como parâmetros para o decorador com conexão automática e o método override ().

Você pode alterar da seguinte maneira:

//  1 
import { DI, AutowiredLifetimes } from "first-di";
DI.defaultOptions.lifeTime = AutowiredLifetimes.PER_INSTANCE;

//  2  
import { autowired, AutowiredLifetimes } from "first-di";
@autowired({lifeTime: AutowiredLifetimes.PER_INSTANCE})
private readonly prodService!: ProdService;

//  3  
import { override, AutowiredLifetimes } from "first-di";
override(ProdRepository, MockRepository, {lifeTime: AutowiredLifetimes.PER_INSTANCE});

Essa é a maneira mais fácil de usar o DI. Porém, na empresa, em vez de implementações específicas, são usadas interfaces, que são um contrato para implementações que serão implementadas. Existe um segundo modo de operação para isso.

Uso profissional


Para uso profissional, substituímos implementações específicas por interfaces. Mas o texto datilografado não gera código para executar interfaces em tempo de execução. Felizmente, existe uma solução simples, para entender é necessário recordar a teoria ... o que é uma interface? Esta é uma classe completamente abstrata!

Felizmente, o typescript suporta classes totalmente abstratas, gera código de tempo de execução e permite o uso de classes abstratas para descrever tipos.

Usaremos essas informações e escreveremos um programa corporativo:

import { autowired, override, reflection } from "first-di";

abstract class AbstractRepository { //    

    abstract getData(): Promise<string>;

}

@reflection
class ProdRepository implements AbstractRepository {

    public async getData(): Promise<string> {
        return await Promise.resolve("production");
    }

}

@reflection
class MockRepository implements AbstractRepository {

    public async getData(): Promise<string> {
        return await Promise.resolve("mock");
    }

}

abstract class AbstractService { //    

    abstract getData(): Promise<string>;

}

@reflection
class ProdService implements AbstractService {

    private readonly prodRepository: AbstractRepository;

    constructor(prodRepository: AbstractRepository) {
        this.prodRepository = prodRepository;
    }

    public async getData(): Promise<string> {
        return await this.prodRepository.getData();
    }

}

class ProdController { //   React, Preact, Mithril  .

    @autowired()
    private readonly prodService!: AbstractService;

    // constructor  ,   React, Preact, Mithril
    //      

    public async getData(): Promise<string> {
        return await this.prodService.getData();
    }

}

override(AbstractService, ProdService);

if (process.env.NODE_ENV === "test") {
    override(AbstractRepository, MockRepository);
} else {
    override(AbstractRepository, ProdRepository);
}

const controllerInstance = new ProdController();
const data = await controllerInstance.getData();

if (process.env.NODE_ENV === "test") {
    assert.strictEqual(data, "mock");
} else {
    assert.strictEqual(data, "production");
}

Agora, temos um programa Professional pronto que não possui implementação padrão e todos os contratos são substituídos por implementações já no estágio de montagem do aplicativo.

É simples assim incorporar o DI ao seu aplicativo. E aqueles que escreveram em C # e Java poderão usar a experiência existente em desenvolvimento web.

Outras características


Usando várias cópias do DI:

import { DI } from "first-di";
import { ProductionService } from "../services/ProductionService";

const scopeA = new DI();
const scopeB = new DI();

export class Controller {

    @scopeA.autowired()
    private readonly serviceScopeA!: ProductionService;

    @scopeB.autowired()
    private readonly serviceScopeB!: ProductionService;

    // constructor  ,   React, Preact, Mithril
    //      

    public async getDataScopeA(): Promise<string> {
        return await this.serviceScopeA.getData();
    }

    public async getDataScopeB(): Promise<string> {
        return await this.serviceScopeB.getData();
    }

}

Extensibilidade, você pode escrever seu próprio DI:

import { DI } from "first-di";

class MyDI extends DI {
    // extended method
    public getAllSingletons(): IterableIterator<object> {
        return this.singletonsList.values();
    }
}

E muitos outros recursos interessantes são descritos no repositório no GitHub .

Por que o seu próprio escrito e não é usado?


O motivo é simples - quando esse DI foi escrito, não havia alternativas. Era um momento em que o angular 1 não era mais relevante e o angular 2 não ia sair. As classes acabaram de aparecer em Javascript e reflexo em texto datilografado. Aliás, o surgimento da reflexão tornou-se o principal impulso para a ação.

InversifyJS - mesmo em sua forma atual não combina comigo. Muito clichê. Além disso, o registro por linha ou caractere do outro lado mata a possibilidade de refatorar dependências.

Gostou?


Se você gostou deste DI, ajude a torná-lo mais popular. Experimente, envie pedidos, coloque estrelas. Repositório do GitHub .

All Articles