Biblioteca “amigável” de Injeção de Dependência (DI)

Eu estou pensando no design de uma biblioteca C #, que terá várias funções diferentes de alto nível. Naturalmente, essas funções de alto nível serão implementadas usando os princípios de projeto de class SOLID , tanto quanto possível. Como tal, provavelmente haverá classs destinadas aos consumidores a usar diretamente em uma base regular, e “classs de suporte” que são dependencies das classs mais comuns de “usuário final”.

A questão é: qual é a melhor maneira de projetar a biblioteca?

  • DI agnóstico – Embora adicionar “suporte” básico para uma ou duas bibliotecas 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 em DI – Se um consumidor da biblioteca não estiver usando DI, a biblioteca ainda deverá ser a mais fácil de usar possível, reduzindo a quantidade de trabalho que um usuário precisa fazer para criar todas essas dependencies “sem importância” apenas para chegar a as classs “reais” que eles querem usar.

Meu pensamento atual é fornecer alguns “módulos de registro DI” para as bibliotecas comuns DI (por exemplo, um registro StructureMap, um módulo Ninject) e um conjunto ou classs de fábrica que são não-DI e contêm o acoplamento para essas poucas fábricas.

Pensamentos?

    Isso é realmente simples de fazer quando você entende que DI é sobre padrões e princípios , não tecnologia.

    Para projetar a API de maneira independente do Contêiner de DI, siga estes princípios gerais:

    Programa para uma interface, não uma implementação

    Esse princípio é, na verdade, uma citação (da memory, no entanto) dos Padrões de Design , mas sempre deve ser seu objective real . DI é apenas um meio para atingir esse fim .

    Aplique o Princípio de Hollywood

    O Princípio de Hollywood em termos de DI diz: Não ligue para o Contêiner DI, ele ligará para você .

    Nunca pergunte diretamente por uma dependência chamando um container de dentro do seu código. Peça implicitamente usando a Injeção de Construtor .

    Use a Injeção de Construtor

    Quando você precisar de uma dependência, peça estaticamente por meio do construtor:

    public class Service : IService { private readonly ISomeDependency dep; public Service(ISomeDependency dep) { if (dep == null) { throw new ArgumentNullException("dep"); } this.dep = dep; } public ISomeDependency Dependency { get { return this.dep; } } } 

    Observe como a class Service garante suas invariantes. Depois que uma instância é criada, a dependência é garantida como disponível devido à combinação da cláusula de proteção e da palavra-chave readonly .

    Use o Abstract Factory se você precisar de um object de vida curta

    As dependencies injetadas com a Injeção de Construtor tendem a ser de longa duração, mas às vezes você precisa de um object de vida curta ou de construir a dependência com base em um valor conhecido apenas em tempo de execução.

    Veja isto para mais informações.

    Compor somente no último momento responsável

    Mantenha os objects desacoplados até o final. Normalmente, você pode esperar e conectar tudo no ponto de input do aplicativo. Isso é chamado de raiz de composição .

    Mais detalhes aqui:

    • Onde devo fazer a injeção com Ninject 2+ (e como faço para organizar meus módulos?)
    • Design – Onde os objects devem ser registrados ao usar o Windsor

    Simplifique usando uma fachada

    Se você achar que a API resultante se torna muito complexa para usuários iniciantes, você sempre pode fornecer algumas classs de Fachada que encapsulam combinações de dependência comuns.

    Para fornecer um Facade flexível com um alto grau de descoberta, você pode considerar fornecer Fluent Builders. Algo assim:

     public class MyFacade { private IMyDependency dep; public MyFacade() { this.dep = new DefaultDependency(); } public MyFacade WithDependency(IMyDependency dependency) { this.dep = dependency; return this; } public Foo CreateFoo() { return new Foo(this.dep); } } 

    Isso permitiria que um usuário criasse um Foo padrão escrevendo

     var foo = new MyFacade().CreateFoo(); 

    Seria, no entanto, muito fácil descobrir que é possível fornecer uma dependência personalizada, e você poderia escrever

     var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo(); 

    Se você imaginar que a class MyFacade encapsula muitas dependencies diferentes, espero que fique claro como ela forneceria os padrões adequados e, ao mesmo tempo, faria a extensibilidade detectável.


    FWIW, muito depois de escrever esta resposta, eu expandi os conceitos aqui contidos e escrevi um post mais longo sobre bibliotecas amigáveis ​​a DI , e um post complementar sobre estruturas amigáveis com DI .

    O termo “injeção de dependência” não tem nada a ver com um contêiner IoC, embora você tenda a vê-los mencionados juntos. Significa simplesmente que, em vez de escrever seu código assim:

     public class Service { public Service() { } public void DoSomething() { SqlConnection connection = new SqlConnection("some connection string"); WindowsIdentity identity = WindowsIdentity.GetCurrent(); // Do something with connection and identity variables } } 

    Você escreve assim:

     public class Service { public Service(IDbConnection connection, IIdentity identity) { this.Connection = connection; this.Identity = identity; } public void DoSomething() { // Do something with Connection and Identity properties } protected IDbConnection Connection { get; private set; } protected IIdentity Identity { get; private set; } } 

    Ou seja, você faz duas coisas quando escreve seu código:

    1. Confie nas interfaces em vez de nas classs sempre que você achar que a implementação precisa ser alterada;

    2. Em vez de criar instâncias dessas interfaces dentro de uma class, passe-as como argumentos de construtor (alternativamente, elas podem ser atribuídas a propriedades públicas; a primeira é a injeção de construtor , a segunda é a injeção de propriedade ).

    Nada disso pressupõe a existência de qualquer biblioteca DI, e isso não torna o código mais difícil de escrever sem um.

    Se você está procurando um exemplo disso, não procure mais do que o próprio .NET Framework:

    • List implementa IList . Se você projetar sua class para usar IList (ou IEnumerable ), você pode tirar proveito de conceitos como carregamento lento, como Linq para SQL, Linq para Entidades e NHibernate, tudo por trás das cenas, geralmente por meio da propriedade. injeção. Algumas classs de estrutura aceitam realmente um IList como um argumento de construtor, como BindingList , que é usado para vários resources de vinculação de dados.

    • Linq para SQL e EF são construídos inteiramente em torno do IDbConnection e interfaces relacionadas, que podem ser passadas através dos construtores públicos. Você não precisa usá-los, no entanto; os construtores padrão funcionam bem com uma cadeia de conexão localizada em um arquivo de configuração em algum lugar.

    • Se você já trabalhou em componentes WinForms, você lida com “serviços”, como INameCreationService ou IExtenderProviderService . Você nem sabe o que são as classs concretas. O .NET, na verdade, tem seu próprio contêiner IoC, IContainer , que é usado para isso, e a class Component possui um método GetService , que é o localizador de serviço real. Naturalmente, nada impede que você use qualquer uma ou todas essas interfaces sem o IContainer ou aquele localizador específico. Os serviços em si são apenas vagamente acoplados ao contêiner.

    • Contratos no WCF são construídos inteiramente em torno de interfaces. A class de serviço concreta real é geralmente referenciada pelo nome em um arquivo de configuração, que é essencialmente DI. Muitas pessoas não percebem isso, mas é perfeitamente possível trocar esse sistema de configuração por outro contêiner IoC. Talvez mais interessante, os comportamentos de serviço são todas as instâncias de IServiceBehavior que podem ser adicionadas posteriormente. Novamente, você pode facilmente conectar isso a um contêiner IoC e selecioná-lo para os comportamentos relevantes, mas o recurso é completamente utilizável sem um.

    E assim por diante. Você vai encontrar DI em todo o lugar no .net, é só que normalmente é feito tão perfeitamente que você nem sequer pensa nisso como DI.

    Se você deseja projetar sua biblioteca ativada por DI para obter o máximo de usabilidade, a melhor sugestão é provavelmente fornecer sua própria implementação IoC padrão usando um contêiner leve. IContainer é uma ótima opção para isso, porque é uma parte do próprio .NET Framework.

    EDIT 2015 : o tempo passou, eu percebo agora que tudo isso foi um grande erro. Contêineres IoC são terríveis e DI é uma maneira muito ruim de lidar com os efeitos colaterais. Efetivamente, todas as respostas aqui (e a questão em si) devem ser evitadas. Simplesmente fique atento aos efeitos colaterais, separe-os do código puro e tudo mais se encheckbox ou é irrelevante e desnecessário.

    A resposta original segue:


    Eu tive que enfrentar essa mesma decisão enquanto desenvolvia o SolrNet . Comecei com o objective de ser amigo do DI e agnóstico de contêineres, 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 integrado , além de fornecer uma instalação de Windsor e um módulo Ninject . Integrar a biblioteca a outros contêineres é apenas uma questão de conectar corretamente os componentes, para que eu possa integrá-los facilmente com o Autofac, Unity, StructureMap, etc.

    A desvantagem disso é que perdi a capacidade de apenas atualizar o serviço. Eu também tive uma dependência em CommonServiceLocator, que poderia ter evitado (eu poderia refatorar isso no futuro) para tornar o contêiner embutido mais fácil de implementar.

    Mais detalhes nesta postagem do blog .

    MassTransit parece confiar em algo semelhante. Tem uma interface IObjectBuilder que é realmente IServiceLocator CommonServiceLocator com mais alguns methods, então ela implementa isto para cada container, ie NinjectObjectBuilder e um módulo / facilidade regular, ie MassTransitModule . Em seguida, ele depende do IObjectBuilder para instanciar o que precisa. Esta é uma abordagem válida, é claro, mas pessoalmente eu não gosto muito, já que ela está passando muito pelo contêiner, usando-o como um localizador de serviço.

    O MonoRail também implementa seu próprio contêiner , que implementa o bom e velho 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 de provedor de serviços integrado . A instalação de Windsor aponta esse localizador de provedores de serviços para Windsor, tornando-o o provedor de serviços selecionado.

    Bottom line: não há solução perfeita. Como acontece com qualquer decisão de projeto, essa questão exige um equilíbrio entre flexibilidade, facilidade de manutenção e conveniência.

    O que eu faria é projetar minha biblioteca em um modo agnóstico do contêiner DI para limitar a dependência do contêiner tanto quanto possível. Isso permite trocar o contêiner 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 de sua interface. Desta forma, eles ainda podem usar a funcionalidade DI que você expôs e eles estão livres para usar qualquer outro framework para seus próprios propósitos.

    Permitir que os usuários da biblioteca conectem sua própria estrutura de 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 DI direto.