Estou pensando no design de uma biblioteca C #, que terá várias funções diferentes de alto nível. Obviamente, essas funções de alto nível serão implementadas usando os princípios de design da classe SOLID , tanto quanto possível. Como tal, provavelmente haverá classes destinadas aos consumidores para usar diretamente regularmente e "classes de suporte" que são dependências dessas classes mais comuns de "usuário final".
A questão é: qual é a melhor maneira de projetar a biblioteca assim:
- Agnóstico de DI - Embora a adição de "suporte" básico a uma ou duas das bibliotecas de DI comuns (StructureMap, Ninject, etc.) pareça razoável, quero que os consumidores possam usar a biblioteca com qualquer estrutura de DI.
- Não utilizável DI - Se um consumidor da biblioteca não estiver usando DI, a biblioteca ainda deve ser o mais fácil de usar possível, reduzindo a quantidade de trabalho que um usuário precisa fazer para criar todas essas dependências "sem importância" apenas para obter as classes "reais" que eles querem usar.
Meu pensamento atual é fornecer alguns "módulos de registro DI" para as bibliotecas DI comuns (por exemplo, um registro do StructureMap, um módulo Ninject) e um conjunto ou classes Factory que não sejam DI e contenham o acoplamento para essas poucas fábricas.
Pensamentos?
Respostas:
Isso é realmente simples de fazer quando você entende que o DI é sobre padrões e princípios , não sobre tecnologia.
Para projetar a API de maneira independente de container DI, siga estes princípios gerais:
Programe para uma interface, não uma implementação
Esse princípio é, na verdade, uma citação (da memória) dos Design Patterns , mas sempre deve ser seu objetivo real . O DI é apenas um meio de atingir esse fim .
Aplique o Princípio de Hollywood
O Princípio de Hollywood em termos de DI diz: Não ligue para o DI Container, ele ligará para você .
Nunca solicite diretamente uma dependência chamando um contêiner de dentro do seu código. Peça implicitamente usando Injeção de Construtor .
Usar injeção de construtor
Quando você precisar de uma dependência, peça-a estaticamente através do construtor:
Observe como a classe Service garante seus invariantes. Depois que uma instância é criada, a dependência é garantida para estar disponível devido à combinação da Cláusula Guard e da
readonly
palavra - chave.Use Abstract Factory se precisar de um objeto de vida curta
As dependências injetadas com a Injeção de construtor tendem a ter vida útil prolongada, mas às vezes você precisa de um objeto de vida útil curta ou construir a dependência com base em um valor conhecido apenas no tempo de execução.
Veja isso para mais informações.
Componha apenas no último momento responsável
Mantenha os objetos dissociados até o fim. Normalmente, você pode esperar e conectar tudo no ponto de entrada do aplicativo. Isso é chamado de raiz da composição .
Mais detalhes aqui:
Simplifique usando uma fachada
Se você acha que a API resultante se torna muito complexa para usuários iniciantes, sempre pode fornecer algumas classes de Fachada que encapsulam combinações de dependências comuns.
Para fornecer uma Fachada flexível com um alto grau de descoberta, considere fornecer o Fluent Builders. Algo assim:
Isso permitiria ao usuário criar um Foo padrão escrevendo
No entanto, seria muito possível descobrir que é possível fornecer uma dependência personalizada e você pode escrever
Se você imagina que a classe MyFacade encapsula muitas dependências diferentes, espero que fique claro como ela forneceria os padrões adequados e, ao mesmo tempo, tornaria a extensibilidade detectável.
FWIW, muito tempo depois de escrever esta resposta, ampliei os conceitos aqui contidos e escrevi uma postagem mais longa no blog sobre DI-Friendly Libraries , e uma publicação complementar sobre DI-Friendly Frameworks .
fonte
O termo "injeção de dependência" não tem nada a ver com um contêiner de IoC, mesmo que você os veja mencionados juntos. Simplesmente significa que, em vez de escrever seu código assim:
Você escreve assim:
Ou seja, você faz duas coisas quando escreve seu código:
Confie nas interfaces em vez de nas classes sempre que achar que a implementação pode precisar ser alterada;
Em vez de criar instâncias dessas interfaces dentro de uma classe, transmita-as como argumentos de construtor (como alternativa, elas podem ser atribuídas a propriedades públicas; a primeira é injeção de construtor e a segunda é injeção de propriedade ).
Nada disso pressupõe a existência de qualquer biblioteca de DI e realmente não torna o código mais difícil de escrever sem uma.
Se você está procurando um exemplo disso, não procure mais, o próprio .NET Framework:
List<T>
implementaIList<T>
. Se você projetar sua classe para usarIList<T>
(ouIEnumerable<T>
), poderá tirar proveito de conceitos como carregamento lento, como Linq to SQL, Linq to Entities e NHibernate, todos os bastidores, geralmente por injeção de propriedade. Algumas classes de estrutura realmente aceitam umIList<T>
argumento como construtor, comoBindingList<T>
, que é usado para vários recursos de ligação de dados.O Linq to SQL e EF são construídos inteiramente em torno das
IDbConnection
interfaces relacionadas, que podem ser passadas pelos construtores públicos. Você não precisa usá-los; os construtores padrão funcionam bem com uma cadeia de conexão em um arquivo de configuração em algum lugar.Se você trabalha nos componentes WinForms, lida com "serviços", como
INameCreationService
ouIExtenderProviderService
. Você nem sabe o que são as classes concretas . Na verdade, o .NET possui seu próprio contêiner de IoCIContainer
, que é usado para isso, e aComponent
classe possui umGetService
método que é o localizador de serviço real. Obviamente, nada impede que você use uma ou todas essas interfaces sem oIContainer
localizador específico. Os serviços em si são apenas fracamente acoplados ao contêiner.Os contratos no WCF são criados inteiramente em torno de interfaces. A classe de serviço concreta real é geralmente referenciada por nome em um arquivo de configuração, que é essencialmente DI. Muitas pessoas não percebem isso, mas é totalmente possível trocar esse sistema de configuração por outro contêiner de IoC. Talvez o mais interessante é que todos os comportamentos de serviço
IServiceBehavior
podem ser adicionados posteriormente. Novamente, você pode conectar isso facilmente a um contêiner de IoC e escolher os comportamentos relevantes, mas o recurso é completamente utilizável sem um.E assim por diante. Você encontrará DI em todo lugar no .NET, mas normalmente é feito de maneira tão fácil que você nem pensa nisso como DI.
Se você deseja projetar sua biblioteca habilitada para DI para máxima usabilidade, a melhor sugestão é provavelmente fornecer sua própria implementação de IoC padrão usando um contêiner leve.
IContainer
é uma ótima opção para isso, porque faz parte do próprio .NET Framework.fonte
IContainer
não é realmente o contêiner em um parágrafo. ;)private readonly
campos? Isso é ótimo se eles são usados apenas pela classe declarante, mas o OP especificou que esse era o código no nível da estrutura / biblioteca, o que implica subclassificação. Nesses casos, você normalmente deseja dependências importantes expostas às subclasses. Eu poderia ter escrito umprivate readonly
campo e uma propriedade com getters / setters explícitos, mas ... desperdiçou espaço em um exemplo e mais código para manter na prática, sem nenhum benefício real.EDIT 2015 : o tempo passou, percebo agora que tudo isso foi um grande erro. Os recipientes de IoC são terríveis e o DI é uma maneira muito ruim de lidar com os efeitos colaterais. Efetivamente, todas as respostas aqui (e a própria pergunta) devem ser evitadas. Basta estar ciente dos efeitos colaterais, separá-los do código puro e tudo o mais se encaixa ou é complexidade irrelevante e desnecessária.
A resposta original segue:
Eu tive que enfrentar essa mesma decisão enquanto desenvolvia o SolrNet . Comecei com o objetivo de ser amigável a DI e independente de contêiner, mas, à medida que adicionava mais e mais componentes internos, as fábricas internas rapidamente se tornaram incontroláveis e a biblioteca resultante era inflexível.
Acabei escrevendo meu próprio contêiner IoC incorporado muito simples , além de fornecer uma instalação de Windsor e um módulo Ninject . A integração da biblioteca com outros contêineres é apenas uma questão de conectar adequadamente os componentes, para que eu possa integrá-la facilmente ao Autofac, Unity, StructureMap, qualquer que seja.
A desvantagem disso é que perdi a capacidade de
new
melhorar o serviço. Também dependi do CommonServiceLocator que eu poderia ter evitado (talvez refatorasse no futuro) para facilitar a implementação do contêiner incorporado.Mais detalhes nesta postagem do blog .
O MassTransit parece confiar em algo semelhante. Ele possui uma interface IObjectBuilder que é realmente o IServiceLocator do CommonServiceLocator com mais alguns métodos; depois, implementa isso para cada contêiner, ou seja, NinjectObjectBuilder e um módulo / instalação regular, ou seja, MassTransitModule . Em seguida, conta com o IObjectBuilder para instanciar o que precisa. Essa é uma abordagem válida, é claro, mas pessoalmente eu não gosto muito, pois na verdade ela está passando muito pelo contêiner, usando-o como localizador de serviço.
O MonoRail também implementa seu próprio contêiner , que implementa o bom e antigo IServiceProvider . Esse contêiner é usado em toda essa estrutura por meio de uma interface que expõe serviços conhecidos . Para obter o contêiner de concreto, ele possui um localizador interno de provedores de serviços . As instalações de Windsor apontam esse localizador de provedor de serviços para Windsor, tornando-o o provedor de serviços selecionado.
Conclusão: não há solução perfeita. Como em qualquer decisão de projeto, esse problema exige um equilíbrio entre flexibilidade, manutenção e conveniência.
fonte
ServiceLocator
. Se você aplicar técnicas funcionais, terá o equivalente a encerramentos, que são classes injetadas em dependência (em que dependências são as variáveis fechadas). De que outra forma? (Eu estou realmente curiosa, porque eu não gosto DI também.)O que eu faria é projetar minha biblioteca de uma maneira independente de contêiner de DI para limitar a dependência do contêiner o máximo possível. Isso permite trocar o contêiner de DI por outro, se necessário.
Em seguida, exponha a camada acima da lógica DI aos usuários da biblioteca para que eles possam usar qualquer estrutura que você escolher através da sua interface. Dessa forma, eles ainda podem usar a funcionalidade de DI que você expôs e podem usar qualquer outra estrutura para seus próprios fins.
Permitir que os usuários da biblioteca conectem sua própria estrutura DI parece um pouco errado para mim, pois aumenta drasticamente a quantidade de manutenção. Isso também se torna mais um ambiente de plug-in do que o DI direto.
fonte