
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()
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.