Analisarei seus pontos numericamente, mas primeiro, há algo que você deve ter muito cuidado: não confunda como um consumidor usa uma biblioteca com a maneira como ela é implementada . Bons exemplos disso são o Entity Framework (que você mesmo cita como uma boa biblioteca) e o MVC do ASP.NET. Ambos fazem muita coisa escondida com, por exemplo, reflexão, o que absolutamente não seria considerado um bom design se você o espalhar pelo código do dia-a-dia.
Essas bibliotecas não são absolutamente "transparentes" na maneira como funcionam ou no que estão fazendo nos bastidores. Mas isso não é um prejuízo, porque eles apóiam bons princípios de programação em seus consumidores . Portanto, sempre que você falar sobre uma biblioteca como essa, lembre-se de que, como consumidor de uma biblioteca, não é seu trabalho se preocupar com sua implementação ou manutenção. Você deve se preocupar apenas sobre como isso ajuda ou dificulta o código que você escreve e que usa a biblioteca. Não confunda esses conceitos!
Então, para passar por ponto:
Imediatamente chegamos ao que só posso assumir é um exemplo do exposto acima. Você diz que os contêineres IOC configuram a maioria das dependências como estáticas. Bem, talvez alguns detalhes de implementação de como eles funcionem incluam armazenamento estático (embora, considerando que eles tendem a ter uma instância de um objeto como o Ninject IKernel
como seu armazenamento principal, duvido disso). Mas essa não é sua preocupação!
Na verdade, um contêiner de IoC é tão explícito no escopo quanto a injeção de dependência do pobre homem. (Vou continuar comparando os contêineres de IoC com o DI do pobre, porque compará-los com nenhum DI seria injusto e confuso. E, para ficar claro, não estou usando o "DI do pobre homem" como pejorativo.)
No DI do pobre homem, você instancia uma dependência manualmente e a injeta na classe que precisa. No ponto em que você constrói a dependência, você escolhe o que fazer com ela - armazena-a em uma variável com escopo local, uma variável de classe, uma variável estática, não a armazena de maneira alguma. Você pode passar a mesma instância para várias classes ou criar uma nova para cada classe. Tanto faz. O ponto é que, para ver o que está acontecendo, você olha para o ponto - provavelmente próximo à raiz do aplicativo - onde a dependência é criada.
Agora, que tal um contêiner de IoC? Bem, você faz exatamente o mesmo! Mais uma vez, indo pela terminologia Ninject, você olhar para onde a ligação está configurado, e descobrir se ele diz algo como InTransientScope
, InSingletonScope
ou o que quer. Se algo é potencialmente mais claro, porque você tem um código declarando seu escopo, em vez de precisar procurar um método para rastrear o que acontece com algum objeto (um objeto pode ter um escopo definido para um bloco, mas ele é usado várias vezes nesse bloco ou apenas uma vez, por exemplo). Portanto, talvez você se sinta repelido pela idéia de ter que usar um recurso no contêiner de IoC em vez de um recurso de idioma bruto para determinar o escopo, mas contanto que você confie na sua biblioteca de IoC - como deveria! - não há nenhuma desvantagem real .
Ainda não sei o que você está falando aqui. Os contêineres de IoC veem as propriedades privadas como parte de sua implementação interna? Não sei por que eles iriam, mas, novamente, se o fizerem, não é sua preocupação como a biblioteca que você está usando é implementada.
Ou, talvez, eles exponham uma capacidade como injetar em incubadoras privadas? Honestamente, nunca encontrei isso e duvido que isso seja realmente um recurso comum. Mas, mesmo que esteja lá, é um caso simples de uma ferramenta que pode ser mal utilizada. Lembre-se, mesmo sem um contêiner de IoC, são apenas algumas linhas do código Reflection para acessar e modificar uma propriedade privada. É algo que você quase nunca deve fazer, mas isso não significa que o .NET seja ruim por expor a capacidade. Se alguém de maneira tão óbvia e descontroladamente usa uma ferramenta, a culpa é da pessoa, não da ferramenta.
O ponto final aqui é semelhante a 2. Na grande maioria das vezes, os contêineres de IoC usam o construtor! A injeção de incubadora é oferecida para circunstâncias muito específicas em que, por razões particulares, a injeção de construtor não pode ser usada. Qualquer um que use a injeção de setter o tempo todo para encobrir quantas dependências estão sendo transmitidas está absentamente massivo com a ferramenta. Isso não é culpa da ferramenta, é deles.
Agora, se esse foi um erro realmente fácil de cometer inocentemente, e um que os contêineres de IoC incentivam, tudo bem, talvez você tenha razão. Seria como tornar todos os membros da minha classe públicos e culpar outras pessoas quando elas modificam coisas que não deveriam, certo? Mas qualquer pessoa que use a injeção de setter para encobrir violações do SRP está deliberadamente ignorando ou completamente ignorando os princípios básicos do projeto. Não é razoável colocar a culpa por isso no contêiner de IoC.
Isso é especialmente verdade porque também é algo que você pode fazer com a mesma facilidade com o DI do pobre homem:
var myObject
= new MyTerriblyLargeObject { DependencyA = new Thing(), DependencyB = new Widget(), Dependency C = new Repository(), ... };
Realmente, essa preocupação parece completamente ortogonal à utilização ou não de um contêiner de IoC.
Mudar a forma como suas aulas trabalham juntas não é uma violação do OCP. Se fosse, toda inversão de dependência encorajaria uma violação do OCP. Se fosse esse o caso, ambos não estariam no mesmo acrônimo SOLID!
Além disso, nem os itens a) nem b) chegam perto de ter algo a ver com o OCP. Eu nem sei como responder isso em relação ao OCP.
A única coisa que posso adivinhar é que você acha que o OCP tem algo a ver com o comportamento não ser alterado no tempo de execução, ou sobre de onde no código o ciclo de vida das dependências é controlado. Não é. OCP não precisa modificar o código existente quando os requisitos são adicionados ou alterados. Trata-se de escrever código, não de como o código que você já escreveu é colado (embora, é claro, o acoplamento flexível seja uma parte importante do alcance do OCP).
E uma última coisa que você diz:
Mas não posso confiar nessa mesma ferramenta de terceiros que está alterando meu código compilado para executar soluções alternativas para coisas que eu não seria capaz de fazer manualmente.
Sim você pode . Não há absolutamente nenhuma razão para você pensar que essas ferramentas - contadas com um grande número de projetos - são mais problemáticas ou propensas a comportamentos inesperados do que qualquer outra biblioteca de terceiros.
Termo aditivo
Acabei de notar que os parágrafos de introdução também podem usar alguns endereços. Você diz ironicamente que os contêineres de IoC "não estão empregando alguma técnica secreta da qual nunca ouvimos falar" para evitar códigos confusos e propensos a duplicação para criar um gráfico de dependência. E você está certo, o que eles estão fazendo é realmente abordar essas coisas com as mesmas técnicas básicas que nós, programadores, sempre fazemos.
Deixe-me falar sobre um cenário hipotético. Você, como programador, monta um aplicativo grande e, no ponto de entrada, onde está construindo seu gráfico de objetos, percebe que possui um código bastante confuso. Existem algumas classes que são usadas repetidamente, e toda vez que você cria uma daquelas, precisa criar toda a cadeia de dependências sob elas novamente. Além disso, você descobre que não possui nenhuma maneira expressiva de declarar ou controlar o ciclo de vida das dependências, exceto com o código personalizado para cada uma. Seu código não está estruturado e está repleto de repetições. Essa é a confusão que você fala no parágrafo de introdução.
Então, primeiro, você começa a refatorar um pouco - onde algum código repetido é estruturado o suficiente para extraí-lo para métodos auxiliares, e assim por diante. Mas então você começa a pensar: esse é um problema que eu poderia resolver em um sentido geral , que não é específico para esse projeto em particular, mas poderia ajudá-lo em todos os seus futuros projetos?
Então, sente-se, pense sobre isso e decida que deve haver uma classe que possa resolver dependências. E você esboça quais métodos públicos seriam necessários:
void Bind(Type interfaceType, Type concreteType, bool singleton);
T Resolve<T>();
Bind
diz "onde você vê um argumento construtor do tipo interfaceType
, passe em uma instância de concreteType
". O singleton
parâmetro adicional diz se deve usar a mesma instância de concreteType
cada vez ou sempre criar uma nova.
Resolve
simplesmente tentará construir T
com qualquer construtor que encontrar cujos argumentos são todos os tipos que foram vinculados anteriormente. Ele também pode se chamar recursivamente para resolver as dependências até o fim. Se não conseguir resolver uma instância, porque nem tudo foi vinculado, gera uma exceção.
Você pode tentar implementar isso sozinho e verá que precisa de um pouco de reflexão e algum armazenamento em cache para as ligações, onde singleton
é verdade, mas certamente nada drástico ou horrível. E assim que terminar , voilá , você terá o núcleo do seu próprio contêiner de IoC! É realmente tão assustador? A única diferença real entre isso e Ninject ou StructureMap ou Castle Windsor ou o que você preferir é que eles têm muito mais funcionalidade para cobrir os (muitos!) Casos de uso em que essa versão básica não seria suficiente. Mas, no fundo, o que você tem é a essência de um contêiner de IoC.
IOC
eExplicitness of code
é exatamente com isso que tenho problemas. A DI manual pode facilmente se tornar uma tarefa árdua, mas pelo menos coloca o fluxo explícito do programa em um só lugar - "Você consegue o que vê, vê o que recebe". Embora as ligações e declarações dos contêineres do IOC possam se tornar facilmente um programa paralelo oculto por conta própria.