Desenvolvimento

29 ago, 2017

Engenharia escalável, recursos móveis isolados com plugins na Uber

Publicidade

À medida que a Uber cresce, a empresa continua a aprimorar sua abordagem para a arquitetura de aplicativos móveis, a fim de suportar a escala de negócios. Quando os aplicativos crescem, pode tornar-se mais difícil adicionar novos recursos sem quebrar outros recursos, experimentar com recursos existentes e decidir onde eles devem ser integrados. Os investimentos feitos pela Uber em arquitetura RIB e ferramentas de plugin nos últimos 18 meses visaram esses problemas de escala e permitiram que os engenheiros aumentassem, de forma mensurável, sua produtividade e, em muitos casos, a dobrassem.

Neste artigo, destaco quatro aspectos-chave da abordagem atual da Uber para ferramentas de plug-in de compilação para Android e iOS:

  1. Por que e como o framework móvel aplica isolamento de código com plugins
  2. Como os plugins são usados para incentivar a estruturação do aplicativo como um pequeno conjunto de pontos de integração de recursos e os benefícios que isso fornece
  3. Como os plugins são para mitigar problemas com sinalizadores de experimentação
  4. Os usuários ideais para esse tipo de sistema de plugin

Em última análise, os plugins permitem a construção e o envio de recursos de forma rápida e eficiente, independentemente da escala.

Aplicando mais isolamento de código

O desenvolvimento de recursos de isolamento é um princípio fundamental do desenvolvimento de aplicativos de escala. Como tal, as arquiteturas para aplicativos de tela única com centenas de tentativas/envios por semana precisam promover o isolamento do código.

A Engenharia da Uber tornou-se poética sobre o seu amor por RIBs, uma arquitetura que encoraja o isolamento do código. Se já sabemos que os RIB manipulam o isolamento tão bem, por que ainda estamos falando sobre isso?

  1. Isolar recursos que não estão contidos dentro de RIBs pode ser útil; por exemplo, isolar várias fontes de dados de busca de localização uma da outra permite que as fontes de dados complexas sejam experimentadas em paralelo.
  2. Um maior isolamento entre RIBs é possível e benéfico.

Abaixo, discutimos algumas razões pelas quais o maior isolamento entre os RIBs pode servir como um enorme benefício para o desenvolvimento da arquitetura móvel.

O que RIBs já nos dá

A arquitetura RIB é projetada em torno da premissa de construir aplicativos como árvores profundas de escopos de lógica de negócios, conforme demonstrado na Figura 1 abaixo:

Em uma versão simplificada da árvore RIB do aplicativo do passageiro (acima), um usuário transita através de estados comuns. Os RIBs são anexados e destacados da árvore à medida que o usuário executa essas ações

Este projeto fornece benefícios significativos de isolamento. Por exemplo, na nossa arquitetura de aplicativos do passageiro, os RIBs de Refinamento de Aeroporto, Refinamento de Localização e Confirmação devem ser escritos independentemente um do outro, uma vez que não podem acessar qualquer um dos estados possuídos pelos escopos uns dos outros.

O projeto da tela de confirmação do aplicativo do passageiro, tela de refinamento da localização e tela de refinamento da porta do aeroporto se beneficiam do isolamento de recurso oferecido pelos RIBs

Então, que isolamento estamos perdendo?

Considere o exemplo de solicitação Refinement Steps/Passos de Refinamento na Figura 1. No aplicativo da Uber, existem mais de 30 crianças diferentes possíveis em Passos de Refinamento. Os RIBs aplicam o isolamento entre as crianças, mas não entre o RIB de Passos de Refinamento pais e crianças, como o RIB de Seleção de Porta do Aeroporto.

A hierarquia entre o RIB de Passos de Refinamento e seus RIBs de criança incorporam uma única camada de isolamento

O RIB de Passos de Refinamento é responsável por anexar e destacar os RIBs de criança sob ele, realizando pedidos entre esses RIBs de criança e passando dados específicos para eles. Quando executado com sucesso, o RIB de Passos de Refinamento é completamente desacoplado de suas crianças, permitindo que os RIB individuais de criança sejam desenvolvidos e lançados em produção sem impactar os outros RIBs de criança.

No entanto, em um ambiente de alta velocidade, a maioria dos engenheiros tem uma tendência natural de codificar com o objetivo de minimizar a complexidade e o tempo de desenvolvimento. Por exemplo, é mais conveniente e mais rápido para um engenheiro individualmente adicionar detalhes que devem pertencer ao RIB de Seleção de Porta do Aeroporto para o RIB de Passos de Refinamento para apoiar uma animação de transição do RIB de Seleção de Porta do Aeroporto para o RIB de Refinamento de Localização. Isto une o RIB de Passos de Refinamento às suas crianças. Uma vez que o RIB de Passos de Refinamento se uniu a crianças específicas, suas crianças são, por extensão, também unidas. No entanto, esta abordagem resulta em uma organização de engenharia que perde a confiança, uma vez que mudar uma criança não quebrará as outras.

Na Uber,  o objetivo é reforçar o isolamento entre RIBs pai e criança. Por isso, precisa-se de uma arquitetura que faça mais do que permitir fácil isolamento de recursos (RIBs); ela também precisa garantir o isolamento dos recursos.

Aplicando o isolamento entre RIBs pais e filhos

Uma boa solução não deve exigir mudanças significativas na forma como as hierarquias RIB são estruturadas e como os relacionamentos (por exemplo, com inversão de dependência), são modulados, cria-se custos de desempenho enormes ou quebra-se segurança de tipo. A solução é simples: adicione uma camada extra para reforçar que os detalhes de implementação de RIB de crianças não possam ser referenciados por RIBs de pais.

Faz-se isso, definindo uma classe de ponto de plugin (basicamente uma fábrica de recursos extravagante) que pode ser referenciada pelo RIB pai. Voltando ao exemplo de RIB de Passos de Refinamento acima, permite-se que o ponto de plugin referencie a RIBs individuais de Passos de Refinamento. Em seguida, usa-se as ferramentas de compilação para garantir que os Passos de Refinamento não possam fazer referência direta a nenhum dos plugins, apenas ao ponto de plugin, conforme mostrado abaixo na Figura 3:

A separação forçada entre o código do plugin e o do não-plugin garante que o RIB de Passos de Refinamento já não possa mais fazer referência à Seleção da Porta do Aeroporto diretamente e agora é forçado a lidar com cada RIB filho de acordo com uma interface retornada do ponto de plugin

Diferentes ferramentas são usadas para garantir isso tanto no Android, quanto no iOS. Veja-as destacadas abaixo:

Android

  • O código do plugin e do não-plugin é distinguido pelo nome do pacote.
  • Linters personalizados e verificadores sujeitos a erros garantem que o código do não-plugin não possa fazer referência a classes de plugins. A única maneira de preencher o espaço entre o código do plugin e o do não-plugin é usando pontos de plugin.

iOS

  • O código do plugin e do não-plugin é distinguido por qual framework eles estão escritos.
  • Os frameworks de não-plugins não podem fazer referência direta aos frameworks de plugins. Portanto, frameworks de ponto de plugin superam essa lacuna. Além disso, as dependências do framework transitivas estão desabilitadas.

Da mesma forma que se aplica o isolamento entre pais e filhos, usa-se ferramentas básicas para reforçar a separação entre plugins. Eles encorajam o desenvolvimento de plugins de recursos dentro de seus próprios objetivos de compilação (módulos do Android ou frameworks do iOS) e reforçam que os objetivos de compilação do plugin não possam se referenciar um ao outro (veja a Figura 4). Este desenvolvimento de objetivos de compilação separados adiciona pequenas despesas gerais do programador, já que os RIBs são naturalmente isolados de qualquer maneira, ao mesmo tempo em que fornecem benefícios de isolamento e melhorias no tempo de compilação.

Ferramentas básicas evitam dependências de destino de compilação inter-plugin, que oferece benefícios de isolamento e melhorias de tempo de compilação

Separando o código de cola do código de recurso

Em muitos aplicativos, o código de cola é misturado com o código de recurso. Quando esta abordagem é tomada com um aplicativo que contém centenas de recursos – na Uber, por exemplo, eles têm muitos recursos e otimizações úteis específicas por região – você começará a observar o seguinte:

  • Dificuldade em adicionar novos recursos. Os engenheiros precisam caçar através da base de códigos para encontrar o local correto para adicionar seu recurso.
  • Impossibilidade de raciocinar sobre o aplicativo. Para entender a estrutura de alto nível do aplicativo, você deve ler/entender tanto o código do recurso, como o código de cola do aplicativo. Nenhum mero mortal pode manter tudo isso em sua cabeça ao mesmo tempo!
  • Instabilidade. O código que cola o aplicativo em conjunto muda em paralelo com a introdução de novos recursos.
  • Limites à escalabilidade do produto. Os engenheiros começam a integrar recursos sempre que for mais conveniente. Como resultado, os recursos podem ser integrados na mesma tela de várias maneiras. Isso cria telas como o exemplo abaixo:
Uma tela teórica no aplicativo da Uber, onde os recursos foram integrados onde fosse mais conveniente. Isso não está muito longe do que costumava ser a nossa realidade

Como separamos a cola?

Uma maneira de garantir que o desenvolvimento de aplicativos e a experiência do produto permaneçam racionais a longo prazo é configurar trilhos fortes no aplicativo para orientar o desenvolvimento de novos recursos.

À medida que a Uber aprimora os aplicativos existentes e cria novos, encoraja-se os recursos a serem escritos como integrações nos pontos de plugin existentes tanto quanto possível. Cada ponto de plugin funciona como um “trilho” de integração. Como resultado, 80% da camada de aplicação do aplicativo do passageiro da Uber agora reside dentro dos plugins. Os restantes 20% do código cola o aplicativo em conjunto, como mostrado na Figura 6 abaixo:

Uma árvore RIB é composta por uma combinação de RIBs de recursos (bege) e código de cola (azul)

Diferencia-se o código do plugin e o código não-plugin com base na estrutura do diretório e adicionamos revisores de código adicionais a todas as revisões de código que tocam o código não-plugin. Isso encoraja os engenheiros a escrever seu código dentro de plugins em pontos de plugin existentes em vez de locais ad hoc.

Como resultado, as centenas de recursos do nosso aplicativo do passageiro são integrados em 50 maneiras diferentes em vez de centenas. Isso alcança:

  • Facilidade de desenvolvimento de recursos. Adicionar o 501º recurso apenas exige que consideremos cerca de 50 lugares para encontrar o ponto de integração correto, em vez de avaliar a base de códigos inteira.
  • Arquitetura de aplicativos fácil de entender. Os 20% do código que cola o aplicativo em conjunto podem ser fundamentados independentemente dos recursos do aplicativo. Este é o único código que você precisa entender a arquitetura de alto nível do aplicativo.
  • Estabilidade. O código que cola o aplicativo em conjunto muda de forma pouco frequente. Quando ele muda, mais olhos (dos revisores de código interno) estão sobre nele.
  • Escalabilidade do produto. Incentivar os engenheiros a reutilizar pontos de integração existentes em aplicativos ao adicionar novos recursos incentiva a reutilização de projetos de produtos existentes, tornando o desenvolvimento mais rápido e fácil.

Usando pontos de plugin: dois exemplos de trilhos

Podemos criar a maioria dos recursos em nossos aplicativos em produtos ou trilhos técnicos existentes. Aqui estão dois exemplos do aplicativo do passageiro:

Exemplo A: Pesquisa de localização

A tela de pesquisa de localização exibida acima é construída em torno de um ponto de plugin que retorna objetos LocationRowProvider.

Ao integrar o novo recurso de Eventos do Calendário na tela de pesquisa de localização, um novo subtipo LocationRowProvider foi criado e adicionado ao LocationSearchPluginPoint. Não havia necessidade de conectar fluxos de dados adicionais no núcleo do aplicativo ou considerar as principais alterações nos produtos porque a arquitetura de plugin incentivava a separação do código de cola de Pesquisa de Localização dos recursos de Pesquisa de localização, conforme mostrado abaixo na Figura 7:

Nossa tela de Pesquisa de Localização e as correspondentes interfaces de pontos de plugin mostram como usamos plugins para criar recursos definidos no aplicativo

Se a tela de Pesquisa de Localização não tivesse sido construída dessa forma desde o início, um novo provedor de dados teria sido integrado de várias maneiras. A adição da nova fonte de dados pode ter exigido mudanças em vários locais de código, incluindo a alteração do código para adicionar uma nova fonte de dados, o código que apresenta visualizações de linha quando é fornecido um modelo de visualização de dados, o local onde os modelos de visualização de linha são classificados e mesclados e o tipo de dados do modelo de visualização de localização.

Exemplo B: Trabalho sem visualização com escopo

Os plugins funcionam bem para RIBs, mas nem todos os plugins precisam ser RIBs. Os trilhos mais versáteis no aplicativo são trilhos simples do ScopedWork. Por exemplo, suponha que um engenheiro deseja executar o código sempre que o LoggedInRIB do aplicativo do passageiro estiver anexado à árvore RIB do aplicativo. O engenheiro pode integrar um plugin no LoggedInScopedWorkPluginPoint, conforme mostrado em sua interface de plugin abaixo:

O LoggedInScopedWorkPluginPoint permite aos desenvolvedores integrar plugins facilmente

O trilho LoggedInScopedWork possui cerca de 30 integrações dentro do aplicativo do passageiro. Se não fosse este ponto de plugin, o LoggedInInteractor.java estaria excedendo mais de mil linhas de comprimento, difícil de modificar e difícil de entender.

Benefícios para experimentação e segurança

Sempre que a Uber lança novos recursos ou faz alterações em seu aplicativo, eles executam testes A/B para garantir que essas mudanças melhorem a estabilidade e métricas de negócios. Mas com centenas de mudanças escritas por semana, eles podem ter problemas para manter experimentos alinhados. As estratégias de teste A/B inconsistentes significam que a base do código estará cheia de condicionamentos que tornam o código mais difícil de se raciocinar sobre.

Eles mitigam esse problema usando plugins. Todo o plugin é liberado através de um teste A/B, o que reduz a necessidade de escrever ramificações de teste A/B condicionais em todo o resto da base de código. Uma vez que a integração dos plugins é consistente, pode-se deixar os sinalizadores do plugin A/B na base de código depois de terem sido completamente implantados sem aumentar a sua complexidade. Isso nos dá o poder de desativar remotamente cada recurso no aplicativo. Por exemplo, se experimentar um acidente de produção generalizado na animação de início de viagem, pode-se resolver o problema desativando remotamente a animação. Implementar plugins nos capacitou para resolver de forma rápida e efetiva vários grandes acidentes de produção usando esta estratégia.

Confia-se que é seguro desativar qualquer plugin porque em cada mudança realizada, executa-se testes de UI que exercem os fluxos principais do aplicativo com todos os plugins desativados. Embora não seja bonito, desativar plugins para esses testes é altamente funcional. Se os princípios por trás dos plugins fossem adotados (por exemplo, uso agressivo de padrões de fábrica abstratos e inversão de dependência) sem usar uma framework de plugin unificado, não seríamos capazes de reforçar essa forma de segurança ou de realizar esses testes de UI facilmente.

Podemos desativar plugins para testar novos recursos, uma funcionalidade altamente útil, mas pouco glamorosa, desse tipo de ferramentas

Você deve fazer o plugin?

Uma arquitetura de plugin unificada é valiosa para grandes aplicações com muitos pontos de integração de recursos e intenção de escala. Antes de você decidir fazer o salto, no entanto, considere se os benefícios superam os desafios para sua equipe.

As vantagens de um sistema de plugin podem não valer a pena para pequenas operações de engenharia. Mesmo grandes aplicativos podem não se beneficiar de um sistema de plugin estático formal se houver um pequeno número de pontos de integração de recursos. Por exemplo, os aplicativos que são principalmente centrados em feed têm uma divisão natural entre a cola do aplicativo e o código de recurso, independentemente de usar padrões ad hoc, um sistema de plugin ou inversão de dependência para injetar cartas de recursos no framework do cartão. Como tal, um padrão de plugin formal é mais útil para aplicativos grandes com muitos pontos de integração de recursos, por exemplo, aplicativos que contêm vários estados e subconjuntos apresentados no topo de um mapa:  TruliaGoogle Maps e o aplicativo do motorista da Uber.

Uma vez que você decide estruturar seu aplicativo como um conjunto de pontos de plugin, você terá que gastar tempo considerando a pergunta: “o menu do aplicativo é um único plugin RIB?” ou “é um framework com o qual os plugins se integram?”. A resposta depende se você deseja que as telas que se integram no menu sejam capazes de evoluir de forma rápida e/ou independente (provavelmente, sim) e se você deseja desencorajar as mudanças no scaffolding de menu.

Essas discussões são importantes para determinar o projeto de software mais adequado para seu aplicativo. Encorajar nossa equipe a discutir a escalabilidade de seus projetos no início foi uma das razões pelas quais os engenheiros móveis da empresa agora são duas vezes mais produtivos dentro do aplicativo do passageiro reescrito como o antigo aplicativo de passageiro. Na verdade, a aprovação da organização da arquitetura após a incorporação de RIBs e plugins mais que dobrou.

Conectando nossos takeaways

O sistema de plugins formal da Uber oferece uma série de benefícios e os princípios por trás da nossa decisão de integrá-los são amplamente aplicáveis, por exemplo:

  • É benéfico manter uma distinção entre o código do recurso do seu aplicativo e o código que cola seu aplicativo em conjunto.
  • Um sistema formal para fornecer isolamento de código é útil para extensibilidade e experimentação segura.

Quando nossos engenheiros criam recursos em cima de nossos aplicativos que são estão “pluginzados”, eles são mais produtivos e enviados com mais rapidez.

Interessado em dimensionar uma das arquiteturas móveis mais rápidas da tecnologia? Candidate-se a um cargo na equipe de Plataforma de Produção da Uber.

Brian Attwell é engenheiro de software na equipe de Plataforma Móvel da Uber. Ele é um dos engenheiros por trás da arquitetura RIB multiplataforma e um co-designer da arquitetura de plug-ins multiplataforma da Uber. Brian trabalhou no Google, Facebook e Apple. Ele originalmente apresentou este artigo como uma palestra na Mobilidade Uber, em São Francisco.

Para saber mais sobre o assunto, confira a conversa técnica do engenheiro de software Manu Sridharan no Curry On sobre como padrões arquitetônicos como plugins podem tornar a análise estática mais efetiva.

***

Fonte: https://eng.uber.com/plugins/