Executando verificação de segurança...
24

Injeção de Dependência em Javascript

Injeção de dependência (DI) é uma técnica extremamente poderosa, mas que por vezes assusta as pessoas, principalmente na comunidade JS, onde o assunto ainda não é tão difundido.

Apesar do nome assustar um pouco, vou mostrar que é uma técnica menos complicada do que parece, que você provavelmente já usa em alguma medida sem saber, e que usada sistematicamente vai te ajudar a escrever código mais desacolpado e testável.

Molhando os Pés

Imagine que na sua aplicação você precisa formatar datas de diferentes maneiras em diferentes lugares, então você cria uma função formatdate que recebe um Date, um formato (uma string que descreve o formato) e retorna a data formatada:

export const formatDate = (date, format) => {
  //...
};

// Exemplos de uso

formatDate(new Date(), "YYYY/MM/DD"); // 2023/19/01
formatDate(new Date(), "DD-MM"); // 19-01

Conforme sua aplicação vai evoluindo, você percebe que em determinada parte da dela você está usando a mesma formatação, por exemplo, formatDate(date, "DD-MM-YYYY") e então pra não ter que ficar repetindo esta mesma formatação o tempo todo e, você decide criar uma abstração:

// Supondo aqui hipoteticamente que esta parte
// da aplicação que usa a mesma formatação é a parte
// de contabilidade
export const formatDateForAccounting = (date) => formatDate(date, "DD-MM-YYYY");

Além do benefício de que agora você não precisa repetir o formato, esta abstração também acaba por esconder o detalhe da formatação específica que ela usa e qual é a string que a representa, de forma que:

  1. Quem usa esta função agora tem uma informação a menos pra se preocupar.
  2. Como a formatação virou detalhe de implementação, se a gente tiver que mudar a formatação ou mesmo a forma como a gente a representa, todos os lugares que usam formatDateForAccounting vão estar protegidos desta mudança, e o único local que teremos que mudar é na definição da função.

O que acabamos de fazer é injeção de dependência.

É isto pessoal e obrigado por virem ao meu TED talk.

Brincadeiras à parte, a abstração que criamos é um exemplo concreto de injeção de dependência.

Isto porque injeção de dependência consiste em criar "versões" de funções a partir da "fixação" (injeção) de alguns dos parâmetros (dependências) dessa função.

E é exatamente o que fizemos no nosso exemplo, onde a partir da função formatDate que é flexível o bastante para que possamos escolher qual formatação específica nós queremos usar, nós criamos uma "versão" mais restrita dela chamada formatDateForAccouting, na qual a formatação já está pré-fixada.

Dentro do jargão da injeção de dependência, nós dizemos que "formatDateForAccounting é uma versão de formatDate com a formatação injetada".

Entrando na Água

Até agora, pode parecer que injeção de dependência não é grande coisa, mas agora eu vou mostrar pra vocês o pulo do gato.

Imagine que na sua aplicação você está implementando uma função pay que:

  1. Recebe os dados do cartão de crédito do usuário.
  2. Tokeniza o cartão de crédito.
  3. Chama a API de algum gateway de pagamento passando o cartão de crédito tokenizado.
  4. Se o pagamento der certo, a função só retorna normalmente, e se der errado, loga uma mensagem de erro.
  5. Se a tokenização falhar, a API do gateway de pagamento não é chamada.

Algo mais ou menos assim:

const pay = async (creditCard) => {
  try {
    const token = await tokenizeCreditCard(creditCard);

    if (token !== undefined) {
      throw new Error("Failed to tokenize credit card!");
    }

    await processPayment(token);
  } catch (error) {
    logger.error(error);
  }
};

Como é que você faria pra testar esta função unitariamente, ou seja, sem ter que bater nas APIs de fato, ou fazer chamadas HTTP?

Esta função não retorna nada, quase tudo que ela faz é interagir com sistemas externos e produzir efeitos colaterais (side-effects), o que a torna bem difícil de testar unitariamente.

Toda vez que algo está difícil de testar, é porque existem coisas que estão fora do nosso controle e portanto é difícil (ou impossível) manipulá-las pra que elas se comportem da forma que precisamos nos nossos testes.

Neste caso, como tokenizeCreditCard, processPayment e logger estão hardcoded dentro de pay, é como se eles estivessem fora do nosso alcance nos testes.

E aqui que mora o pulo do gato, porque da mesma forma que nós podemos criar versões mais restritas de funções fixando seus argumentos, a gente também pode fazer o processo inverso, ou seja, criar versões mais flexíveis de funções, parametrizando coisas que outrora estavam hardcoded.

Portanto, podemos fazer:

const pay = async (tokenizeCreditCard, processPayement, logger, creditCard) => {
  try {
    const token = await tokenizeCreditCard(creditCard);

    if (token !== undefined) {
      throw new Error("Failed to tokenize credit card!");
    }

    await processPayment(token);
  } catch (error) {
    logger.error(error);
  }
};

Perceba que a implementação da nossa função continua exatamente igual, porém agora, nós temos controle sobre tokenizeCreditCard, processPayment e logger, uma vez que nós conseguimos passar essas funções para pay.

Por conta disto, nós podemos passar versões mockadas destas funções para pay, de forma que nós não só conseguimos fazer com que elas se comportem da forma que precisamos, como também conseguimos "espiá-las", para saber se elas foram chamadas, com quais argumentos, e etc.

E agora, no nosso teste:

describe("When credit card tokenization fails", () => {
  const setup = () => {
    // Forçando o erro na tokenização
    const tokenizeCreditCard = () => {
      return undefined;
    };
    // Para podermos "espiar" esta função

    const processPayment = jest.fn();

    // Provavelmente a implementação "real" deste logger
    // possui mais métodos, mas como estamos num teste,
    // nós podemos mockar só os métodos que nos interessam
    const logger = {
      error: jest.fn(),
    };

    const creditCard = {
      number: "4242 4242 4242 4242",
      expiration: "10/25",
      cvc: "123",
    };

    pay(tokenizeCreditCard, processPayment, logger, creditCard);

    return {
      processPayment,
      logger,
    };
  };

  it("Logs error", () => {
    const { logger } = setup();

    expect(logger.error).toHaveBeenCalledTimes(1);
  });

  it("processPayment is NOT called", () => {
    const { processPayment } = setup();

    expect(processPayment).not.toHaveBeenCalled();
  });
});

describe("When payment processing fails", () => {
  const setup = () => {
    const tokenizeCreditCard = () => "Some Token";

    const processPayment = () => {
      throw new Error("");
    };

    const logger = {
      error: jest.fn(),
    };

    const creditCard = {
      number: "4242 4242 4242 4242",
      expiration: "10/25",
      cvc: "123",
    };

    pay(tokenizeCreditCard, processPayment, logger, creditCard);

    return {
      logger,
    };
  };

  it("Logs error", () => {
    const { logger } = setup();

    expect(logger.error).toHaveBeenCalledTimes(1);
  });
});

describe("When payment succeeds", () => {
  const setup = () => {
    const tokenizeCreditCard = () => "Some Token";

    const processPayment = () => {
      // No op
    };

    const logger = {
      error: jest.fn(),
    };

    const creditCard = {
      number: "4242 4242 4242 4242",
      expiration: "10/25",
      cvc: "123",
    };

    pay(tokenizeCreditCard, processPayment, logger, creditCard);

    return {
      logger,
    };
  };

  it("Does not log anything", () => {
    const { logger } = setup();

    expect(logger).not.toHaveBeenCalled();
  });
});

Eu imagino que agora você provavelmente está pensando:

Mas a gente não poderia simplesmente mockar os módulos como um todo, usando por exemplo o jest.mock ?

E a resposta é sim, poderíamos, mas eu acredito que a injeção de dependência é uma abordagem superior pelos seguintes motivos:

  • Injeção de dependência não está atrelada a uma biblioteca específica, ela vai funcionar com qualquer framework de testes.
  • O jest.mock vive dando dor de cabeça pelo fato dele ter que fazer monkey patching com o require/import, e inclusive já tive projetos meus que TODOS os mocks quebraram por atualizar a versão de coisas como o NextJS.
  • Ao contrário do jest.mock, a injeção de dependência não se importa se você está usando require ou import, se você está usando CommonJS ou ESM, it just works.
  • Com injeção de dependência, você consegue organizar seus testes de maneira que você nunca vai precisar lembrar de resetar/limpar seus mocks, uma vez que você consegue deixar eles escopados em cada teste/setup.

Além disto, injeção de dependência nos dá uma abordagem uniforme pra lidar com mocks/valores que a gente quer controlar nos nossos testes.

Por exemplo, digamos que nós temos uma função payWithRetry que vai fazer o pagamento e, caso ele dê erro em algum lugar, ele vai retentar o pagamento um certo número de vezes controlado por uma variável de ambiente PAY_RETRY_TIMES:

const payWithRetry = async (creditCard) => {
  let times = 1;
  while (times < process.env.PAY_RETRY_TIMES) {
    try {
      await pay(creditCard);
      return;
    } catch {
      times++;
    }
  }
};

Se a gente quiser testar esta função com diferentes quantidades de retries, ao invés de termos que ficar mutando o process.env nos nossos testes, e lembrando de ficar limpando/restaurando ele a cada teste, com injeção dependência, nossa abordagem é idêntica à anterior:

const payWithRetry = async (payRetryTimes, creditCard) => {
  let times = 1;
  while (true) {
    try {
      await pay(creditCard);
      return;
    } catch(error) {
      if(times < payRetryTimes) {
        times++;
        continue;
      }
      
      throw error;
    }
  }
};

Basta parametrizarmos a quantidade de retries.

E se estivermos no frontend e a nossa função de pagamento redireciona o usuário para alguma página específica e quisermos checar que esse redirecionamento está sendo feito de fato?

const pay = async (creditCard) => {
  //...

  window.location.assign("https://example.com");
};

Novamente, ao invés de termos que mutar/mockar objetos globais, principalmente nos testes que rodam em Node e não no browser, basta recebermos a window como parâmetro:

const pay = async (window, creditCard) => {
  //...

  window.location.assign("https://example.com");
};

Desta forma, nos nossos testes podemos substituir a window original por um spy.

A mesma coisa vai funcionar com outros globais como Math.random, setTimeout, setInterval, Date, entre outros.

Usando as utilities de bibliotecas de teste/mock, cada uma dessas situações que vimos, vai ter alguma técnica ou API específica para que possa ser mockada, porém com injeção de dependência, todos os casos são resolvidos da mesma forma.

Nadando

Tá, mas só tem um problema, antes, quando eu precisava fazer um pagamento, bastava chamar a função pay passando o cartão de crédito, só que agora eu tenho que ficar passando também tokenizeCreditCard, processPayment e o logger em todos os lugares que eu for chamar pay?

Calma jovem, que aqui vem o segundo pulo do gato:

Da mesma forma que lá trás, a gente tinha a função formatDate, pra ser usada nos contextos onde a flexibilidade de escolhar a formatação é importante e, ao mesmo tempo, uma versão mais restrita, formatDateForAccounting, onde essa flexibilidade não é importante, a gente vai criar duas versões da nossa função pay.

// Importamos as implementações "reais"
import { tokenizeCreditCard } from "./tokenizeCreditCard";
import { processPayment } from "./processPayment";
import { logger } from "./logger";

// makePay é uma função que "cria" `pay`.
// Recebemos as dependências como parâmetro
// e usamos as implementações "reais" como default
// Este formato é equivalente ao primeiro onde
// tínhamos uma única função, porém desta forma
// além de ficar explícito quais são as dependências,
// também fica mais fácil de mockar apenas algumas
// dependências específicas, enquanto as outras
// vão usar as dependências "originais"
export const makePay =
  ({
    tokenizeCreditCard = tokenizeCreditCard,
    processPayment = processPayment,
    logger = logger,
  }) =>
  (creditCard) => {
    //...
  };

// É esta a função que será importada
// pela nossa aplicação, de forma
// que ela não precise ficar passando
// as dependências de `pay` em todo lugar
// e, tão pouco saber que elas existem
export const pay = makePay();

E aqui fechamos o ciclo de injeção de dependência.

Primeiro, nós parametrizamos as dependências que estavam hardcoded na nossa função para que possamos usar implementações mockadas dessas dependências nos testes.

Depois, criamos uma versão mais restrita de pay, com as dependências já injetadas, para usarmos na aplicação, uma vez que ela não precisa ter controle sobre quais implementações estão sendo usadas (em runtime).

Perceba que agora, pay recebe apenas o cartão de crédito, enquanto makePay é a função que nos permite criar uma "versão alternativa" de pay substituindo suas dependências.

Tornar uma aplicação testável é provavelmente o caso de uso mais comum de injeção de dependência, mas em geral, ela vai nos ajudar sempre que precisarmos usar implementações diferentes para as dependências de uma determinada função.

Dois exemplos comuns disto, são:

Quanto estamos usando feature flags.

const foo = () => {
  //...

  if (process.env.ALTERNATE_FOO) {
    //...
  }

  //...

  if (process.env.ALTERNATE_FOO) {
    //...
  }

  //...
};

Onde ao invés de lotarmos de "ifs", podemos fazer:

const originalFoo = () => {
  // ...
};

const altFoo = () => {
  //...
};

export const foo = process.env.FEATURE_FLAG_ALTERNATE_FOO ? altFoo : foo;

Ou quando estamos trabalhando no frontend com aplicações universais, que rodam tanto no servidor quanto no cliente (e.g. NextJS) e, por vezes, precisam de implementações diferentes para cada um desses ambientes:

const clientFoo = () => {
  // ...
};

const serverFoo = () => {
  // ...
};

const isServer = typeof window === "undefined";

export const foo = isServer ? serverFoo : clientFoo;

Fin

Injeção de dependência é um assunto extremamente extenso e que por isso não daria pra cobrir tudo aqui, porém com o que vimos é possível entender a essência da injeção de dependência e utilizar a maior parte dos seus benefícios.

Dito isto, ainda haveria muito a se falar, como por exemplo o uso de injeção de dependência com classes (que inclusive é como surgiu), framekworks de injeção de dependência, containers, service locators, e enfim, a lista é longa.

Assim, se você deseja fazer um mergulho mais profundo no assunto, vou deixar aqui um outro post bem mais extenso e aprofundado sobre o assunto que escrevi:

https://blog.codeminer42.com/dependency-injection-in-js-ts-part-1/

Até mais, e obrigado pelos peixes!

Carregando publicação patrocinada...
3

como faz falta um botão de favoritar esse tipo de post...
preciso me organizar pra encapsular conteúdo assim, é mudança de paradigma a ponto de sobrescrever uma vida inteira de if-elses desnecessários
parabéns, e toma aqui meus tabcoins! é pouco, mas é honesto

2
1
1
1
1
1

Opa, muito bom o conteúdo. Trabalho com injeção de dependência desde a minha primeira experiência em código como profissional, pois onde trampo todos os Microservices usam o Awilix, que é justamente uma lib Node pra fazer parte desse processo.

Eu só fiquei pensativo sobre algo: Ingeção de dependência é o mesmo que inversão de dependência? Eu tinha pra mim que "inversão" era justamente a estratégia de desacoplamento de uma classe de suas dependências, sendo que as mesmas passam a ser recebidas por parâmetro e a classe só se preocupa em usar os métodos que desejar.

Já a "injeção" seria instanciar todas os módulos, talvez globalmente, para que as classes
não precisem sequer saber onde estão esses parâmetros.

Veja que, apesar da classe receber os módulos que precisa, o "arquivo" da classe ainda precisa saber exatamente onde se encontra aquele módulo. E se o módulo mudar de lugar? E se outras classes quiserem usar o mesmo módulo, todas vão ter que saber onde eles estão? Esse tipo de coisa, por exemplo, pode ser resolvido com libs como "Awilix", que infelizmente tem pouca documentação na internet. Com ferramentas como essa você pode indicar em quais pastas se encontram todos os módulos "públicos" e instanciá-los. Assim, cada classe que quiser usar, apenas precisa desustruturar o módulo desejado de uma "instância global".

Já o lance do Feature Flag (conheço como feature toggle") é muito legal. Não tinha pensado nessa estratégia. Show.

Peço desculpas se o raciocínio ficou confuso, mas acho que a ideia geral ficou minimamente clara.

Parabéns pelo post!!

1

Seu raciocínio está no caminho correto.

Acho que consigo ajudar a esclarecer alguns detalhes.

De fato, apesar de inversão de dependência (o "D" no SOLID) e injeção de dependência terem relação estreita um com o outro, como você mesmo observou, são coisas diferentes.

Inversão de dependência é sobre fazer seus serviços (palavra guarda-chuva pra significar funções, classes, objetos) não dependerem diretamente de implementações concretas, mas sim de abstrações, de maneira que um determinado serviço não conheça diretamente as dependências que está utilizando.

Injeção de dependência é uma técnica específica que pode ser utilizada pra alcançarmos a inversão de dependências, mas, dito isto, é possível usar injeção de dependência sem necessariamente estar "invertendo" elas.

Inclusive, na forma específica de injetar dependências que eu mostrei, acontece justamente isto, estamos injetando dependências sem inverter elas, dado que pelo fato desta injeção acontecer no mesmo arquivo que o serviço está sendo implementado, o serviço sabe exatamente quem são as suas dependêndencias e onde elas se encontram.

Isto foi proposital, porque como eu queria fazer uma breve introdução à injeção de dependência, achei mais proveitoso mostrar a técnica de um jeito que, ainda que simplificado, vai trazer vários benefícios.

Para alcançarmos inversão de dependências usando DI, a gente invariavelmente vai acabar caindo na necessidade de ter um container, que é o único lugar que vai saber sobre as implementações concretas de todas as dependências e vai plugar elas todas.

Mas ó, um ponto interessante é que não necessariamente a gente precisa de uma lib/framework de injeção de dependência pra termos um container, dá pra gente montar um container manualmente.

No artigo que eu citei no final do post, eu dou alguns exemplos disto: https://blog.codeminer42.com/dependency-injection-in-js-ts-part-1/

1

Achei interessante, pra mim injeção de dependencias se praticava mais na Orientação a Objetos, mas nunca tinha pensado por esse ponto, uma duvida que me ficou, e se eu poderia usar esse metodo de injeção de dependencias pra trabalhar com componentes do react, sendo funcional e sem o uso do typeScript, talvez eu tenho uma ideia de como abstrair um componente para ele ser mais generico, porem esta um pouco nebuloso em minha mente, alguém teria algum exemplo, ou dica de como eu poderia fazer essa abstração?

2

De fato, injeção de dependências surge, "originalmente" num contexto de orientação a objetos, inclusive uma das ideias desse post era mostrar que também é plenamente possível, e até menos burocrático, de aplicar essa técnica num contexto mais """funcional""".

Respondendo a sua pergunta sobre o React: Sim, é possível usar injeção de dependências com qualquer coisa, inclusive com o React e seus componentes, segue um exemplo abaixo:

Imagine que você tem um componente <Hero /> que representa a seção "Hero" da homepage da sua aplicação e, dentro dessa <Hero />, você tem um subcomponente <HeroImage />.

export const Hero = () => {
    // Esse componente pode fazer várias coisas
    // na sua implementação e retornar vários outros
    // componentes, mas por simplicidade, 
    // vamos fingir que ele retorna somente o <HeroImage />

    return (
        <HeroImage />
    );
}

Vamos considerar que esse <HeroImage /> possui duas implementações diferentes porque estamos fazendo um teste A/B, de forma que metade dos nossos usuários vai receber uma implementação e a outra metade, a outra.

Podemos adaptar o <Hero /> (que afinal é uma função), da seguinte maneira:

export const makeHero = ({ HeroImage }) => () => {
    return (
        <HeroImage />
    );
}

// Aqui estou assumindo que a variante está vindo de uma 
// variável de ambiente, mas ela poderia vir de qualquer outro
// lugar
export const Hero = makeHero({
    HeroImage: process.env.HERO_IMAGE_VARIANT === "A" ? HeroImageA : HeroImageB
})

Perceba que continuamos exportando o componente <Hero /> e a assinatura dele continua exatamente igual, de forma que quem consome esse componente não precisa fazer nenhuma mudança.

Obs: Aqui a gente está escolhendo a implementação do <HeroImage /> em "boot time", ou seja, logo que a aplicação é "inicializada", porém é possível fazer isso mais "adiante" na aplicação, por exemplo, puxando a variante que vamos utilizar de uma API, porém para isto precisaríamos de uma maneira um pouco mais sofisticada de utilizar injeção de dependência, como por exemplo usando um container.

1

E bem mais simples do que eu imaginei kkk, deu uma boa esclarecida nas ideias, vou começar a praticar a injeção de dependencias em alguns cenarios, Muito obrigado pelo exemplo Henrique!

1
1
1

Ótimo ponto abordado e explicação muito assertiva, mas fiquei com uma dúvida, quando você disse a respeito de tornar a função mais testavel logo me veio a mente o TDD e pensando por esse lado não dificultaria mais o processo de escrever os testes?
Acho que é uma ótima medida pra uma refatoração no código posteriormente mas não sei se a melhor das óticas para encarar o desenvolvimento desses tipos de funções.
ps: sou só um estudante de programação, me perdoa se falei groselha

1

Pelo contrário, injeção de dependência anda de mãos dadas com TDD, principalmente se a gente estiver desenvolvendo "de cima para baixo".

Por exemplo, vamos voltar pro exemplo do payWithRetry do post:

const payWithRetry = async (payRetryTimes, creditCard) => {
  let times = 1;
  while (true) {
    try {
      await pay(creditCard);
      return;
    } catch(error) {
      if(times < payRetryTimes) {
        times++;
        continue;
      }
      
      throw error;
    }
  }
};

Imagine que neste momento a gente ainda não tem a implementação do pay (pois estamos desenvolvendo "de cima pra baixo") e, antes de ter escrito esta implementação, a gente tivesse começado com o seguinte teste:

describe("When pay fails more times than `payRetryTimes`", () => {
  it("Throws error", () => {
    const creditCard = {
      //...
    };

    expect(payWithRetry(3, creditCard)).rejects.toThrow();
  });
});

Como é que a gente faz pra de fato testar este caso específico do payRetryTimes?

O primeiro obstáculo é que pay ainda não está implementado e, segundo, mesmo que estivesse, como ele chama um serviço externo, não é fácil fazer forçar a falha dele pra que nós possamos testar este caso.

É aí que entra a injeção de dependência.

Primeiro a gente ajusta a implementação de payWithRetry pra receber pay como parâmetro:

export const makePayWithRetry =
  ({ pay }) =>
  async (payRetryTimes, creditCard) => {
    let times = 1;
    while (true) {
      try {
        await pay(creditCard);
        return;
      } catch (error) {
        if (times < payRetryTimes) {
          times++;
          continue;
        }

        throw error;
      }
    }
  };

Depois, ajustamos nosso teste pra passarmos uma versão mockada de pay que vai se comportar exatamente como precisamos para o nosso teste:

describe("When pay fails more times than `payRetryTimes`", () => {
  it("Throws error", () => {
    const payMock = jest.fn().mockImplementation(() => {
      throw new Error("");
    });
    const creditCard = {
      //...
    };

    const payWithRetry = makePayWithRetry({ pay: payMock });

    expect(payWithRetry(3, creditCard)).rejects.toThrow();
  });
});

Veja que, desta maneira, nós podemos testar a lógica do payWithRetry mesmo sem termos a implementação do pay.

1
1

Esse é o tipo de conteúdo pelo qual eu fico atualizando a página, muito bem escrito, usando de artifícios como storytellingn, onde nós como leitores conseguimos identificar claramente o início o meio e o fim, e sem falar na qualidade técnica. Quando vi a quantidade de coins, também não entendi como uma publicação como essa não estava no topo da página relevante.

1

Meu amigo, conteúdo riquíssimo esse! Quando ví o título já achei que você iria falar o Awilix, que é um framework para injeção de dependências. Eu acho terrível esse framework e forma "caixa preta" como ele trabalha certos aspectos da injeção e seu artigo só mostra como é possível fazer uma gestão de DI sem precisar se frameworks externos!
Parabéns!

1

No meu trampo, todos os Microsseriços usam o Awilix. De fato, é uma caixa preta. E na internet, mesmo em inglês, tem muito pouco conteúdo sobre. Inclusive, uma sublib de uma lib que o Awilix usa tava com vulnerabilidade esses dias. Não tinha, inclusive, como contornar, pois a versão mais atualizada, 8.0 se não me engano, ainda era vulnerável.

1
1
1

Eu entendo que possa dar esta impressão, principalmente pelo fato de que a minha explicação de DI é, de certa forma, simplificada.

Dito isto, cabe observar que, ainda que seja possível usar Strategy sem DI, na grande maioria dos casos, DI faz uso de Strategy.

Imagine, por exemplo, que temos um use case createUser que depende de um UsersRepository e que este, por sua vezes, possui três implementações possíveis:

  • SqliteUsersRepository -> Que usa SQLite
  • PostgresUsersRepository -> Que usa Postgres
  • InMemoryUsersRepository -> Que faz as operações in-memory

Uma possível implementação pra esse nosso application service é:

export const createUser = async (userDto) => {
  const persistenceMechanism = process.env.PERSISTENCE_MECHANISM;

  const repositoryMatrix = {
    "SQLite": SqliteUsersRepository,
    "Postgres": PostgresUsersRepository,
    "InMemory": InMemoryUsersRepository
  };

  const repository = repositoryMatrix[persistenceMechanism];

  const createdUser = await repository.createUser(userDto);

  return createdUser;
};

E no nosso controller:

app.post("/users", async (req, res) => {
  const userDto = req.body;

  const createdUser = await createUser(userDto);

  res.send(createdUser);
});

Neste caso, estamos usando Strategy, mas não estamos usando DI, dado que não estamos criando uma versão de createUser com as suas dependências pré-fixadas.

Todavia, se fizermos:

export const makeCreateUser = ({ usersRepository }) => async (userDto) => {
  const createdUser = await usersRepository.createUser(userDto);

  return createdUser;
};

E no controller (poderiamos ter um container, mas farei no controller pra simplificar):

app.post("/users", async (req, res) => {
  const persistenceMechanism = process.env.PERSISTENCE_MECHANISM;

  const repositoryMatrix = {
    SQLite: SqliteUsersRepository,
    Postgres: PostgresUsersRepository,
    InMemory: InMemoryUsersRepository,
  };

  const repository = repositoryMatrix[persistenceMechanism];
  
  const createUser = makeCreateUser({
    usersRepository: repository
  })

  const userDto = req.body;

  const createdUser = await createUser(userDto);

  res.send(createdUser);
});

Ainda temos Strategy, mas adicionalmente temos DI, dado que o createUser é criado a partir de uma factory function, onde injetamos a estratégia que queremos usar.

O ponto de "virada" entre Strategy e DI, é que DI envolve sempre alguma aplicação parcial de funções.

0
0