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


230

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?


A nova referência ao artigo de link quebrado (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Respostas:


360

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:

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 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 readonlypalavra - 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:

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 ao usuário criar um Foo padrão escrevendo

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

No entanto, seria muito possível descobrir que é possível fornecer uma dependência personalizada e você pode escrever

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

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 .


21
Embora isso pareça ótimo no papel, na minha experiência, quando você tem muitos componentes internos interagindo de maneiras complexas, você acaba com muitas fábricas para gerenciar, dificultando a manutenção. Além disso, as fábricas terão que gerenciar o estilo de vida de seus componentes criados, uma vez que você instala a biblioteca em um contêiner real, isso entra em conflito com o gerenciamento do estilo de vida do contêiner. Fábricas e fachadas atrapalham os contêineres reais.
Mauricio Scheffer

4
Ainda não encontrei um projeto que faça tudo isso.
Mauricio Scheffer

31
Bem, é assim que desenvolvemos software na Safewhere, para não compartilharmos sua experiência ...
Mark Seemann

19
Penso que as fachadas devem ser codificadas manualmente, porque representam combinações conhecidas (e presumivelmente comuns) de componentes. Um contêiner de DI não deve ser necessário porque tudo pode ser conectado manualmente (pense no DI do pobre homem). Lembre-se de que uma Fachada é apenas uma classe de conveniência opcional para os usuários da sua API. Usuários avançados ainda podem querer ignorar a Fachada e conectar os componentes ao seu gosto. Eles podem querer usar seu próprio DI Contaier para isso, então eu acho que seria cruel forçar um determinado DI Container sobre eles se eles não o usarem. Possível, mas não recomendável.
Mark Seemann

8
Esta pode ser a melhor resposta que eu já vi no SO.
Nick Hodges

40

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:

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 classes sempre que achar que a implementação pode precisar ser alterada;

  2. 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>implementa IList<T>. Se você projetar sua classe para usar IList<T>(ou IEnumerable<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 um IList<T>argumento como construtor, como BindingList<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 IDbConnectioninterfaces 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 INameCreationServiceou IExtenderProviderService. Você nem sabe o que são as classes concretas . Na verdade, o .NET possui seu próprio contêiner de IoC IContainer, que é usado para isso, e a Componentclasse possui um GetServicemétodo que é o localizador de serviço real. Obviamente, nada impede que você use uma ou todas essas interfaces sem o IContainerlocalizador 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 IServiceBehaviorpodem 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.


2
A abstração real para um contêiner é IServiceProvider, não IContainer.
Mauricio Scheffer

2
@Mauricio: Você está certo, é claro, mas tente explicar a alguém que nunca usou o sistema LWC por que IContainernão é realmente o contêiner em um parágrafo. ;)
Aaronaught

Aaron, apenas curioso para saber por que você usa um conjunto privado; em vez de apenas especificar os campos como somente leitura?
Jr3

@Jreeter: Você quer dizer por que eles não são private readonlycampos? 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 um private readonlycampo 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.
precisa saber é o seguinte

Obrigado por esclarecer! Presumi que os campos somente leitura seriam acessíveis pela criança.
Jr3

5

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 newmelhorar 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.


12
Embora eu respeite sua opinião, considero suas generalizadas "todas as outras respostas aqui desatualizadas e devem ser evitadas" extremamente ingênuas. Se aprendemos alguma coisa desde então, é que a injeção de dependência é uma resposta para as limitações de linguagem. Sem evoluir ou abandonar nossas linguagens atuais, parece que precisaremos do DI para nivelar nossas arquiteturas e obter modularidade. A IoC, como conceito geral, é apenas uma conseqüência desses objetivos e definitivamente desejável. IoC containers não são absolutamente necessárias para qualquer IoC ou DI, e sua utilidade depende das composições módulo que fazemos.
tne 24/05

Sua menção de eu ser "extremamente ingênua" é um ad-hominem não bem-vindo. Eu escrevi aplicativos complexos sem DI ou IoC em C #, VB.NET, Java (idiomas que você acha que "precisam" de IoC aparentemente a julgar pela sua descrição), definitivamente não se trata de limitações de idiomas. É sobre limitações conceituais. Convido você a aprender sobre programação funcional em vez de recorrer a ataques ad-hominem e argumentos ruins.
Mauricio Scheffer

18
Mencionei que achei sua afirmação ingênua; isso não diz nada sobre você pessoalmente e tem tudo a ver com minha opinião. Por favor, não se sinta insultado por um estranho aleatório. Implicar que eu sou ignorante de todas as abordagens relevantes de programação funcional também não ajuda; você não sabe disso e estaria errado. Eu sabia que você era conhecedor de lá (olhou rapidamente para o seu blog), e foi por isso que achei irritante que um cara aparentemente com conhecimento dissesse coisas como "não precisamos de IoC". A IoC acontece literalmente em toda parte na programação funcional (..)
25-15

4
e em software de alto nível em geral. De fato, linguagens funcionais (e muitos outros estilos) provam que resolvem a IoC sem DI, acho que nós dois concordamos com isso, e é isso que eu queria dizer. Se você conseguiu sem DI nos idiomas mencionados, como conseguiu? Presumo que não usando 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.)
tne

1
Os contêineres de IoC são dicionários globais mutáveis, eliminando a parametridade como uma ferramenta de raciocínio, a ser evitada. IoC (o conceito) como em "não ligue para nós, ligaremos para você" se aplica tecnicamente toda vez que você passa uma função como um valor. Em vez de DI, modele seu domínio com ADTs e escreva intérpretes (consulte Free).
Mauricio Scheffer

1

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.