Um dos maiores pesadelos de qualquer desenvolvedor é ter que inserir funcionalidades não previstas no escopo inicial do projeto, e perceber que sua arquitetura perfeita e modular não é tão perfeita assim.
Quem nunca olhou uma classe durante horas pensando: Como diabos eu vou acrescentar isso. Ou ainda, tentar levar uma classe que julgava ser modular e quando vê teve que copiar metade do projeto, pois várias dependências não previstas apareceram.
Escrever código capaz de sobreviver bem a mudanças de escopo, modular de verdade está menos relacionado a inteligência e muito mais a experiência, cada um desses pequenos traumas que vamos desenvolvendo durante nossas carreiras nos torna capazes de ver os potenciais problemas por um diferente ângulo, e desenvolver passa a ser um jogo de xadrez, onde você tenta evitar cometer os mesmos erros anteriores.
Isso não é fácil.
Faz parte da vida do programador a busca constante por melhores estratégias de arquitetar seu código, a busca código perfeito digamos assim.
Numa dessas minhas jornadas encontrei um video muito interessante da Niantic, a produtora de Pokemon Go, e traz diversos pontos extremamente válidos sobre desenvolvimento de software. O principal ponto foi como eles utilizaram Injeção de Dependência para resolver vários problemas que projetos maiores enfrentam ao utilizar o Unity, e exemplificam a incrível biblioteca Zenject.
Mas o que diabos Injeção de Dependência ?
*Para quem sabe inglês recomendo ler o material original do Zenject, de onde me baseei para criar os exemplos
Normalmente para realizarmos algum trabalho útil dentro de uma classe precisamos interagir com outras, imagine:
1 2 3 4 5 6 7 8 9 10 |
class Obj{ IService service; public Obj(){ service = new Service(); } public void doSomething(){ service.doTask(); } } |
Não chega a ser um problema em pequenos projetos, porém a classe Obj está acoplada a classe Service, o que começa a ser um problema enorme conforme o projeto cresce, você pode necessitar de diversos serviços, a criação de Service pode alterar, e você precisa alterar Obj também, ou pior, o construtor de Service pode vir a ter uma nova dependência em outra classe, o que forçaria Obj a propagá-la.
Não demora muito e percebemos que seria muito mais interessante:
1 2 3 4 5 6 7 8 9 |
class Obj{ IService service; public Obj(IService _service){ service = _service; } public void doSomething(){ service.doTask(); } } |
Com essa mudança simples a classe Obj diminui seu acoplamento com a classe Service, porém imagine uma classe que utiliza a nossa Obj, deveria se parecer com:
1 2 3 4 5 6 7 |
class OtherObj{ Obj obj; public OtherObj(){ obj = new Obj(new Service()); obj.doSomething(); } } |
Isso também é um problema, além dos mesmos argumentos anteriores, estamos passando a responsabilidade de OtherObj escolher qual implementação da interface IService será utilizada, o que faríamos se ao invés de utilizar Service quiséssemos utilizar Service2?
Novamente poderíamos fazer algo como:
1 2 3 4 5 6 7 |
class OtherObj{ Obj obj; public OtherObj(IService service){ obj = new Obj(service); obj.doSomething(); } } |
Aplicando essa lógica a demais classes, acabamos deslocamos a responsabilidade de escolher qual implementação utilizar para cada vez mais alto no grafo de classes, em algum momento teríamos algo como:
1 2 3 4 5 |
Iservice service = new Service2() OtherObj oobj = new OtherObj(service) Obj obj = new Obj(service) ... |
Se levarmos isso ao extremo, teremos o caso onde antes mesmo do programa iniciar seu trabalho, todas as dependências terão sido resolvidas.
Bibliotecas de DI (Dependency Injection) como o Zenject automatizam esse processo.
Zenject
Caso utilizássemos o Zenject, poderíamos reescrever o exemplo como:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Obj{ [Inject] IService service; public void doSomething(){ service..doTask(); } } class OtherObj{ [Inject] Obj obj; public OtherObj(){ obj.doSomething(); } } |
Dessa forma, as classes não precisam se preocupar com a criação dos objetos e suas dependências, o que cria um padrão mais saudável conforme o projeto cresce. Inclusive, um bom critério para dividir uma classe é quando ela começa a ter dependências demais, mas isso é assunto para outro post.
Outro benefício não abordado aqui, é que ao utilização Injeção de Dependência, tornamos o código amigável a testes unitários, já que podemos substituir as dependências por mocks (objetos falsos) de forma a testar o código eliminando as dependências externas. Também será abordado num próximo artigo.
Agora que já temos uma ideia do que é Injeção de Dependência, num próximo artigo vou entrar mais em detalhes sobre como utilizar Zenject em um código um pouco maior!
Por enquanto é isso!