portaldacalheta.pt
  • Principal
  • Vida Designer
  • Design De Marca
  • Ciclo De Vida Do Produto
  • Ferramentas E Tutoriais
Tecnologia

Segure a estrutura - Explorando padrões de injeção de dependência



Vistas tradicionais sobre inversão de controle (IoC) parecem traçar uma linha dura entre duas abordagens diferentes: o localizador de serviço e os padrões de injeção de dependência (DI).

Praticamente todos os projetos que conheço incluem uma estrutura de DI. As pessoas são atraídas por eles porque promovem um acoplamento fraco entre clientes e suas dependências (geralmente por meio de injeção de construtor) com o mínimo ou nenhum código clichê. Embora seja ótimo para um desenvolvimento rápido, algumas pessoas acham que pode dificultar o rastreamento e depuração do código. A “magia dos bastidores” geralmente é alcançada por meio da reflexão, que pode trazer todo um conjunto de novos problemas.



Neste artigo, exploraremos um padrão alternativo que é adequado para bases de código Java 8+ e Kotlin. Ele retém a maioria dos benefícios de uma estrutura de DI ao mesmo tempo em que é tão direto quanto um localizador de serviço, sem exigir ferramentas externas.



Motivação

  • Evite dependências externas
  • Evite reflexão
  • Promova injeção de construtor
  • Minimize o comportamento do tempo de execução

Um exemplo

No exemplo a seguir, vamos modelar uma implementação de TV, onde diferentes fontes podem ser usadas para obter conteúdo. Precisamos construir um dispositivo que pode receber sinais de várias fontes (por exemplo, terrestre, cabo, satélite, etc.). Vamos construir a seguinte hierarquia de classes:



Hierarquia de classes de um dispositivo de TV que implementa uma fonte de sinal arbitrária

Agora vamos começar com uma implementação tradicional de DI, onde uma estrutura como o Spring está conectando tudo para nós:



public class TV { private final TvSource source; public TV(TvSource source) { this.source = source; } public void turnOn() { System.out.println('Turning on the TV'); this.source.tuneChannel(42); } } public interface TvSource { void tuneChannel(int channel); } public class Terrestrial implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Adjusting dish frequency to channel %d ', channel); } } public class Cable implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Changing digital signal to channel %d ', channel); } }

Notamos algumas coisas:

  • A classe TV expressa uma dependência de um TvSource. Uma estrutura externa verá isso e injetará uma instância de uma implementação concreta (Terrestre ou Cabo).
  • O padrão de injeção do construtor permite um teste fácil porque você pode construir facilmente instâncias de TV com implementações alternativas.

Começamos bem, mas percebemos que trazer uma estrutura de DI para isso pode ser um pouco exagerado. Alguns desenvolvedores relataram problemas de depuração de problemas de construção (rastreamentos de pilha longos, dependências não rastreáveis). Nosso cliente também expressou que os tempos de fabricação são um pouco mais longos do que o esperado e nosso perfilador mostra lentidão nas chamadas reflexivas.



Uma alternativa seria aplicar o padrão Service Locator. É simples, não usa reflexão e pode ser suficiente para nossa pequena base de código. Outra alternativa é deixar as classes sozinhas e escrever o código de localização da dependência em torno delas.

Depois de avaliar muitas alternativas, escolhemos implementá-lo como uma hierarquia de interfaces de provedor. Cada dependência terá um provedor associado que terá a responsabilidade exclusiva de localizar as dependências de uma classe e construir uma instância injetada. Também faremos do provedor uma interface interna para facilidade de uso. Vamos chamá-lo de injeção de Mixin porque cada provedor se mistura com outros provedores para localizar suas dependências.



Os detalhes de porque eu me conformei com essa estrutura são elaborados em Detalhes e Justificativa, mas aqui está a versão resumida:

  • Ele segregou o comportamento do local de dependência.
  • Estender interfaces não é problema do diamante.
  • As interfaces têm implementações padrão.
  • Dependências ausentes impedem a compilação (pontos de bônus!).

O diagrama a seguir mostra como as dependências e os provedores interagem, e a implementação é ilustrada abaixo. Também adicionamos um método principal para demonstrar como podemos compor nossas dependências e construir um objeto de TV. Uma versão mais longa deste exemplo também pode ser encontrada neste GitHub .



Interações entre provedores e dependências

public interface TvSource { void tuneChannel(int channel); interface Provider { TvSource tvSource(); } } public class TV { private final TvSource source; public TV(TvSource source) { this.source = source; } public void turnOn() { System.out.println('Turning on the TV'); this.source.tuneChannel(42); } interface Provider extends TvSource.Provider { default TV tv() { return new TV(tvSource()); } } } public class Terrestrial implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Adjusting dish frequency to channel %d ', channel); } interface Provider extends TvSource.Provider { @Override default TvSource tvSource() { return new Terrestrial(); } } } public class Cable implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Changing digital signal to channel %d ', channel); } interface Provider extends TvSource.Provider { @Override default TvSource tvSource() { return new Cable(); } } } // Here compose the code above to instantiate a TV with a Cable TvSource public class Main { public static void main(String[] args) { new MainContext().tv().turnOn(); } static class MainContext implements TV.Provider, Cable.Provider { } }

Algumas notas sobre este exemplo:



  • A classe TV depende de um TvSource, mas não conhece nenhuma implementação.
  • O TV.Provider estende o TvSource.Provider porque ele precisa do método tvSource () para construir um TvSource e pode usá-lo mesmo se não estiver implementado lá.
  • As fontes Terrestre e Cabo podem ser usadas alternadamente pela TV.
  • As interfaces Terrestrial.Provider e Cable.Provider fornecem implementações TvSource concretas.
  • O método principal tem uma implementação concreta MainContext de TV.Provider que é usada para obter uma instância de TV.
  • O programa requer uma implementação TvSource.Provider no tempo de compilação para instanciar uma TV, então incluímos Cable.Provider como exemplo.

Detalhes e justificativa

Nós vimos o padrão em ação e alguns dos motivos por trás dele. Você pode não estar convencido de que deve usá-lo agora, e você estaria certo; não é exatamente uma bala de prata. Pessoalmente, acredito que é superior ao padrão de localizador de serviço na maioria dos aspectos. No entanto, quando comparado aos frameworks de DI, é preciso avaliar se as vantagens superam a sobrecarga de adicionar código clichê.

Provedores estendem outros provedores para localizar suas dependências

Quando um provedor estende outro, as dependências são vinculadas. Isso fornece a base básica para validação estática que evita a criação de contextos inválidos.

Um dos principais pontos fracos do padrão do localizador de serviço é que você precisa chamar um GetService() genérico | método que de alguma forma resolverá sua dependência. Em tempo de compilação, você não tem garantias de que a dependência será registrada no localizador e seu programa pode falhar em tempo de execução.

O padrão DI também não aborda isso. A resolução de dependência geralmente é feita por meio de reflexão por uma ferramenta externa que fica quase totalmente oculta do usuário, que também falha no tempo de execução se as dependências não forem atendidas. Ferramentas como CDI do IntelliJ (disponível apenas na versão paga) fornecem algum nível de verificação estática, mas apenas Punhal com seu pré-processador de anotação parece resolver esse problema por design.

As classes mantêm a injeção típica do construtor do padrão DI

Isso não é obrigatório, mas definitivamente desejado pela comunidade de desenvolvedores. Por um lado, você pode apenas olhar para o construtor e ver imediatamente as dependências da classe. Por outro lado, permite o tipo de teste de unidade que muitas pessoas aderem, que é construindo o assunto em teste com simulações de suas dependências.

Isso não quer dizer que outros padrões não sejam suportados. Na verdade, pode-se até descobrir que Mixin Injection simplifica a construção de gráficos de dependência complexos para teste porque você só precisa implementar uma classe de contexto que estenda o provedor do seu assunto. O MainContext acima é um exemplo perfeito onde todas as interfaces têm implementações padrão, portanto, pode ter uma implementação vazia. Substituir uma dependência requer apenas a substituição de seu método de provedor.

Vejamos o seguinte teste para a aula de TV. Ele precisa instanciar uma TV, mas em vez de chamar o construtor da classe, está usando a interface TV.Provider. O TvSource.Provider não tem implementação padrão, portanto, precisamos escrevê-lo nós mesmos.

public class TVTest { @Test public void testWithProvider() { TvSource source = Mockito.mock(TvSource.class); TV.Provider provider = () -> source; // lambdas FTW provider.tv().turnOn(); Mockito.verify(source, times(1)).tuneChannel(42); } }

Agora vamos adicionar outra dependência à aula de TV. A dependência CathodeRayTube faz a mágica para fazer uma imagem aparecer na tela da TV. Ele é desacoplado da implementação da TV porque podemos mudar para LCD ou LED no futuro.

public class TV { public TV(TvSource source, CathodeRayTube cathodeRayTube) { ... } public interface Provider extends TvSource.Provider, CathodeRayTube.Provider { default TV tv() { return new TV(tvSource(), cathodeRayTube()); } } } public class CathodeRayTube { public void beam() { System.out.println('Beaming electrons to produce the TV image'); } public interface Provider { default CathodeRayTube cathodeRayTube() { return new CathodeRayTube(); } } }

Se você fizer isso, notará que o teste que acabamos de escrever ainda compila e passa conforme o esperado. Adicionamos uma nova dependência à TV, mas também fornecemos uma implementação padrão. Isso significa que não precisamos simular se quisermos apenas usar a implementação real, e nossos testes podem criar objetos complexos com qualquer nível de granularidade simulada que desejarmos.

Isso é útil quando você deseja simular algo específico em uma hierarquia de classes complexa (por exemplo, apenas a camada de acesso ao banco de dados). O padrão permite configurar facilmente o tipo de testes sociáveis que às vezes são preferidos a testes solitários.

Independentemente de sua preferência, você pode ter certeza de que pode recorrer a qualquer forma de teste que melhor se adapte às suas necessidades em cada situação.

Evite dependências externas

Como você pode ver, não há referências ou menções a componentes externos. Isso é fundamental para muitos projetos que têm tamanho ou mesmo restrições de segurança. Também ajuda com a interoperabilidade porque os frameworks não precisam se comprometer com um framework DI específico. Em Java, tem havido esforços como JSR-330 Dependency Injection para Java Standard que atenuam os problemas de compatibilidade.

Evite Reflexo

As implementações do localizador de serviço geralmente não dependem de reflexão, mas as implementações de DI sim (com a notável exceção do Dagger 2). Isso tem as principais desvantagens de desacelerar a inicialização do aplicativo porque o framework precisa verificar seus módulos, resolver o gráfico de dependência, construir reflexivamente seus objetos, etc.

O Mixin Injection exige que você escreva o código para instanciar seus serviços, semelhante à etapa de registro no padrão de localizador de serviço. Esse pequeno trabalho extra remove completamente as chamadas reflexivas, tornando seu código mais rápido e direto.

Dois projetos que recentemente me chamaram a atenção e se beneficiam de evitar a reflexão são Graal’s Substrate VM e Kotlin / Native . Ambos compilam para bytecode nativo, e isso requer que o compilador saiba com antecedência sobre quaisquer chamadas reflexivas que você fará. No caso do Graal, é especificado em um Arquivo JSON que é difícil de escrever , não pode ser verificado estaticamente, não pode ser facilmente refatorado usando suas ferramentas favoritas. Usar o Mixin Injection para evitar reflexos em primeiro lugar é uma ótima maneira de obter os benefícios da compilação nativa.

Minimize o comportamento do tempo de execução

Ao implementar e estender as interfaces necessárias, você constrói o gráfico de dependência uma parte de cada vez. Cada provedor fica próximo à implementação concreta, o que traz ordem e lógica ao seu programa. Este tipo de camadas será familiar se você já usou o padrão Mixin ou o padrão Bolo antes.

Neste ponto, pode valer a pena falar sobre a classe MainContext. É a raiz do gráfico de dependência e conhece o quadro geral. Esta classe inclui todas as interfaces de provedor e é a chave para habilitar verificações estáticas. Se voltarmos ao exemplo e removermos Cable.Provider de sua lista de implementos, veremos isso claramente:

o que é power pivot no excel
static class MainContext implements TV.Provider { } // ^^^ // MainContext is not abstract and does not override abstract method tvSource() in TvSource.Provider

O que aconteceu aqui é que o aplicativo não especificou o TvSource concreto a ser usado e o compilador detectou o erro. Com localizador de serviço e DI baseado em reflexão, esse erro poderia ter passado despercebido até que o programa travasse em tempo de execução - mesmo se todos os testes de unidade fossem aprovados! Acredito que esses e outros benefícios que mostramos superam a desvantagem de escrever o clichê necessário para fazer o padrão funcionar.

Capturar Dependências Circulares

Vamos voltar ao exemplo CathodeRayTube e adicionar uma dependência circular. Digamos que queremos que seja injetada uma instância de TV, então estendemos TV.Provedor:

public class CathodeRayTube { public interface Provider extends TV.Provider { // ^^^ // cyclic inheritance involving CathodeRayTube.Provider default CathodeRayTube cathodeRayTube() { return new CathodeRayTube(); } } }

O compilador não permite herança cíclica e não podemos definir esse tipo de relacionamento. A maioria dos frameworks falham em tempo de execução quando isso acontece, e os desenvolvedores tendem a contornar isso apenas para fazer o programa funcionar. Mesmo que esse antipadrão possa ser encontrado no mundo real, geralmente é um sinal de design ruim. Quando o código falha na compilação, devemos ser encorajados a procurar soluções melhores antes que seja tarde demais para mudar.

Manter a simplicidade na construção de objetos

Um dos argumentos a favor do SL em vez do DI é que ele é direto e fácil de depurar. É claro a partir dos exemplos que instanciar uma dependência será apenas uma cadeia de chamadas de método do provedor. Rastrear a origem de uma dependência é tão simples quanto entrar na chamada do método e ver onde você termina. A depuração é mais simples do que ambas as alternativas, porque você pode navegar exatamente onde as dependências são instanciadas, direto do provedor.

Serviço vitalício

Um leitor atento pode ter notado que esta implementação não resolve o problema de vida útil do serviço. Todas as chamadas para métodos de provedor irão instanciar novos objetos, tornando isso semelhante a Escopo do protótipo do Spring .

Essa e outras considerações estão um pouco fora do escopo deste artigo, pois eu apenas queria apresentar a essência do padrão sem distrair os detalhes. O uso e a implementação completos em um produto, entretanto, precisam levar em consideração a solução completa com suporte vitalício.

Conclusão

Esteja você acostumado com estruturas de injeção de dependência ou escrevendo seus próprios localizadores de serviço, você pode querer explorar esta alternativa. Considere usar o padrão mixin que acabamos de ver e veja se você pode tornar seu código mais seguro e fácil de raciocinar.

Relacionado: Práticas recomendadas de JS: construir um bot Discord com TypeScript e injeção de dependência

Compreender o básico

O que significa inversão de controle?

Falamos sobre inversão de controle quando um trecho de código é capaz de delegar comportamento a outro componente que não é conhecido no momento da escrita desse código (geralmente por meio de interfaces e pontos de extensão bem conhecidos).

O que é injeção de dependência?

A injeção de dependência é um padrão pelo qual podemos alcançar a inversão de controle no nível de componente de um sistema. Uma classe declara apenas os componentes de que precisa para cumprir seu objetivo, não como encontrar ou criar esses componentes.

Conheça o seu monumento: ‘Taj de Haryana’, a tumba de Sheikh Chilli

Eventos Coisas Para Fazer

Conheça o seu monumento: ‘Taj de Haryana’, a tumba de Sheikh Chilli
Por que as startups precisam de um guia de estilo

Por que as startups precisam de um guia de estilo

Design Ux

Publicações Populares
Rede Al-Jazeera America é encerrada hoje
Rede Al-Jazeera America é encerrada hoje
Por que as moedas dos mercados emergentes são voláteis?
Por que as moedas dos mercados emergentes são voláteis?
Um pai compartilha sua jornada pessoal ao lidar com uma filha disléxica
Um pai compartilha sua jornada pessoal ao lidar com uma filha disléxica
Você precisa de um herói: o gerente de projeto
Você precisa de um herói: o gerente de projeto
Folha de dicas de CSS rápida e prática do ApeeScape
Folha de dicas de CSS rápida e prática do ApeeScape
 
Tutorial OpenCV: Detecção de objetos em tempo real usando MSER no iOS
Tutorial OpenCV: Detecção de objetos em tempo real usando MSER no iOS
Discurso de Barack Obama marca contagem regressiva não oficial para americanos negros
Discurso de Barack Obama marca contagem regressiva não oficial para americanos negros
Arquitetura orientada a serviços com AWS Lambda: um tutorial passo a passo
Arquitetura orientada a serviços com AWS Lambda: um tutorial passo a passo
Dez principais regras de design de front-end para desenvolvedores
Dez principais regras de design de front-end para desenvolvedores
UE adia negociações comerciais com a Austrália em meio a negociações de submarinos
UE adia negociações comerciais com a Austrália em meio a negociações de submarinos
Publicações Populares
  • melhores práticas de design de banco de dados relacional
  • qual destes melhor descreve um exemplo de usabilidade
  • aulas de codificação c ++
  • Práticas recomendadas de design responsivo 2019
  • melhores cursos c ++
  • qualquer llc que tenha pode optar por ser tributada como uma sociedade ou como uma empresa.
Categorias
  • Vida Designer
  • Design De Marca
  • Ciclo De Vida Do Produto
  • Ferramentas E Tutoriais
  • © 2022 | Todos Os Direitos Reservados

    portaldacalheta.pt