Artigo

Qual e a diferença entre Dependency Injection e Dependency Inversion?

Rodrigo Branas
Rodrigo Branas
08 out 2023·4 min de leitura
#solid#dependency-injection

Um dos fatores que limitam a testabilidade e a dependência de recursos externos, principalmente por motivos de performance e consistencia. Imagine que um determinado use case, como por exemplo um checkout, precise processar um pagamento em seu fluxo de execução.

export default class Checkout {
	async execute (input: Input): Promise<void> {
		...
		const paymentGateway = new PayPalGateway();
		const transaction = await paymentGateway.createTransaction(input.payment, amount);
		...
	}
}

type Input = {
	email: string,
	items: { itemId: string, quantity: number }[],
	payment: {
		creditCardNumber: string,
		holderName: string,
		expDate: string,
		cvv: string
	}
}

Em produção, a transacao será processada no gateway de pagamento, nesse caso o PayPal, e dependendo do resultado o status do pedido será determinado como pago ou cancelado. No entanto, durante o desenvolvimento, Não é possível testar essa transacao em um ambiente real, caso contrario seria necessario criar dezenas ou até mesmo centenas de debitos em um cartao de credito para depois estorna-lo, o que poderia inclusive ser classificado pela operadora do cartao de credito como uma tentativa de fraude, possivelmente ocasionando o bloqueio do cartao.

Esse tipo de teste, em um ambiente real, será necessario na homologacao da funcionalidade mas não durante o desenvolvimento.

Com isso temos algumas opcoes, entre elas:

  • Utilizar um Test Pattern como um Stub ou Mock para sobreescrever o comportamento do metodo createTransaction, fazendo com que ele retorne um valor deterministico.
  • Passar uma implementação diferente para o PaymentGateway, fazendo com que ele utilize uma API de sandbox ao inves de produção.
  • Implementar uma versão falsa de PaymentGateway que dependendo do cartao de credito de entrada seja capaz de retornar tanto o sucesso quanto os erros de forma realista e parecida com a versão de produção, de forma rápida, sem precisar ir até a API de sandbox por motivos principalmente de performance.

A primeira opção e mais simples, basta utilizar qualquer biblioteca de testes para sobreescrever o comportamento e tornar os testes isentos desse tipo de dependência. No entanto, para ter a liberdade de variar a implementação de forma polimorfica e necessario segregar uma interface, criando uma porta que pode receber diferentes adaptadores no use case de Checkout.

Ter esse contrato definido nos da controle sobre a dependência, ainda que em um primeiro momento um simples Stub ou Mock pudesse resolver o problema.

export default class Checkout {
	constructor (readonly paymentGateway: PaymentGateway) {
	}

	async execute (input: Input): Promise<void> {
		...
		const transaction = await this.paymentGateway.createTransaction(input.payment, amount);
		...
	}
}

type Input = {
	email: string,
	items: { itemId: string, quantity: number }[],
	payment: {
		creditCardNumber: string,
		holderName: string,
		expDate: string,
		cvv: string
	}
}

O Principio da Inversao de Dependência

Este e o principio de design conhecido como Dependency Inversion, ou DIP, do SOLID. Na essência ele diz que: "High level modules should not depend on low level modules", ou seja, modulos que lidam com regras de negocio, que são considerados de alto nível, não devem depender de conceitos de infraestrutura como o banco de dados ou a rede, que são considerados de baixo nível.

Dessa forma, e possível tornar um use case livre de características especificas de uma implementação externa, não corrompendo conceitos de domínio e agindo como uma especie de camada anti-corrupcao (padrao de integração do Domain-Driven Design). Em termos praticos, existe um risco de ao integrar com o PayPal seu domínio absorver conceitos específicos desse fornecedor como nomes de status para pagamentos ou mesmo tipos de identificador de transacoes. Ao criar esse contrato, e possível ser neutro, adotar uma terminologia alinhada com o seu negocio e fazer a tradução seja pro PayPal, Stripe ou qualquer outro gateway de pagamento.

Repare que o Checkout (HL1) conhece a interface PaymentGateway (I) mas em tempo de execução não sabe exatamente quem vai implementa-la e isso faz com que Checkout (HL1) e PayPalGateway (ML1) sejam desacopladas.

Injecao de Dependência

Nesse exemplo, passamos a dependência por meio do construtor, no entanto, conforme a quantidade de dependencias aumenta, podemos ter um Code Smell conhecido como longa lista de parametros, dificultando a instanciacao e impactando o código existente sempre que uma nova dependência e criada.

Na prática, passar dependencias para uma classe, seja por um construtor, um metodo ou de forma automática e conhecido como Dependency Injection e esse padrao e muito utilizado na comunidade, principalmente de forma automática, onde e possível registrar a dependência de forma global e centralizada, fazendo a injecao onde for necessario.

export default class Checkout {
	@inject("paymentGateway")
	paymentGateway?: PaymentGateway;

	async execute (input: Input): Promise<void> {
		...
		const transaction = await this.paymentGateway.createTransaction(input.payment, amount);
		...
	}
}

type Input = {
	email: string,
	items: { itemId: string, quantity: number }[],
	payment: {
		creditCardNumber: string,
		holderName: string,
		expDate: string,
		cvv: string
	}
}

Nesse caso, em tempo de execução, temos um algoritmo que localiza as dependencias registradas e injeta nos lugares determinados por uma anotacao (no TypeScript esse recurso se chama Decorator).

Muitos frameworks já vem com esse recurso e também e possível utilizar bibliotecas especificas para este proposito. Abaixo, segue uma forma simples de implementação:

export class Registry {
  dependencies: { [name: string]: any };
  static instance: Registry;

  private constructor() {
    this.dependencies = {};
  }

  provide(name: string, dependency: any) {
    this.dependencies[name] = dependency;
  }

  inject(name: string) {
    return this.dependencies[name];
  }

  static getInstance() {
    if (!Registry.instance) {
      Registry.instance = new Registry();
    }
    return Registry.instance;
  }
}

export function inject(name: string) {
  return (target: any, propertyKey: string) => {
    target[propertyKey] = new Proxy(
      {},
      {
        get(target: any, propertyKey: string, receiver: any) {
          const dependency = Registry.getInstance().inject(name);
          return dependency[propertyKey];
        },
      }
    );
  };
}

Dessa forma, vamos registrar a dependência durante a inicializacao da aplicação, no entrypoint, também conhecido como Composition Root.

const paymentGateway = new PayPalGateway();
Registry.getInstance().provide("paymentGateway", paymentGateway);
const checkout = new Checkout();

Quando utilizar

Não vejo necessidade em recorrer a este tipo de recurso apenas para desacoplar uma biblioteca específica como por exemplo um gerador de token ou manipulacao de date. Sempre existira algum acoplamento com framework ou biblioteca e o ponto aqui Não é esse, e desacoplar os recursos externos que impedem a testabilidade e afetam o design.

Utilize principalmente para desacoplar o banco de dados, uma API externa, uma Fila ou até mesmo um sistema de arquivos, evitando que os testes sejam prejudicados por este tipo de dependência. Se você quiser criar um Adapter em torno de uma biblioteca que pode mudar no futuro, sem problemas, mas são conceitos completamente diferentes.

Lembre-se que uma coisa e o principio de design e outra e a implementação deste principio.

Continue aprendendo

Já conhece nossas formações?

Os desenvolvedores que vão se manter no futuro são os que dominam a Inteligência Artificial e são arquitetos de software. Aprenda como ser esse tipo de desenvolvedor.

Newsletter

Novidades de IA direto no seu inbox.

Cookies e privacidade

Utilizamos cookies para melhorar sua experiência, analisar o tráfego do site e personalizar conteúdo. Você pode aceitar todos, rejeitar ou personalizar suas preferências.