Olá, agora vou tocar em um assunto que parece uma ferida para muitos, mas considero bem básico. Infelizmente, acredito haja quem confunda estes conceitos, e por ter encontrado pela estrada alguns projetos com tais problemas relacionados à compreensão de IoC e DI, venho escrever esse post. É um tema tão cotidiano que hoje está disponível também no Macoratti*, portanto não vou explicar o conceito, mas me ater no que vejo de errado por aí.
Service Locators
A questão que é que vejo em muitos lugares, implementações de Service Locators e Factories como sendo consideradas implementações de IoC e/ou DI. Esse é um equívoco gigantesco e causa problemas não só estruturais, como o aumento da complexidade nas soluções, também afeta o design de grandes sistemas. É o tipo de problema que você leva meses para resolver, pois geralmente está proliferado em todo o código e não é difícil encontrarmos outros problemas graves nas modelagens, causados geralmente pela abordagem.
Vamos ao que interessa, vamos falar de algumas soluções que vejo no lugar de IoC / DI.
1) Service Locator é um padrão para resolução de dependências, mas não é um padrão para injeção das dependências.
Quando seu código chama um Service Locator para obter uma dependência, a classe em questão está assumindo o controle da execução, e está apenas delegando para um terceiro (o Service Locator) o papel de encontrar uma instância correta. Se houvesse injeção de dependência, sua classe não precisaria chamar um Service Locator, a dependência já estaria injetada e pronta para uso. Nesse caso, você não inverteu o controle, e sequer injetou a dependência. Isso significa que cada classe que precise de uma dependência, precisa saber construí-la ou solicitá-la, e é esse o problema em questão.
Essa abordagem geralmente funciona bem quando só há uma única implementação possível para cada dependência. Bastando haver duas ou mais implementações possíveis para um mesmo tipo, para que este padrão exija deformações no seu design. O problema fica pior quando você quer duas instâncias da mesma classe, configuradas de forma diferente.
Exemplo:
Contexto
Vou dar o exemplo de um Aluno que estuda. Definimos uma classe Aluno, que possui um método Estudar. Para incrementar o problema, vamos assumir que este método faça uso de um “local de estudo”, modelado como uma propriedade chamada LocalDeEstudo, do tipo ILocalDeEstudo, uma interface implementada por duas classes: Escrivaninha e MesaDeJantar.
Imagine que o problema para resolver é representar dois alunos que estudam em locais diferentes, depois solicitar que ambos estudem. Para o exemplo teremos Aluno1, que estuda numa escrivaninha enquanto o segundo, chamado de Aluno2, estuda em uma Mesa de Jantar.
Problema
Como você preencheria cada instância com uma implementação diferente de ILocalDeEstudo?
Algumas soluções possíveis
Se você pensou em um IF no construtor, esqueça! Isso gera um aumento de complexidade equivocado, um erro de estratégia que penaliza o design. Afinal, sobre dependências, você não deveria precisar saber sobre o contexto ao qual é colocado, deveria apenas fazer seu trabalho, desde que tenha sua dependência e necessidades atendidas.
Se você pensou em adicionar um argumento(string, int, enum) ao construtor para que este seja usado na escolha da implementação, esqueça.
Se você pensou na possibilidade da própria classe Aluno solicitar a um Service Locator uma instância que implemente ILocalDeEstudo, então o Service Locator terá um problema a resolver. Saber qual implementação de ILocalDeEstudo você precisa.
Se você pensou que o é responsabilidade de quem criou a instância da classe Aluno, definir explicitamente qual é a dependência necessária pelo aluno, você está no caminho certo, mas o S do SOLID, está pesando contra você.
Então inevitavelmente, usando Service Locators para esse problema fará com que este seja obrigado a compreender o contexto em que está sendo executado, para determinar a implementação, instância ou mesmo versão correta de um objeto para entregá-lo e assim você terá de poluir seu design para conseguir contextualizá-lo.
2) Factories
Factories e suas variações são muito interessantes, mas oferecem os mesmos problemas dos Service Locators.
Então, isso não é IoC
Você pode está pensando em diversas outras soluções para resolver o problema, até usar AOP para contextualizar o Factory ou o Service Locator, mas qualquer abordagem que exija intervenção na classe que necessita da dependência, está gerando em algum nível conflito com princípios do SOLID. Toda a complexidade deste cenário existe porque não há inversão de controle, mas uma divisão de responsabilidades, entre a classe dependente e o Service Locator ou Factory. Para que haja de fato inversão de controle, é necessário delegar a construção das dependências, completamente. Tentar controlar o fluxo de execução é um erro grave.
Sugestão
Injeção de dependência é uma excelente solução em conjunto com IoC. Embora Factories possam fazer IoC, acredito que a gestão do código dos factories pode ser algo extremamente complexo e extenso. Para resolver essa complexidade, sugiro containers. A proximidade do IoC do DI é exatamente essa, a inversão de controle só é real, quando usando injeção de dependência. Mas cuidado, Injeção de Dependência, sem container, é traumático e algo extremamente e trabalhoso.
E lembre-se, somente o nível mais elevado do sua arquitetura deve solicitar instâncias ao seu container / Factory / Service Locator. No caso do ASP.NET, você pode configurar factories, eles podem usar um container para obter todo o grafo de dependências desde a UI até o nível mais baixo. No ASP.NET MVC também é possível, e é até mais simples, com mais exemplos. Windows Services, Consoles não possuem abstração e não há alternativa**, apenas codificar manualmente, mas lembre-se sempre, somente Entry Points devem solicitar instâncias (acho que essa dica soluciona a questão).
Grande abraço a todos.
* Macoratti é um excelente site, mas para quem está aprendendo. Que fique claro meu orgulho por ter feito parte dos primeiros leitores dos artigos relativos a .Net, isso em ~2003.
** Existem frameworks que ajudam a abstrair Windows Services, como TopShelf.
Leia o artigo do Martin Fowler ( http://martinfowler.com/articles/injection.html )
[update][2017-05-04]
Não se convenceu com exemplos acima? Ok, vou apresentar um novo exemplo:
Mail Services (v1)
Vamos supor que eu tenha uma interface IMailService, como abaixo:
public interface IMailService { void SendMail(MailMessage messageToSend); }
Premissas:
Agora vou assumir que meu sistema deve utilizar em 90% das ações um SMTP server próprio da empresa enquanto para 10% das necessidades de envio de email, utilize MailChimp ou algum serviço na nuvem parecido. Olhando para a implementação, assumo que tenho 2 implementações possíveis para envio de email: MailChimpMailService e SMTPMailService. Para dar vida ao exemplo, vamos supor que MailChimpMailService envie um HTTP POST para um endereço de api, enquanto SMTPMailService conecta a um servidor SMTP comum. Pronto, já temos o problema evidenciado.
Explicação:
Quem quer que consuma essa interface, não deve se preocupar com a forma como o email é enviado. Seja com o envio para uma API REST, uma conexão SMTP, o insert em diversas tabelas de um banco, ou mesmo o envio para um Message Broker. Quem consome essa interface, deve saber construir um email, para envio. Nada mais. E esse é o ponto chave que faz com que a abordagem escolhida pela Microsoft e hoje em dia por toda a comunidade .NET esteja errada. Falta algo mais.
Mail Services (v2)
Não está convencido, vou piorar. Pense que o MailChimp criou uma nova forma de enviar email, em vez de uma API REST, você simplesmente conectando ao Servidor SMTP deles. Então vc pode jogar a classe MailChimpMailService no lixo (feliz) e passar a usar apenas a classe SMTPMailService. Isso resolveria a questão? Não, pois ainda: meu sistema deve utilizar em 90% das ações um SMTP server próprio da empresa enquanto para 10% das necessidades de envio de email, utilize MailChimp ou algum serviço na nuvem parecido.
Então, a complexidade continua ali, onde não deveria existir. Por precisar usar mais de uma configuração para a resolução de uma dependência.
Até há soluções para isso, mas a forma como a Microsoft escolheu apresentar Injeção de Dependência, sem IoC, chega a ser criminosa. Mas isso é culpa do projeto Castle Windsor, e esse problema começa em 2003!
Olá Luiz!
Primeiramente, parabéns pelo post… e foi interessante que um post leva à outro e cheguei até este, legal!
Mas, senti falta da solução concreta, seja pro exemplo do aluno, seja praquele do email. Conseguiria passar qual seria a solução ideal nesses casos?
Grande abraço
André, reli o texto e a explicação está nele. É abstrato. Mas falar de código sem código é um “”músculo” que precisa ser estimulado, e uma intenção adjacente do texto é essa.
De qualquer forma eu vou trabalhar em cima desse texto para tentar melhorar a explicação.
Estou estudando o assunto e excelente a sua reflexão, todavia não consegui chegar à solução do problema. Seria de grande ajuda se pudesse publicar em forma de código como o amigo já comentou.