Ensine como construir sua própria ferramenta de injeção de dependência

Preparação antes de ler

Antes de ler este documento, você pode primeiro compreender esses conhecimentos, para poder acompanhar o conteúdo do documento:

  • Conhecimentos conceituais: Inversão de Controle, Injeção de Dependência, Inversão de Dependência;

  • Conhecimento técnico: decorador Decorador, reflexão Reflect;

  • Definição de TSyringe: Token, Provedor https://github.com/microsoft/tsyringe#injection-token .

Todas as práticas de implementação neste artigo estão escritas em codesandbox. Se você estiver interessado, pode clicar para ver o código-fonte https://codesandbox.io/s/di-playground-oz2j9.

O que é injeção de dependência

exemplo simples

Aqui implementamos um exemplo simples para explicar o que é injeção de dependência: um aluno dirige um veículo de casa para a escola.

class Transportation {
  drive() {
    console.log('driving by transportation')
  }
}

class Student {
  transportation = new Transportation()
  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

Então, na vida real, os alunos que estão longe escolherão ir de carro para a escola, e os alunos que estão perto escolherão ir para a escola de bicicleta. Então podemos continuar a abstrair o código acima e escrevê-lo da seguinte forma:

class Car extends Transportation {
  drive() {
    console.log('driving by car')
  }
}

class FarStudent extends Student {
  transportation = new Car()
}

Isso atende às necessidades dos alunos que estão longe de dirigir para a escola, mas há um problema aqui: os alunos também têm suas próprias escolhas e preferências específicas. Algumas pessoas gostam da BMW e outras da Tesla. Para resolver esses problemas, podemos continuar a usar herança e abstrair, de modo a obter estudantes ricos que gostam de BMW e Tesla.

Todos provavelmente pensam que escrever código dessa forma é completamente inviável e que o grau de acoplamento é muito alto. Cada tipo de aluno está diretamente vinculado a um meio de transporte específico quando abstraído. O meio de transporte de propriedade de um aluno não é criado pelo aluno, mas determinado de acordo com sua situação familiar e preferências, que tipo de transporte ele utiliza para ir à escola; pode até haver muitos carros em casa, e ele dirige para a escola todos os dias dependendo de seu humor.

Então, para reduzir o acoplamento e criar dependências de acordo com estados e condições específicos, precisamos falar sobre os seguintes padrões.

inversão de controle

Inversão de Controle (Inversão de Controle, abreviado como IoC ) é um princípio de design que reduz o acoplamento entre códigos invertendo a lógica do programa.

Uma inversão de contêiner de controle (contêiner IoC) é uma ferramenta ou estrutura específica usada para executar a lógica de código invertida de um programa interno, melhorando assim a reutilização e legibilidade do código. A ferramenta DI que usamos frequentemente desempenha o papel de contêiner IoC, conectando todos os objetos e suas dependências.

e29967fd9ce06208ccbaabb3f939ff71.png

Consulte o artigo de Martin Fowler sobre Inversão de Controle e Injeção de Dependência https://martinfowler.com/articles/injection.html

Injeção de dependência

A injeção de dependência é uma implementação específica de inversão de controle. Ao abrir mão do controle da criação da vida do objeto dentro do programa, os objetos dependentes são criados e injetados de fora.

Existem quatro métodos principais de injeção de dependência:

  • Baseado em interface. Implemente uma interface específica para contêineres externos para injetar objetos do tipo dependente.

  • Com base no método definido. Implemente o método public set de uma propriedade específica para permitir que o contêiner externo chame o objeto do tipo dependente.

  • Baseado no construtor. Implemente um construtor com parâmetros específicos e passe um objeto do tipo dependente ao criar um novo objeto.

  • Com base nas anotações, adicione anotações como "@Inject" antes das variáveis ​​privadas, para que as ferramentas ou estruturas possam analisar dependências e injetar dependências automaticamente.

Os dois primeiros métodos não serão usados ​​nas ferramentas de DI comumente usadas no front-end. Aqui apresentamos principalmente os dois últimos.

Se passado do construtor, pode ser escrito assim:

class Student {
  constructor(transportation: Transportation) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

class Car extends Transportation {
  drive() {
    console.log('driving by car')
  }
}

const car = new Car()
const student = new Student(car)
student.gotoSchool()

Na ausência de ferramentas, o método de definição baseado no construtor pode ser escrito à mão, mas embora o método de escrita aqui pertença à injeção de dependência, muitas instanciações manuais complicadas serão um pesadelo para os desenvolvedores; especialmente Car O próprio objeto pode depender de pneus diferentes, diferentes instâncias de motores.

Ferramentas para injeção de dependência

A ferramenta de injeção de dependência é uma espécie de contêiner IoC. Ao analisar automaticamente as dependências, o processo de instanciação do objeto que originalmente era realizado manualmente é concluído na ferramenta.

@Injectable()
class Student {
  constructor(transportation: Transportation) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const injector = new Injector()
const student = injector.create(Student)
student.gotoSchool()

Se você usar anotações, poderá escrever assim:

@Injectable()
class Student {
  @Inject()
  private transportation: Transportation

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const injector = new Injector()
const student = injector.create(Student)
student.gotoSchool()

A diferença entre os dois está na dependência da ferramenta. A classe definida a partir do construtor ainda pode rodar normalmente mesmo que seja criada manualmente, mas a classe definida na forma de anotação só pode ser criada pela ferramenta, e não pode ser criado manualmente.

Inversão de Dependência

Um dos seis princípios dos padrões de design de software, Princípio de Inversão de Dependência, abreviatura em inglês DIP , nome completo Princípio de Inversão de Dependência.

Módulos de alto nível não devem depender de módulos de baixo nível, ambos devem depender de suas abstrações; abstrações não devem depender de detalhes, detalhes devem depender de abstrações.

No cenário com um contêiner loC, o controle da criação de objetos não está em nossas mãos, mas criado dentro da ferramenta ou estrutura. Se um aluno dirige um BMW ou um Tesla quando está na escola é determinado pelo ambiente operacional. No ambiente operacional real do JS, o modelo do pato é seguido, seja um veículo ou não, desde que possa ser dirigido, qualquer carro pode ser utilizado.

Portanto, podemos alterar o código para o seguinte, contando com uma abstração em vez de uma implementação específica.

// src/transportations/car.ts
class Car {
   drive() {
     console.log('driving by car')
   }
}

// src/students/student.ts
interface Drivable {
   drive(): void
}

class Student {
  constructor(transportation: Drivable) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

Por que é tão importante confiar em abstrações em vez de implementações? Em uma arquitetura complexa, a abstração razoável pode nos ajudar a manter a simplicidade, melhorar a coesão dentro dos limites do domínio, reduzir o acoplamento entre diferentes limites e orientar os projetos para que sejam divididos em estruturas de diretórios razoáveis. Ao implementar a capacidade composta de SSR e CSR, quando o cliente está em execução, ele precisa solicitar dados por meio de HTTP, enquanto no servidor, precisamos apenas chamar diretamente o DB ou RPC para obter os dados.

Podemos abstrair o objeto que solicita dados, definir um serviço abstrato e implementar a mesma função no cliente e no servidor respectivamente para solicitar dados:

interface IUserService {
  getUserInfo(): Promise<{ name: string }>
}

class Page {
  constructor(userService: IUserService) {
    this. userService = userService
  }

  async render() {
    const user = await this.userService. getUserInfo()
    return `<h1> My name is ${user.name}. </h1>`
  }
}

Use isso em uma página da web:

class WebUserService implements IUserService {
  async getUserInfo() {
    return fetch('/api/users/me')
  }
}

const userService = new WebUserService()
const page = new Page(userService)

page.render().then((html) => {
  document.body.innerHTML = html
})

Use isso no lado do servidor:

class ServerUserService implements IUserService {
  async getUserInfo() {
    return db.query('...')
  }
}

class HomeController {
  async renderHome() {
    const userService = new ServerUserService()
    const page = new Page(userService)
    ctx.body = await page.render()
  }
}

Testabilidade

Além de alcançar a alta coesão e o baixo acoplamento mais importantes na engenharia de software, a injeção de dependência também pode melhorar a testabilidade do código.

Um teste geral que poderíamos escrever assim:

class Car extends Transportation {
  drive() {
    console.log('driving by car')
  }
}

class Student {
  this.transportation = new Car()

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

it('goto school successfully', async () => {
  const driveStub = sinon.sub(Car.prototype, 'drive').resolve()
  const student = new Student()
  student. gotoSchool()
  expect(driveStub.callCount).toBe(1)
})

Embora tal teste de unidade possa ser executado normalmente, porque a função do Stub é invadida no protótipo, este é um efeito colateral global, que afetará outros testes de unidade se forem executados aqui. Se os efeitos colaterais do sinon forem eliminados no final do teste, isso não afetará o teste da unidade serial, mas o teste paralelo não será possível. Com o método de injeção de dependência, não haverá tais problemas.

class Student {
  constructor(transportation: Transportation) {
    this.transportation = transportation
  }

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

it('goto school successfully', async () => {
  const driveFn = sinon.fake()
  const student = new Student({
    { drive: driveFn },
  })
  student.gotoSchool()
  expect(driveFn.callCount).toBe(1)
})

dependência circular

No caso de dependências circulares, geralmente não podemos criar objetos, como as duas definições de classe a seguir. Embora seja necessário evitar tal situação logicamente, é difícil dizer que tal situação não será totalmente utilizada no processo de escrita do código.

export class Foo {
  constructor(public bar: Bar) {}
}

export class Bar {
  constructor(public foo: Foo) {}
}

Com DI, por meio da inversão do controle de criação de IoC, quando o objeto é criado pela primeira vez, a instância não será realmente criada, mas um objeto proxy será fornecido, e a instância será criada quando o objeto for realmente usado, e então a dependência circular será resolvida. Quanto ao motivo pelo qual o decorador Lazy deve existir aqui, explicaremos mais tarde, quando o implementarmos posteriormente.

@Injectable()
export class Foo {
  constructor(public @Lazy(() => Bar) bar: Bar) {}
}

@Injectable()
export class Bar {
  constructor(public @Lazy(() => Foo) foo: Foo) {}
}

algumas desvantagens

É claro que o uso de ferramentas de DI não é totalmente isento de desvantagens. As desvantagens mais óbvias incluem:

  • Ciclo de vida incontrolável, pois a instanciação do objeto é em IoC, portanto, quando o objeto é criado não é totalmente determinado pelo programa atual. Portanto, isso exige que tenhamos um bom entendimento dos princípios antes de usar ferramentas ou estruturas, e é melhor ler o código-fonte interno.

  • Quando as dependências dão errado, é mais difícil localizar o que está errado. Como as dependências são injetadas, quando as dependências dão errado, você só pode analisá-las por meio da experiência ou da depuração ao vivo no local e fazer uma depuração aprofundada um pouco para saber onde o conteúdo está errado. Isso requer muita capacidade de depuração ou capacidade de controle geral do projeto.

  • O código não pode ser lido de forma coerente. Se depender da implementação, você pode ver toda a árvore de execução do código desde a entrada até o fim; se depender da abstração, o relacionamento de conexão entre a implementação específica e a implementação é separado, e geralmente são necessários documentos para ver o panorama geral do projeto.

ferramentas comunitárias

Na categoria DI do github, você pode visualizar algumas ferramentas populares de DI https://github.com/topics/dependency-injection?l=typescript.

InversifyJS (https://github.com/inversify/InversifyJS): Uma poderosa ferramenta de injeção de dependência com uma implementação estrita de abstração de dependência; embora a declaração estrita seja boa, é muito repetitiva e detalhada de escrever.

TSyringe (https://github.com/microsoft/tsyringe): Fácil de usar, herdado da definição abstrata de angular, vale a pena aprender.

alcançar competências básicas

Para realizar os recursos das ferramentas básicas de DI, para assumir a criação de objetos, realizar a inversão e a injeção de dependência, realizamos principalmente três recursos:

  • Análise de dependências: Para poder criar um objeto, a ferramenta precisa saber quais dependências existem.

  • Criador de registro: Para suportar diferentes tipos de métodos de criação de instâncias, ele suporta dependências diretas, dependências abstratas e criação de fábrica; contextos diferentes registram implementações diferentes.

  • Crie uma instância: use o criador para criar uma instância, com suporte ao modo singleton e ao modo multi-instância.

Supondo que nossa forma final seja o código de execução como este, se quiser o resultado final, você pode clicar no link de codificação online https://codesandbox.io/s/di-playground-oz2j9 .

@Injectable()
class Transportation {
  drive() {
    console.log('driving by transportation')
  }
}

@Injectable()
class Student {
  constructor(
    private transportation: Transportation,
  ) {}

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const container = new Container()
const student = container.resolve(Student)
student.gotoSchool()

Análise de Dependência

Para permitir que as ferramentas de DI realizem análise de dependência, é necessário habilitar a função de decorador do TS e a função de metadados do decorador.

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Decorador

Então, primeiro vamos dar uma olhada em como a dependência do construtor é analisada. Depois de habilitar as funções de decorador e metadados, tente compilar o código anterior no playground do TS e você verá que o código JS em execução se parece com isto.

bc8f0160bd6789904a4479ba8260040d.png

Pode-se notar que as definições de código mais críticas são as seguintes:

Student = __decorate([
  Injectable(),
  __metadata("design:paramtypes", [Transportation])
], Student);

Se você ler a lógica da função __decorate com atenção, na verdade é uma função de ordem superior. Para executar o ClassDecorator e o Metadata Decorator na ordem inversa, traduza o código acima, que é equivalente a:

Student = __metadata("design:paramtypes", [Transportation])(Student)
Student = Injectable()(Student)

Em seguida, lemos atentamente a lógica da função __metadata, que executa a função Reflect, que equivale ao código:

Student = Reflect.metadata("design:paramtypes", [Transportation])(Student)
Student = Injectable()(Student)

reflexão

Para os resultados do código anterior, podemos ignorar temporariamente a primeira linha e ler o significado da segunda linha. Esta é exatamente a capacidade de análise de dependência que precisamos. Reflect.metadata é uma função de alto nível que retorna uma função decoradora. Após a execução, os dados são definidos no construtor. Você pode encontrar os dados definidos deste construtor ou de seus sucessores por meio de getMetadata.

eeea816ea2b2e41be0eaef3daef553f0.png

Por exemplo, na reflexão acima, podemos obter os dados definidos da seguinte forma:

const args = Reflect.getMetadata("design:paramtypes", Student)
expect(args).toEqual([Transportation])

Proposta de metadados reflexivos: https://rbuckton.github.io/reflect-metadata/#syntax

Após habilitar o emitDecoratorMetadata, o TS preencherá automaticamente três tipos de metadados ao compilar o local decorado:

  • design:type Os metadados de tipo da propriedade atual, que aparecem em PropertyDecorator e MethodDecorator;

  • Os metadados dos parâmetros de entrada design:paramtypes aparecem em ClassDecorator e MethodDecorator;

  • design:returntype retorna metadados de tipo, que aparecem em MethodDecorator.

f8f7687eb577ee4fcd53d0adb001e7db.png

dependências de tags

Para que as ferramentas de DI coletem e armazenem dependências, precisamos analisar os construtores dependentes no Injetável e, em seguida, definir os construtores por meio de reflexão e registrar a descrição dos dados em reflexão por meio de um valor de Símbolo.

const DESIGN_TYPE_NAME = {
  DesignType: "design:type",
  ParamType: "design:paramtypes",
  ReturnType: "design:returntype"
};

const DECORATOR_KEY = {
  Injectable: Symbol.for("Injectable"),
};

export function Injectable<T>() {
  return (target: new (...args: any[]) => T) => {
    const deps = Reflect.getMetadata(DESIGN_TYPE_NAME.ParamType, target) || [];
    const injectableOpts = { deps };
    Reflect.defineMetadata(DECORATOR_KEY.Injectable, injectableOpts, target);
  };
}

Isso serve a dois propósitos:

  • Os dados de configuração são marcados pelo Símbolo interno, e o construtor superficial é decorado com Injetável e pode ser criado por IoC.

  • Colete e monte dados de configuração, definidos no construtor, incluindo dados dependentes, dados de configuração como instância única e múltiplas instâncias que podem ser usadas posteriormente.

definir contêiner

Com os dados definidos pelo decorador em reflexão, a parte mais importante do Container no IoC pode ser criada. Implementaremos uma função de resolução para criar automaticamente a instância:

const DECORATOR_KEY = {
  Injectable: Symbol.for("Injectable"),
};

const ERROR_MSG = {
  NO_INJECTABLE: "Constructor should be wrapped with decorator Injectable.",
}

export class ContainerV1 {
  resolve<T>(target: ConstructorOf<T>): T {
    const injectableOpts = this.parseInjectableOpts(target);
    const args = injectableOpts.deps.map((dep) => this.resolve(dep));
    return new target(...args);
  }

  private parseInjectableOpts(target: ConstructorOf<any>): InjectableOpts {
    const ret = Reflect.getOwnMetadata(DECORATOR_KEY.Injectable, target);
    if (!ret) {
      throw new Error(ERROR_MSG.NO_INJECTABLE);
    }
    return ret;
  }
}

A lógica principal é a seguinte:

  • Analise os dados definidos pelo decorador Injetável através da reflexão do construtor. Se não houver dados, será gerado um erro; deve-se prestar um pouco de atenção ao fato de que, como os dados de reflexão são herdados, apenas getOwnMetadata pode ser usado para obter os dados de reflexão do alvo actual.Certifique-se de que o alvo actual deve ser decorado.

  • Em seguida, crie recursivamente instâncias dependentes por meio de dependências e obtenha a lista de parâmetros de entrada do construtor.

  • Finalmente, ao instanciar o construtor, obtemos o resultado que desejamos.

criar instância

Neste ponto, a função mais básica de criação de objetos foi realizada e o código a seguir pode finalmente ser executado normalmente.

@Injectable()
class Transportation {
  drive() {
    console.log('driving by transportation')
  }
}

@Injectable()
class Student {
  constructor(
    private transportation: Transportation,
  ) {}

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const container = new Container()
const student = container.resolve(Student)
student.gotoSchool()

Você também pode visitar a caixa de códigos e selecionar o modo ContainerV1 à esquerda para ver esse resultado.

1012a51979f1f826653d53f03fbeeb89.png

Abstração de dependência

Então concluímos o IoC básico, mas precisamos alterar os requisitos, esperando substituir o veículo por qualquer ferramenta que desejarmos em tempo de execução, e a dependência do Aluno ainda deve ser um veículo que possa ser dirigido.

A seguir, implementamos em duas etapas:

  • Substituição de instância: Substitua Transporte por Bicicleta em tempo de execução.

  • Abstração de dependência: mudança de transporte de classe para interface.

Antes de perceber a capacidade de substituição de instância e abstração de dependência, devemos primeiro definir a relação entre dependência e implementação de dependência, para que IoC possa saber qual instância criar para injetar dependências, portanto, devemos primeiro falar sobre Token e Provedor.

Símbolo

Como um token exclusivo de dependência, pode ser String, Símbolo, Construtor ou TokenFactory. Na ausência de abstração dependente, na verdade é uma dependência direta entre diferentes Construtores; String e Símbolo são os IDs de dependência que usaremos após confiar na abstração; e TokenFactory é usado quando realmente queremos realizar referências circulares de arquivos. Resolver dependente esquemas.

Podemos ignorar o TokenFactory, a outra parte da definição Token não precisa ser implementada separadamente, é apenas uma definição de tipo:

export type Token<T = any> = string | symbol | ConstructorOf<T>;

Fornecedor

A definição de criação da instância correspondente ao Token registrado no contêiner, e então o IoC pode criar o objeto de instância correto através do Provedor após obter o Token. Subdividindo ainda mais, os Provedores podem ser divididos em três tipos:

  • Provedor de classe

  • Provedor de valor

  • Fornecedor de fábrica

Provedor de classe

Use construtores para definir a instanciação. Geralmente, a versão simples que implementamos anteriormente é na verdade uma versão simplificada deste modelo; com uma pequena modificação, é fácil implementar esta versão e, após implementar ClassProvider, podemos passar A forma de registrar o Provedor é para substituir o meio de transporte do exemplo anterior.

interface ClassProvider<T = any> {
  token: Token<T>
  useClass: ConstructorOf<T>
}

Provedor de valor

ValueProvider é muito útil quando já existe uma implementação única globalmente, mas as dependências abstratas são definidas internamente. Para dar um exemplo simples, no modo de arquitetura concisa, exigimos que a lógica do código principal seja independente do contexto, portanto, se o front-end quiser usar objetos globais no ambiente do navegador, ele precisará definir abstratamente e depois colocar esses objetos são passados ​​​​por meio do ValueProvider.

interface ClassProvider<T = any> {
  token: Token<T>
  useValue: T
}

Fornecedor de fábrica

Este Provedor terá uma função de fábrica, e depois criará uma instância, o que é muito útil quando precisarmos utilizar o padrão de fábrica.

interface FactoryProvider<T = any> {
  token: Token<T>;
  useFactory(c: ContainerInterface): T;
}

Implementar registro e criação

Após definir Token e Provedor, podemos implementar uma função de registro através deles e conectar Provedor com criação. A lógica também é relativamente simples, com dois pontos principais:

  • Utilize o Mapa para formar o relacionamento de mapeamento entre Token e Provedor, e ao mesmo tempo desduplicar a implementação do Provedor, e os cadastrados cobrirão os anteriores. TSyringe pode ser registrado várias vezes. Se o construtor depender de uma matriz de exemplo, uma instância será criada para cada Provedor por vez; esta situação é raramente usada e aumentará a complexidade da implementação do Provedor. Muito alto, estudantes interessados ​​podem estudar esta parte de sua implementação e definição.

  • Ao analisar diferentes tipos de provedores, crie diferentes dependências.

export class ContainerV2 implements ContainerInterface {
  private providerMap = new Map<Token, Provider>();

  resolve<T>(token: Token<T>): T {
    const provider = this.providerMap.get(token);
    if (provider) {
      if (ProviderAssertion.isClassProvider(provider)) {
        return this.resolveClassProvider(provider);
      } else if (ProviderAssertion.isValueProvider(provider)) {
        return this.resolveValueProvider(provider);
      } else {
        return this.resolveFactoryProvider(provider);
      }
    }

    return this.resolveClassProvider({
      token,
      useClass: token
    });
  }

  register(...providers: Provider[]) {
    providers.forEach((p) => {
      this.providerMap.set(p.token, p);
    });
  }
 }

substituição de instância

Depois de implementar a função que suporta o registo de Provedor, podemos substituir o meio de transporte que os alunos utilizam quando vão para a escola, definindo o Provedor de Transporte.

const container = new ContainerV2();
container.register({
  token: Transportation,
  useClass: Bicycle
});

const student = container.resolve(Student);
return student.gotoSchool();

Assim, podemos ver o seguinte efeito no codeandbox e, finalmente, podemos ir para a escola de bicicleta.

976e7af51b5ebb713a6dd442c9d67910.png

padrão de fábrica

Realizamos a substituição das dependências. Antes de implementar a abstração das dependências, primeiro inserimos um novo requisito, pois é muito difícil ir para a escola de bicicleta, então as condições das estradas são melhores nos finais de semana, e esperamos poder dirigir para a escola. Através do padrão de fábrica, podemos implementá-lo da seguinte forma:

const container = new ContainerV2();
container.register({
  token: Transportation,
  useFactory: (c) => {
    if (weekday > 5) {
      return c.resolve(Car);
    } else {
      return c.resolve(Bicycle);
    }
  }
});

const student = container.resolve(Student);
return student.gotoSchool();

Aqui está uma introdução simples ao modo de fábrica. Tanto o TSyringe quanto o InversifyJS possuem funções de criação de modo de fábrica, o que é uma forma recomendada; ao mesmo tempo, você também pode projetar outras ferramentas DI. Algumas ferramentas colocarão o julgamento da função de fábrica em o Onde a classe é declarada.

Isso não é impossível, será mais fácil escrever quando uma única função for implementada individualmente, mas aqui falaremos sobre o propósito de introduzir DI, para desacoplamento. O julgamento lógico da função de fábrica é, na verdade, parte da lógica de negócios e não pertence ao campo da implementação específica; e quando a implementação é usada em múltiplas lógicas de fábrica, a lógica neste local se tornará muito estranha.

definir abstração

Então, após a substituição da instância, vamos ver como tornar o Transporte uma abstração em vez de um objeto de implementação concreto. Portanto, o primeiro passo é mudar a dependência do Student da lógica de implementação concreta para a lógica abstrata.

O que precisamos é de uma abstração de transporte, um veículo que possa ser dirigido, bicicletas, motocicletas e carros; desde que possa ser dirigido, qualquer carro pode ser usado. Em seguida, crie uma nova classe de aluno para herdar o objeto antigo para distinção e comparação.

interface ITransportation {
  drive(): string
}

@Injectable({ muiltple: true })
export class StudentWithAbstraction extends Student {
  constructor(protected transportation: ITransportation) {
    super(transportation);
  }
}

Se você escrever assim, descobrirá que a análise de dependência estará errada; porque quando o TS é compilado, a interface é um tipo e se tornará o objeto de construção correspondente do tipo em tempo de execução, e a dependência não pode ser resolvida corretamente .

5039719890e82237db246a09ae977ed4.png

Portanto, além de definir um tipo abstrato aqui, também precisamos definir uma tag exclusiva para esse tipo abstrato, que é a string ou símbolo no Token. Geralmente escolhemos o símbolo, que é um valor globalmente único. Aqui você pode usar as múltiplas definições de TS com o mesmo nome em valor e tipo, e deixar o TS analisá-lo por si só como um valor e como um tipo.

const ITransportation = Symbol.for('ITransportation')
interface ITransportation {
  drive(): string
}

@Injectable({ muiltple: true })
export class StudentWithAbstraction extends Student {
  constructor(
    protected @Inject(ITransportation) transportation: ITransportation,
  ) {
    super(transportation);
  }
}

substituir dependências abstratas

Observe que, além de definir o valor do token da dependência abstrata, também precisamos adicionar um decorador adicional para tornar a dependência do parâmetro do construtor do marcador e atribuir a ele uma marca Token.

function Inject(token: Token) {
  return (target: ConstructorOf<any>, key: string | symbol, index: number) => {
    if (!Reflect.hasOwnMetadata(DECORATOR_KEY.Inject, target)) {
      const tokenMap = new Map([[key, token]]);
      Reflect.defineMetadata(DECORATOR_KEY.Inject, tokenMap, target);
    } else {
      const tokenMap: Map<number, Token> = Reflect.getOwnMetadata(
        DECORATOR_KEY.Inject,
        target
      );
      tokenMap.set(index, token);
    }
  };
}

Ao mesmo tempo, a lógica do Injetável também precisa ser alterada para substituir as dependências nas posições correspondentes.

export function Injectable<T>(opts: InjectableDecoratorOpts = {}) {
  return (target: new (...args: any[]) => T) => {
    const deps = Reflect.getMetadata(DESIGN_TYPE_NAME.ParamType, target) || [];
    const tokenMap: Map<number, Token> = Reflect.getOwnMetadata(
      DECORATOR_KEY.Inject,
      target
    );
    if (tokenMap) {
      for (const [index, token] of tokenMap.entries()) {
        deps[index] = token;
      }
    }

    const injectableOpts = {
      ...opts,
      deps
    };
    Reflect.defineMetadata(DECORATOR_KEY.Injectable, injectableOpts, target);
  };
}

Registrar provedor abstrato

Ainda há o último passo aqui, injetar o Provider correspondente do Token pode ser utilizado, só precisamos alterar a definição do Token do FactoryProvider anterior, e então atingimos nosso objetivo.

const ITransportation = Symbol.for('ITransportation')
interface ITransportation {
  drive(): string
}

const container = new ContainerV2();
container.register({
  token: ITransportation,
  useFactory: (c) => {
    if (weekday > 5) {
      return c.resolve(Car);
    } else {
      return c.resolve(Bicycle);
    }
  }
});
const student = container.resolve(StudentWithAbstraction);
return student.gotoSchool();

Implementar criação preguiçosa

Anteriormente, implementamos o método de injeção de dependência baseado em construtor, que é muito bom e não afeta o uso normal do construtor. Mas um problema com isso é que todas as instâncias de objetos na árvore de dependências serão criadas quando o objeto raiz for criado. Haverá algum desperdício desta forma e as instâncias que não forem utilizadas poderão não ser criadas originalmente.

Para garantir que as instâncias criadas sejam utilizadas, optamos por criar instâncias ao utilizá-las em vez de inicializar o objeto raiz.

Definir uso

Aqui precisamos alterar a função Inject para que ela possa suportar tanto a decoração do construtor quanto a decoração do Property.

const ITransportation = Symbol.for('ITransportation')
interface ITransportation {
  drive(): string
}

@Injectable()
class Student {
  @Inject(ITransportation)
  private transportation: ITransportation

  gotoSchool() {
    console.log('go to school by')
    this.transportation.drive()
  }
}

const container = new Container()
const student = container.resolve(Student)
student.gotoSchool()

decorador de imóveis

Vamos dar uma olhada nas características de ParameterDecorator e PropertyDecorator combinando os resultados da compilação TS e definições de tipo.

Abaixo está a descrição em .d.ts

acc6faa471f279101c66b456b5469b15.png

O seguinte é o resultado da compilação

1c8bb607855bc7c2732659ef69631080.png

Você pode ver as seguintes diferenças:

  • A quantidade de parâmetros de entrada é diferente, pois ParameterDecorator terá os dados da quantidade de parâmetros.

  • A descrição do objeto é diferente. O ParameterDecorator do construtor descreve o construtor e o PropertyDecorator descreve o Prototype do construtor.

Assim, ao identificar a marca e retornar o arquivo de descrição da propriedade, a função getter da propriedade correspondente é adicionada ao Protótipo e a lógica de criação do objeto durante o uso é realizada.

function decorateProperty(_1: object, _2: string | symbol, token: Token) {
  const valueKey = Symbol.for("PropertyValue");
  const ret: PropertyDescriptor = {
    get(this: any) {
      if (!this.hasOwnProperty(valueKey)) {
        const container: IContainer = this[REFLECT_KEY.Container];
        const instance = container.resolve(token);
        this[valueKey] = instance;
      }

      return this[valueKey];
    }
  };
  return ret;
}

export function Inject(token: Token): any {
  return (
    target: ConstructorOf<any> | object,
    key: string | symbol,
    index?: number
  ) => {
    if (typeof index !== "number" || typeof target === "object") {
      return decorateProperty(target, key, token);
    } else {
      return decorateConstructorParameter(target, index, token);
    }
  };
}

Uma coisa a se notar aqui é que no design da própria descrição do TS, não é recomendado retornar ao PropertyDescriptor para alterar a definição das propriedades, mas na verdade, na implementação de padrões e TS, ele realmente fez isso, então aqui pode estar no futuro mudará.

dependência circular

Depois de terminar a criação preguiçosa, vamos falar sobre um problema com um pouco de relacionamento, a dependência circular. Em geral, devemos evitar dependências circulares da nossa lógica, mas se tivermos que usá-las, ainda precisaremos fornecer soluções para resolver dependências circulares.

Por exemplo, um exemplo:

@Injectable()
class Son {
  @Inject()
  father: Father

  name = 'Thrall'

  getDescription() {
    return `I am ${this.name}, son of ${this.father.name}.`
  }
}

@Injectable()
class Father {
 @Inject()
  son: Son

  name = 'Durotan'

  getDescription() {
    return `I am ${this.name}, my son is ${this.son.name}.`
  }
}

const container = new Container()
const father = container.resolve(Father)
console.log(father. getDescription())

por que há um problema

O problema é por causa do tempo de execução do decorador. O objetivo do decorador do construtor é descrever o construtor, ou seja, quando o construtor for declarado, a lógica do decorador será executada imediatamente, mas neste momento suas dependências não foram declaradas e o valor obtido ainda está indefinido.

a19e858fbad32dddffdf8dd630193292.png

loop de arquivo

Além do loop dentro dos arquivos, também existem loops entre os arquivos, como no exemplo a seguir.

ae37a45d872fd31b857290498a5afb9f.png

Acontecerá o seguinte:

  • O arquivo pai é lido pelo Node;

  • O arquivo Pai irá inicializar um módulo no Node e registrá-lo no total de módulos, mas exports ainda é um objeto vazio, aguardando atribuição;

  • O arquivo Pai começa a executar a primeira linha, referenciando o resultado do Filho;

  • Comece a ler o arquivo Son;

  • O arquivo Son irá inicializar um módulo no Node e registrá-lo no total de módulos, mas exports ainda é um objeto vazio, aguardando para ser atribuído;

  • O arquivo Son executa a primeira linha, cita o resultado do Pai e depois lê o módulo vazio registrado pelo Pai;

  • Son começa a declarar o construtor, depois lê o construtor do Pai, mas neste momento ele está indefinido, e executa a lógica do decorador;

  • Módulo do Filho atribui exportações e finaliza a execução;

  • Depois que o Pai lê o construtor do Filho, ele começa a declarar o construtor; lê o construtor do Filho corretamente e executa a lógica do decorador.

quebrar o ciclo

Quando ocorre uma dependência circular, a primeira ideia deve ser quebrar o ciclo; deixar a dependência se tornar uma lógica abstrata sem ciclo e quebrar a sequência de execução.

export const IFather = Symbol.for("IFather");
export const ISon = Symbol.for("ISon");

export interface IPerson {
  name: string;
}

@Injectable()
export class FatherWithAbstraction {
  @Inject(ISon)
  son!: IPerson;

  name = "Durotan";
  getDescription() {
    return `I am ${this.name}, my son is ${this.son.name}.`;
  }
}

@Injectable()
export class SonWithAbstraction {
  @Inject(IFather)
  father!: IPerson;

  name = "Thrall";
  getDescription() {
    return `I am ${this.name}, son of ${this.father.name}.`;
  }
}

const container = new ContainerV2(
 { token: IFather, useClass: FatherWithAbstraction },
 { token: ISon, useClass: SonWithAbstraction }
);
const father = container.resolve(FatherWithAbstraction);
const son = container.resolve(SonWithAbstraction);
console.log(father.getDescription())
console.log(son.getDescription())

Ao definir a abstração pública Person, a função getDescription pode ser executada normalmente; ao fornecer os Provedores de ISon e IFather, as implementações específicas de suas respectivas dependências são fornecidas e o código lógico pode ser executado normalmente.

dependência preguiçosa

Além de confiar na abstração, se dependências circulares forem realmente necessárias, ainda podemos resolver esse problema por meios técnicos, ou seja, permitir que a resolução da dependência seja executada após a definição do construtor, e não quando for declarada com o construtor. Neste momento, apenas um método simples é necessário, usando a execução de funções, que é a lógica lenta que mencionamos anteriormente.

Como as variáveis ​​dentro do escopo do JS são promovidas, as referências às variáveis ​​podem ser mantidas nas funções.Desde que as variáveis ​​tenham recebido valores quando a função é executada, as dependências podem ser resolvidas corretamente.

@Injectable()
class Son {
  @LazyInject(() => Father)
  father: Father

  name = 'Thrall'

  getDescription() {
    return `I am ${this.name}, son of ${this.father.name}.`
  }
}

@Injectable()
class Father {
  @LazyInject(() => Son)
  son: Son

  name = 'Durotan'

  getDescription() {
    return `I am ${this.name}, my son is ${this.son.name}.`
  }
}

const container = new Container()
const father = container.resolve(Father)
console.log(father. getDescription())

Fábrica de tokens

O que precisamos fazer é adicionar um novo método de análise de token, que pode usar funções para obter dependências dinamicamente.

interface TokenFactory<T = any> {
  getToken(): Token<T>;
}

Em seguida, adicione um decorador LazyInject e seja compatível com esta lógica.

export function LazyInject(tokenFn: () => Token): any {
  return (
    target: ConstructorOf<any> | object,
    key: string | symbol,
    index?: number
  ) => {
    if (typeof index !== "number" || typeof target === "object") {
      return decorateProperty(target, key, { getToken: tokenFn });
    } else {
      return decorateConstructorParameter(target, index, { getToken: tokenFn });
    }
  };
}

Por fim, torne essa lógica compatível no Container e escreva uma versão V3 do Container.

export class ContainerV3 extends ContainerV2 implements IContainer {
  resolve<T>(tokenOrFactory: Token<T> | TokenFactory<T>): T {
    const token =
      typeof tokenOrFactory === "object"
        ? tokenOrFactory.getToken()
        : tokenOrFactory;

    return super.resolve(token);
  }
}

Por fim, veja o efeito do uso:

const container = new ContainerV3();
const father = container.resolve(FatherWithLazy);
const son = container.resolve(SonWithLazy);
father.getDescription();
son.getDescription();
84b973cbc08fabf26b69f10648f35119.png

afinal

Até agora implementamos basicamente uma ferramenta de DI básica e utilizável. Vamos revisar um pouco nosso conteúdo:

  • Usando reflexão e lógica de decorador TS, implementamos resolução de dependências e criação de objetos;

  • Através da definição do provedor, são realizadas substituição de instância, abstração de dependência e padrão de fábrica;

  • Ao usar PropertyDecorator para definir a função getter, alcançamos a criação preguiçosa;

  • Ao obter dependências dinamicamente por meio do TokenFactory, resolvemos dependências circulares.

- FIM -

Sobre Grupo Qi Wu

Qi Wu Troupe é a maior equipe de front-end do Grupo 360 e participa do trabalho dos membros do W3C e ECMA (TC39) em nome do grupo. Qi Wu Troupe atribui grande importância ao treinamento de talentos e tem várias direções de desenvolvimento, como engenheiros, palestrantes, tradutores, pessoas de interface de negócios e líderes de equipe para os funcionários escolherem, e oferece cursos de treinamento técnico, profissional, geral e de liderança correspondentes. A Qi Dance Troupe dá as boas-vindas a todos os tipos de talentos excepcionais para prestar atenção e ingressar na Qi Dance Troupe com uma atitude aberta e em busca de talentos.

bd69e53b4b655e3ab8d79d03ed94843c.png

Acho que você gosta

Origin blog.csdn.net/qiwoo_weekly/article/details/132288468
Recomendado
Clasificación