Este artigo foi traduzido por máquina.

Explorando padrões e recomendações da Microsoft

Injeção de dependência em bibliotecas

Chris Tavares

Refatoração Microsoft Enterprise Library

Injeção de dependência (DI) é um padrão ganhando traction na comunidade de desenvolvedores .NET sobre últimos anos. Blogueiros proeminentes tem sido falando sobre os benefícios do DI por bastante tempo. Vários artigos sobre o tópico foram publicados no do MSDN Magazine. .NET 4.0 irá ser entrega alguma funcionalidade semelhante DI com planos de crescer em um sistema completo DI no futuro.

Lendo artigos em DI e postagens de blog, percebi uma pequena mas significativa de ajuste na cobertura do tópico. Os gravadores falam sobre usando DI no contexto de um aplicativo inteiro. Mas e se você deseja gravar uma biblioteca ou estrutura que usa DI? Como essa alteração no foco afetam o uso do padrão? Isso era algo que nós (a equipe de padrões e práticas Enterprise Library) frente visitas alguns meses atrás, como podemos estava trabalhando a arquitetura da Enterprise Library 5.0.

Experiência

Microsoft Enterprise Library (Entlib) é uma versão muito conhecida a partir do grupo Microsoft patterns & practices. Com mais de 2 milhões downloads a data, ele é usado em praticamente todos nicho imaginável do governo para fabricantes de equipamento médico e restaurantes e instituições financeiras. Entlib é, como o nome implica, uma biblioteca que ajuda um desenvolvedor endereço preocupações comuns compartilhadas por muitos desenvolvedores de empresa. Se não estiver familiarizado com Entlib, dê uma olhada no nosso site no p & p desenvolvimento central para obter mais informações.

Entlib altamente é orientada por configuração. Uma grande parte do seu código é dedicada para ler a configuração e montando gráficos de objeto com base em que a configuração. Objetos entlib podem obter muito complexos. A maioria dos blocos contêm muita funcionalidade opcional. Além disso, também há muita infra estrutura subjacente para oferecer suporte a coisas como instrumentação, também precisa obter cabeada backup. Porque não queremos tornar nossos usuários manualmente criar provedores de instrumentação, ler a configuração e assim por diante, basta usar Entlib, criação de objeto é encapsulada por trás de objetos de fábrica e façades estático.

O núcleo da versão 2 através de versão 4 Entlib é uma pequena estrutura chamada ObjectBuilder. ObjectBuilder é descrita por seus autores como “ uma estrutura para a criação de contêineres de injeção de dependência. ” Enterprise Library é apenas um p & p projetos que usa ObjectBuilder; outros incluem Composite UI Application Block, Smart Client Software Factory e Web Client Software Factory. Em particular entlib levou parte “ framework ” da descrição do coração e criado um grande conjunto de personalizações para ObjectBuilder. Essas personalizações fornecidas a funcionalidade necessária para ler configuração Entlib e montar gráficos de objeto. Eles foram também necessário, em muitos casos, para melhorar o desempenho estoque ObjectBuilder implementação.

A desvantagem era levou bastante um tempo para entender tanto ObjectBuilder próprio (um design extremamente abstrata mais ausência completa de documentação deu ObjectBuilder uma reputação deserved para complexidade) e o Entlib personalizações. Como resultado, as pessoas que desejavam gravar blocos personalizados enganchado em estratégia de criação de objeto do Entlib freqüentemente foram stymied pela curva de aprendizado maciça que eles necessário subir apenas para começar.

E, para adicionar uma complicação adicional em Entlib 4.0 lançamos contêiner de injeção de dependência Unity. Com muitas vantagens de DI, queríamos Certifique-se de que aqueles clientes que foram, para qualquer motivo, não é possível usar um dos recipientes fino Abrir origem muitos tinham uma boa opção para DI da Microsoft. E, claro, queríamos tornar fácil obter objetos Entlib trabalhando quando usando Unity bem. Entlib 4.0, a integração Unity terminou sendo um sistema de criação do objeto paralela próximo a infra-estrutura de ObjectBuilder existente. Agora bloco gravadores tinham que saber não apenas ObjectBuilder e extensões Entlib, mas Unity internos bem mais algumas extensões Entlib não existe. Não é uma etapa na direção certa.

Movendo para simplicidade

Começamos trabalho em Entlib 5.0 em abril de 2009. Um tema principal para esta versão foi “ simplicidade para o win ”. Isso incluiu simplicidade não apenas para o usuário final (desenvolvedores chamando Entlib) mas no código de Entlib próprio. Aperfeiçoamentos aqui seriam tornar mais fácil para nós manter Entlib indo adiante e tornar mais fácil de entender, personalizar e estender para nossos clientes.

Uma das principais áreas que sabíamos que o trabalho necessário foi o pipeline de criação do objeto — ou deve digo pipelines? Manter dois conjuntos diferentes mas paralelos de código para a mesma funcionalidade é uma receita para desastres. Algo tinha que ser feito.

Definimos esses objetivos para a refatoração:

  • Código de cliente existente não deve ter que alterar apenas devido as alterações de arquitetura. Exigir uma recompilação é VÁLIDO, mas exigir alterações de código de origem não é (do curso, o cliente API pode alterar por outros motivos ). Internal ou APIs de extensibilidade são feira jogo.
  • Remova os pipelines de criação do objeto redundantes. Podemos deve ter apenas uma maneira de criar objetos não dois (ou mais).
  • Os clientes que não se preocupa DI não devem ser afetados por Entlib usando internamente.
  • Os clientes que fazer preocupa DI podem escolher o contêiner que deseja usar e obter seus objetos e objetos de Entlib fora do.

Essas metas tinham algumas implicações, separadamente e combinação. O objetivo de “ um objeto criação pipeline ” foi em sua superfície bastante direto. Decidimos remover completamente o sistema baseado no ObjectBuilder e vá com um contêiner DI como nosso mecanismo de criação do objeto internamente. Mas, em seguida, obtemos “ código de cliente existente não deve alterar ”. A API Entlib clássico é um conjunto de façades estático e fábricas. Por exemplo, uma mensagem usando o bloco de registro de log é feito como este:

Logger.Write("My Message");

Nos bastidores, fachada Logger usa uma instância de um objeto LogWriter para fazer o trabalho real. Assim como fachada Logger obter o LogWriter? LogWriter é uma classe bastante complexa com muitas dependências, portanto, não é possível apenas novas ele backup e esperar que a configuração ser com fio apropriadamente. Podemos veio à conclusão que precisávamos de uma instância global contêiner para todas as outras estáticas classes na API e Logger. Podemos poderia manter apenas um recipiente Unity global, mas em seguida, executamos em “ clientes obter escolher recipiente desejarem. ”

Queremos que a combinação de Unity e Entlib ser uma experiência de primeira classe. Também queremos fornecer essa experiência de primeira classe com outros recipientes também. Enquanto os recursos gerais do DI recipientes são comuns a tabuleiro, a maneira de acessar esses recursos variam muito. Na verdade, muitos desenvolvedores de recipientes considerar sua configuração APIs para ser seu principal vantagem competitiva. Então como podemos mapear nossa configuração Entlib até recipiente totalmente diferente APIs?

O clássico Computer Science Solution

É um truism ciência de computador: Cada problema em ciência pode ser resolvido adicionando uma camada de indireção. E é exatamente da maneira podemos solucionados nosso problema de independência do recipiente. A camada de indireção é o que chamamos um Configurador recipiente. Em seu núcleo, a função do Configurador é ler configuração do Entlib e configurar um contêiner para coincidir.

Infelizmente, próprio ler a configuração não suficiente. Formato de arquivo de configuração do entlib é muito usuário final focalizado. Configurar categorias de log, diretivas de exceção e cache fazendo armazenamentos. Ele não dizem nada sobre quais objetos são realmente necessários para implementar a funcionalidade, ou o que valores para passar para construtores ou quais propriedades para definir. DI recipiente configuração, por outro lado, é tudo sobre “ mapear esta interface para este tipo ” “ chamar esse construtor ” e “ definir essa propriedade ”. Precisávamos outra camada de indireção mapeado a configuração de um bloco para objetos reais necessários para implementar desse bloco. A alternativa era para que cada Configurador (seria necessário um Configurador por contêiner) saber sobre detalhes de cada um dos blocos. Untenable imediatamente; cada alteração para o código de um bloco seria ondulação através de configurators. E o que acontece quando alguém grava um bloco personalizado?

Acabamos com um conjunto de objetos estiver chamando TypeRegistrations. Várias seções de configuração são responsáveis por gerar um do modelo de registro de tipo; uma seqüência de objetos de TypeRegistration. Do Figura 1 mostra a interface para TypeRegistration.

Figura 1 do TypeRegistration Class

public class TypeRegistration
    {

        public TypeRegistration(LambdaExpression expression);
        public TypeRegistration(LambdaExpression expression, Type serviceType);

        public Type ImplementationType { get; }
        public NewExpression NewExpressionBody { get; }
        public Type ServiceType { get; private set; }
        public string Name { get; set; }

        public static string DefaultName(Type serviceType);
        public static string DefaultName<TServiceType>();

        public LambdaExpression LambdaExpression { get; private set; }

         public bool IsDefault { get; set; }

         public TypeRegistrationLifetime Lifetime { get; set; }

         public IEnumerable<ParameterValue> ConstructorParameters { get; }

         public IEnumerable<InjectedProperty> InjectedProperties { get; }
    }

Há muito aqui, mas a estrutura básica é muito simples. Essa classe descreve a configuração necessária para um único tipo. O Tipo_de_serviço é a interface do usuário irá solicitar de recipiente, enquanto o ImplementationType é o tipo que realmente implementa a interface. É o nome que o serviço deve ser registrado em. Tempo de vida determina singleton (retornar sempre a mesma instância) ou transitório (criar uma nova instância sempre) comportamento de criação. E assim por diante. Optamos por usar uma expressão lambda para criar o objeto TypeRegistration porque ela torna muito fácil especificar essas informações em um único, CD especiais. Aqui está um exemplo de criação de um registro de tipo de bloco Data Access:

yield return new TypeRegistration<Database>(
       () => new SqlDatabase(
           ConnectionString,
           Container.Resolved<IDataInstrumentationProvider>(Name)))
       {
           Name = Name,
           Lifetime = TypeRegistrationLifetime.Transient
       };

Esse registro de tipo está dizendo “ ao solicitar um banco de dados chamado nome, retornar um novo objeto SqlDatabase, construído com ConnectionString e um IDataInstrumentationProvider. ” A coisa interessante sobre usando o lambda aqui é que, ao escrever os blocos, podemos poderia criar essas expressões como se estivéssemos renovação até os objetos diretamente. O compilador irá digite verificar a expressão, portanto, não podemos acidentalmente tentar chamar um construtor que não existe. Para definir propriedades, basta usar a sintaxe do inicializador de objeto translation from VPE for Csharp dentro de expressão lambda. A classe TypeRegistration cuida dos detalhes da separação através o lambda e extraindo a assinatura de construtor, parâmetros, tipos e assim por diante, para que o autor do Configurador não tenha que.

Um truque útil que usamos é chamada para “ Container.Resolved ”. Método realmente não faz nada; na verdade, sua implementação é simplesmente isso:

public static T Resolved<T>(string name)
        {
            return default(T);
        }

Por que há ele?Lembre-se, essa expressão lambda é nunca realmente executado.Em vez disso, percorremos a estrutura da expressão em tempo de execução para retirar as informações de registro.Este método é simplesmente um marcador conhecido.Quando encontramos uma chamada para Container.Resolved como um parâmetro, podemos interpretar que como “ resolver este parâmetro através do recipiente ”. Encontramos essa técnica de método de marcador para ser útil em uma variedade de lugares quando fazendo trabalho avançado com árvores de expressão.

No final, o fluxo de arquivo de configuração para configurado recipiente aparência do Figura 2.


Figura 2 do Container Configuration

Uma decisão de design que fizemos é importante explicar aqui.TypeRegistration sistema não é e nunca será, finalidade geral, configure-tudo abstração para qualquer recipiente DI.Ele foi projetado especificamente para as necessidades do projeto Enterprise Library.A equipe patterns & practices não está posicionando isso como diretrizes baseadas em código.Enquanto a idéia básica (extrair sua configuração em um modelo abstrato) é geralmente aplicável, a implementação específica é Entlib somente para.

Obtendo objetos do container

Assim, podemos tiver chegado nosso recipiente configurado.Que é metade da batalha.Mas como obter os objetos de volta out?Interfaces de recipiente variam em isso bem, embora Felizmente não tanto quanto fazer suas interfaces de configuração.

Felizmente, tínhamos inventar uma nova abstração aqui.Inspirada pelo de uma postagem de blog de Jeremy Miller no verão 2008, os padrões & práticas grupo, a equipe MEF da Microsoft e autores de muitas diferentes recipientes DI trabalharam juntos para definir um menor denominador comum para resolução de objetos de um recipiente.Isso foi publicado para CodePlex e MSDN como de projeto Common Service Locator .Essa interface nos deu exatamente precisávamos; de dentro do Enterprise Library, sempre que precisávamos obter um objeto de recipiente, chamamos poderia através desta interface e isolados do recipiente específico que está sendo usado.Obviamente, a próxima pergunta é: onde está o contêiner?

Enterprise Library não tem qualquer tipo de requisito de bootstrap.Quando usando façades estático, você não precisa chamar uma função de inicialização em qualquer lugar.Biblioteca original trabalhada por puxando configuração quando necessário primeiro.Tínhamos duplicar esse comportamento para que a biblioteca seria pronta quando chamado.

O que precisávamos era um padrão conhecido para obter um contêiner configurado corretamente.Biblioteca comum Service Locator realmente tem uma destas opções: a propriedade estática ServiceLocator.Current.Decidimos para não usar isso para alguns dos motivos.O motivo principal foi que a ServiceLocator.Current poderia ser usado por outras bibliotecas ou até mesmo pelo próprio aplicativo.Precisávamos ser capaz de definir o recipiente no primeiro acesso de qualquer objeto Entlib; qualquer outra coisa era uma receita para perda de cabelo como pessoas tentaram descobrir por que desapareceram seus recipientes construídos cuidadosamente ou por que Entlib trabalhou na primeira chamada mas não posteriormente.O segundo motivo tem a ver com shortcoming na própria interface.Não há nenhuma maneira para consultar a propriedade para descobrir se está foi definido.Que feitas disco rígido determinar quando configurar o recipiente.

Assim, criamos nossa própria propriedade estática: EnterpriseLibraryContainer.Current.Você pode definir essa propriedade do código do usuário, bem, mas é especificamente a parte da Enterprise Library para que haja menos chance de conflitos com outras bibliotecas ou aplicativo principal.Na primeira chamada para uma fachada estática, verifique EnterpriseLibraryContainer.Current.Se definido, use qualquer item que está lá.Se não, criar um objeto UnityContainer, configurá-lo usando um Configurador e definir como o valor da propriedade Current.

O resultado é que agora há três maneiras diferentes para acessar a funcionalidade da Enterprise Library.Se você usar a API clássica, tudo apenas funciona.Um recipiente Unity nos bastidores será criado e usado.Se você estiver usando um contêiner DI diferente em seu aplicativo e não deseja Unity no seu processo mas ainda está usando a API clássica, configurar seu recipiente usando um Configurador, dispor em um IServiceLocator, fique em EnterpriseLibraryContainer.Current e em seguida, o façades continuarão a funcionar.Eles só agora está usando seu contêiner de escolha nos bastidores.Estamos realmente não fornecer qualquer configurators recipiente em que o projeto principal Entlib other than para Unity; nossa esperança é que a comunidade será implementá-los para outros recipientes.

Uma segunda opção é usar EnterpriseLibraryContainer.Current diretamente.Você pode chamar GetInstance < T > () para obter qualquer objeto da Enterprise Library e ela vai lhe um.E, novamente, fique um recipiente diferente por trás dela, se você desejar.

Finalmente, você pode simplesmente usar seu contêiner de escolha diretamente.Você terá de bootstrap configuração Entlib no recipiente usando um Configurador, mas se você estiver usando um contêiner, você precisa defini-la independentemente, portanto, isso não é um novo requisito.De lá, simplesmente injetar o Entlib objetos que deseja como dependências e estiver desativado e executando.

Como gostaria fazer?

Let’s olhar novamente nosso conjunto de metas e ver como esse design pilhas.

  1. Código de cliente existente não deve ter que alterar apenas devido as alterações de arquitetura.Exigir uma recompilação está ok, mas exigir alterações de código de origem não é (do curso, o cliente API pode alterar por outros motivos ).Internal ou APIs de extensibilidade são feira jogo.

    ATENDIDOS. A API original ainda funciona inalterado.Se você não se preocupa injeção de dependência, você nem precisa saber nem preocupar como os objetos são cabeados backup nos bastidores.

  2. Remova os pipelines de criação do objeto redundantes.Podemos deve ter apenas uma maneira de criar objetos não dois (ou mais).

    ATENDIDOS. Pilha ObjectBuilder desaparecida de base de código; tudo agora é criado por meio de mecanismos TypeRegistration e do Configurador.É necessário um Configurador por contêiner.

  3. Os clientes que não se preocupa DI não devem ser afetados por Entlib usando internamente.

    ATENDIDOS. DI não apresente próprio a menos que você deseja que ele.

  4. Os clientes que fazer preocupa DI podem escolher o contêiner que deseja usar e obter seus objetos e objetos de Entlib fora do.

    ATENDIDOS. Você pode usar que seu recipiente DI de preferência diretamente ou você pode ter utilizado nos bastidores atrás façades estático.

Acabamos com alguns benefícios adicionais também.A base de código Entlib ficou mais simples.Acabamos excluindo classes sobre 200 da implementação original.Após adicionar as partes do registro de tipo, conseguimos para baixo sobre 80 classes total após a refatoração foi feita.Além disso, as classes que foram adicionadas foram mais simples do que aqueles que foram removidos e a estrutura geral foi significativamente mais consistente, com menos partes móveis ou casos especiais.

Outra vantagem foi Refatorada versão ativada para ser um pouco mais rápido do que o original, com algumas medidas iniciais, informais mostrando um ganho de desempenho de 10 por cento.Após vimos os números, isso realmente fazia sentido para nós.Muito da complexidade no código original foi partir de uma série de otimizações de desempenho que trabalhou em torno implementação lenta do ObjectBuilder.A maioria dos recipientes DI tiveram um trabalho significativo feito no seu desempenho geral.Por reconstrução Entlib na parte superior de um recipiente, podemos aproveitar que funcionam de desempenho e não ter fazê-lo muito sozinhos.Como Unity e outro recipiente evoluem e são otimizados ainda mais, Entlib deve obter mais rápido sem muito esforço da nossa parte.

Lições aprendidas para outras bibliotecas

A Enterprise Library é um bom exemplo de uma biblioteca realmente aproveita um contêiner de injeção de dependência sem rígido estar acoplado a um.Se você gostaria de escrever uma biblioteca que usa um recipiente DI mas não força sua escolha no seu consumidor, esperamos que você pode encontrar alguns inspiração design do nosso exemplo.Acho que nossas metas para alteração, particularmente essas duas últimas são relevantes para qualquer biblioteca autor, não apenas Entlib:

  • Os clientes que não se preocupa DI não devem ser afetados por Entlib usando internamente.
  • Os clientes que fazer preocupa DI podem escolher o contêiner que deseja usar e obter seus objetos e objetos de Entlib fora do.

Ao projetar sua biblioteca, há várias perguntas que você precisa pensar.Certifique-se de considerar essas:

  • Como é bootstrapped sua biblioteca?Os clientes que faça algo específico para obter seu código configurar ou ter um ponto de entrada estática que deve funcionar apenas?
  • Como modelar seus gráficos de objeto para que um recipiente pode ser configurado sem precisar chamadas de código rígido em que recipiente?Uma olhada no nosso sistema TypeRegistration para inspiração.
  • Como o recipiente que você está usando será gerenciado?Está indo para ser manipulados internamente ou seus chamadores gerenciá-lo?Como o chamador dizer qual recipiente usar?

Podemos surgiu com um bom conjunto de respostas a essas perguntas para nosso projeto.Espero que nosso exemplo pode fornecer inspiração quando você estiver criando sua.

Chris Tavares é um desenvolvedor em Microsoft patterns & practices equipe, onde ele é o líder de desenvolvimento de Enterprise Library e Unity.Anteriores ao Microsoft, ele trabalhou em consultoria, encolher quebra automática de software e sistemas incorporados.Ele sobre Entlib, p & p e tópicos gerais de desenvolvimento em tavaresstudios.com de .