Noções básicas de injeção de dependência

Noções básicas de injeção de dependência


Neste artigo, falarei sobre os conceitos básicos da injeção de dependência (Eng. Dependency Injection, DI ) em uma linguagem simples e também sobre os motivos para usar essa abordagem. Este artigo é destinado a quem não sabe o que é injeção de dependência ou que duvida da necessidade de usar essa técnica. Então, vamos começar.


O que é vício?


Vamos ver um exemplo primeiro. Temos ClassA , ClassB e ClassC como mostrado abaixo:


 class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { } 

Você pode ver que a classe ClassA contém uma instância da classe ClassB , portanto, podemos dizer que a classe ClassA depende da classe ClassB . Porque Porque ClassA precisa ClassB para funcionar corretamente. Também podemos dizer que a classe ClassB é uma dependência da classe ClassA .


Antes de continuar, quero esclarecer que esse relacionamento é bom, porque não precisamos de uma classe para fazer todo o trabalho no aplicativo. Precisamos dividir a lógica em diferentes classes, cada uma das quais será responsável por uma determinada função. E, neste caso, as classes poderão interagir efetivamente.


Como trabalhar com dependências?


Vejamos três métodos usados ​​para executar tarefas de injeção de dependência:


Primeira maneira: criar dependências em uma classe dependente


Simplificando, podemos criar objetos sempre que precisarmos deles. Veja o seguinte exemplo:


 class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } } 

É muito fácil! Criamos uma classe quando precisamos dela.


Os benefícios


  • É fácil e simples.
  • A classe dependente ( ClassA no nosso caso) controla totalmente como e quando criar as dependências.

Desvantagens


  • ClassA e ClassB intimamente relacionados entre si. Portanto, sempre que precisarmos usar a ClassA , seremos forçados a usar a ClassB e será impossível substituir a ClassB por outra coisa .
  • Com qualquer alteração na inicialização da classe ClassB , você precisará ajustar o código dentro da classe ClassA (e todas as outras classes dependentes da ClassB ). Isso complica o processo de mudança de dependência.
  • ClassA não pode ser testado. Se você precisar testar uma classe, e ainda assim esse for um dos aspectos mais importantes do desenvolvimento de software, será necessário realizar testes de unidade de cada classe separadamente. Isso significa que, se você deseja verificar a operação correta da classe ClassA e criar vários testes de unidade para verificá-la, então, como mostrado no exemplo, você também criará uma instância da classe ClassB , mesmo quando ela não lhe interessar. Se ocorrer um erro durante o teste, você não conseguirá entender onde ele está localizado - na ClassA ou na ClassA ClassB Afinal, existe a possibilidade de que parte do código da ClassB causado um erro, enquanto a ClassA funcionando corretamente. Em outras palavras, o teste de unidade não é possível porque os módulos (classes) não podem ser separados um do outro.
  • ClassA deve ser configurada para injetar dependências. Em nosso exemplo, ele precisa saber como criar um ClassC e usá-lo para criar um ClassB . Seria melhor se ele não soubesse nada sobre isso. Porque Devido ao princípio da responsabilidade única .

Cada classe deve apenas fazer seu trabalho.

Portanto, não queremos que as classes sejam responsáveis ​​por nada além de suas próprias tarefas. A implementação de dependências é uma tarefa adicional que definimos para elas.


Segunda maneira: injetar dependências através de uma classe personalizada


Portanto, entendendo que injetar dependências em uma classe dependente não é uma boa ideia, vamos explorar uma maneira alternativa. Aqui, a classe dependente define todas as dependências necessárias dentro do construtor e permite que a classe do usuário as forneça. Esta é uma solução para o nosso problema? Nós descobriremos um pouco mais tarde.


Dê uma olhada no código de exemplo abaixo:


 class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium - 

Agora ClassA obtém todas as dependências dentro do construtor e pode simplesmente chamar os métodos da classe ClassB sem inicializar nada.


Os benefícios


  • ClassA e ClassB agora ClassB fracamente acoplados, e podemos substituir a ClassB sem quebrar o código dentro da ClassA . Por exemplo, em vez de passar na ClassB podemos passar AssumeClassB , que é uma subclasse da ClassB , e nosso programa funcionará corretamente.
  • ClassA agora pode ser testado. Ao escrever um teste de unidade, podemos criar nossa própria versão da ClassB (objeto de teste) e passá-la para a ClassA . Se ocorrer um erro ao passar no teste, agora sabemos com certeza que esse é definitivamente um erro na ClassA .
  • ClassB livre de trabalhar com dependências e pode se concentrar em suas tarefas.

Desvantagens


  • Esse método se assemelha a um mecanismo de cadeia e, em algum momento, a cadeia deve ser interrompida. Em outras palavras, o usuário da classe ClassA deve saber tudo sobre a inicialização da ClassB , que por sua vez requer conhecimento sobre a inicialização da ClassC , etc. Portanto, você vê que qualquer alteração no construtor de qualquer uma dessas classes pode levar a uma alteração na classe de chamada, sem mencionar que a ClassA pode ter mais de um usuário, portanto a lógica de criação de objetos será repetida.
  • Apesar de nossas dependências serem claras e fáceis de entender, o código do usuário não é trivial e difícil de gerenciar. Portanto, nem tudo é tão simples. Além disso, o código viola o princípio da responsabilidade única, pois é responsável não apenas por seu trabalho, mas também pela implementação de dependências em classes dependentes.

O segundo método obviamente funciona melhor que o primeiro, mas ainda tem suas falhas. É possível encontrar uma solução mais adequada? Antes de considerar a terceira maneira, vamos primeiro falar sobre o próprio conceito de injeção de dependência.


O que é injeção de dependência?


A injeção de dependência é uma maneira de lidar com dependências fora da classe dependente quando a classe dependente não precisa fazer nada.

Com base nessa definição, nossa primeira solução obviamente não usa a idéia de injeção de dependência, e a segunda maneira é que a classe dependente não faz nada para fornecer as dependências. Mas ainda achamos que a segunda solução é ruim. POR QUE ?!


Como a definição de injeção de dependência não diz nada sobre onde o trabalho com dependências deve ocorrer (exceto fora da classe dependente), o desenvolvedor deve escolher um local adequado para a injeção de dependência. Como você pode ver no segundo exemplo, a classe de usuário não é o lugar certo.


Como fazer melhor? Vejamos uma terceira maneira de lidar com dependências.


Terceira maneira: deixar que outra pessoa lide com dependências em vez de nós


De acordo com a primeira abordagem, as classes dependentes são responsáveis ​​por obter suas próprias dependências e, na segunda abordagem, movemos o processamento de dependências da classe dependente para a classe de usuário. Vamos imaginar que haja alguém que possa lidar com as dependências, como resultado das quais nem as classes dependentes nem as classes de usuários fariam o trabalho. Este método permite trabalhar diretamente com dependências no aplicativo.


Uma implementação "limpa" da injeção de dependência (na minha opinião pessoal)

A responsabilidade de lidar com dependências é de terceiros, portanto, nenhuma parte do aplicativo irá interagir com eles.

A injeção de dependência não é uma tecnologia, uma estrutura, uma biblioteca ou algo assim. Isto é apenas uma ideia. A idéia é trabalhar com dependências fora da classe dependente (de preferência em uma parte especialmente alocada). Você pode aplicar essa ideia sem usar bibliotecas ou estruturas. No entanto, geralmente recorremos às estruturas para implementar dependências, porque simplifica o trabalho e evita a gravação de código de modelo.


Qualquer estrutura de injeção de dependência tem duas características inerentes. Outras funções adicionais podem estar disponíveis para você, mas essas duas funções sempre estarão presentes:


Primeiramente, essas estruturas oferecem uma maneira de determinar os campos (objetos) que devem ser implementados. Algumas estruturas fazem isso anotando um campo ou construtor usando a anotação @Inject , mas existem outros métodos. Por exemplo, Koin usa os recursos de linguagem incorporados do Kotlin para determinar a implementação. Inject significa que a dependência deve ser tratada pela estrutura de DI. O código será algo como isto:


 class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } } 

Em segundo lugar, as estruturas permitem determinar como fornecer cada dependência, e isso acontece em um (s) arquivo (s) separado (s). Aproximadamente, fica assim (lembre-se de que este é apenas um exemplo e pode variar de estrutura para estrutura):


 class OurThirdPartyGuy { fun provideClassC(){ return ClassC() //just creating an instance of the object and return it. } fun provideClassB(classC: ClassC){ return ClassB(classC) } fun provideClassA(classB: ClassB){ return ClassA(classB) } } 

Portanto, como você pode ver, cada função é responsável pelo processamento de uma dependência. Portanto, se precisarmos usar a ClassA em algum lugar do aplicativo, acontecerá o seguinte: nossa estrutura DI cria uma instância da classe provideClassC chamando provideClassC , passando-a para provideClassB e recebendo uma instância da ClassB , que é passada para o provideClassA e, como resultado, a ClassA é criada. Isso é quase mágico. Agora vamos examinar as vantagens e vantagens do terceiro método.


Os benefícios


  • Tudo é o mais simples possível. A classe dependente e a classe que fornece as dependências são claras e simples.
  • As classes são fracamente acopladas e são facilmente substituíveis por outras classes. Suponha que desejamos substituir ClassC por AssumeClassC , que é uma subclasse de ClassC . Para fazer isso, basta alterar o código do provedor da seguinte maneira e, onde quer que ClassC seja usado, a nova versão agora será automaticamente usada:

 fun provideClassC(){ return AssumeClassC() } 

Observe que nenhum código dentro do aplicativo é alterado, apenas o método do provedor. Parece que nada poderia ser ainda mais simples e flexível.


  • Testabilidade incrível. Você pode substituir facilmente dependências por versões de teste durante o teste. De fato, a injeção de dependência é o seu principal auxiliar quando se trata de testes.
  • Melhorando a estrutura do código, como o aplicativo possui um local separado para processamento de dependência. Como resultado, o restante do aplicativo pode se concentrar exclusivamente em suas funções e não se sobrepor às dependências.

Desvantagens


  • As estruturas de DI têm um certo limite de entrada; portanto, a equipe do projeto precisa gastar tempo e estudá-lo antes de usá-lo efetivamente.

Conclusão


  • O tratamento de dependência sem DI é possível, mas pode causar falhas no aplicativo.
  • A DI é apenas uma idéia eficaz, segundo a qual é possível lidar com dependências fora da classe dependente.
  • É mais eficaz usar o DI em certas partes do aplicativo. Muitas estruturas contribuem para isso.
  • Estruturas e bibliotecas não são necessárias para o DI, mas podem ajudar muito.

Neste artigo, tentei explicar o básico sobre como trabalhar com o conceito de injeção de dependência e também listei os motivos para o uso dessa idéia. Há muito mais recursos que você pode explorar para aprender mais sobre o uso do DI em seus próprios aplicativos. Por exemplo, uma seção separada na parte avançada do nosso curso profissional no Android é dedicada a este tópico.

Source: https://habr.com/ru/post/pt434380/


All Articles