Android

30 mai, 2017

A jornada para o monorepo do Android: a história da organização da base de código Android da engenharia da Uber

Publicidade

Durante o nosso primeiro Uber Technology Day, a engenheira de software Aimee Lucido fez uma apresentação sobre a história da base de código Android da Engenharia da Uber. Neste artigo, ela aprofunda sobre as razões por trás da decisão da Uber em construir um monorepo para apoiar o crescimento do nosso desenvolvimento Android.

Hoje é o dia em que você vai construir um aplicativo Android novo – e bom para você, começar é sempre a parte mais difícil. Qual é a primeira coisa que você faz?

Se você é como eu, você criará um novo projeto no Android Studio. Você faz uma atividade principal, conecta o Gradle, e talvez até mesmo cria um repositório git para que seus amigos possam colaborar com você no aplicativo. Parabéns! Sua organização de código agora se parece com a primeira versão do aplicativo Android da Uber do passageiro.

Uma caixa de papelão simples representa a primeira base de código Android Uber: uma grande caixa de código.

Quando lançamos nosso aplicativo Android do passageiro em 2010, éramos uma pequena empresa. A Engenharia da Uber contava com cerca de uma dúzia de pessoas. Tínhamos um contratado trabalhando na plataforma Android e – se você puder acreditar – nem tínhamos um aplicativo do motorista Android. Então, nosso único engenheiro Android escreveu a primeira versão do aplicativo do passageiro em um único repositório: uma grande caixa de código.

Criar um novo aplicativo usando um único repositório resultou em alguns benefícios no início:

  • Android pronto para uso: Um único repositório é a estrutura de código gratuita oferecida no Eclipse, a IDE usada para a primeira versão do aplicativo Android do passageiro. Hoje, em 2017, a construção de uma base de código precoce em um único repositório é ainda mais fácil do que era em 2010, porque temos o Android Studio, que faz parceria com as bibliotecas do Android. Mas não importa qual IDE você usa, uma IDE irá fornecer uma única estrutura de repositório pronta para uso e ferramentas básicas, como scripts de construção e integração git.
  • Desenvolvimento rápido para equipes pequenas: Como todas as dependências estão em um só lugar, usar um único repositório permite que as coisas se movam rapidamente para uma pequena equipe de engenheiros, simplificando o compartilhamento de código e a refatoração.

Durante vários anos, o estado do Android na Uber era de pequena escala. Em 2013, contratamos o nosso primeiro engenheiro Android em tempo integral, e durante esse período a nossa equipe de engenharia tinha mais que duplicado. Só então começamos a construir um aplicativo do motorista Android.

Construir o aplicativo do motorista nos deu a oportunidade de melhorar nossa organização de base de código. O aplicativo do passageiro ainda existia em uma única base de código, mas como agora tínhamos os recursos e ferramentas para extrair componentes reutilizáveis, fizemos exatamente isso. O código do parceiro principal existia em seu próprio repositório separado, mas também construímos uma biblioteca cheia de componentes reutilizáveis para os dois aplicativos.

Essa estrutura geral serviu-nos bem por algum tempo. Mas, em 2014, o nosso crescimento subsequente exigiu uma solução diferente. A Uber tinha mais de 100 engenheiros. A equipe de engenharia do Android cresceu em tamanho, de um para oito engenheiros. Como nosso número de engenheiros estava crescendo, também cresceu a nossa base de código.

Demos uma olhada na direção em que estávamos indo e percebemos que, se não mudássemos, iríamos encontrar os seguintes problemas:

  • Builds demoradas: Nosso projeto inicial do Eclipse usava o Ant como sua ferramenta de construção padrão, que tem uma tendência a desacelerar ao lidar com grandes bases de código.
  • Acoplamento de recursos: Uma desvantagem de compartilhar código facilmente é que às vezes ele pode ser compartilhado em demasia. À medida que o número de recursos do aplicativo Android aumentou, preocupamo-nos com o fato de que os recursos também começariam desnecessariamente a acoplar.
  • Mestre quebrado: Quantas vezes você rebased/rebaseou sua mudança para o mestre mais recente, atingiu uma falha de construção… e depois passou uma hora depurando-a. Então, você percebeu que sua falha de construção não tinha nada a ver com seu próprio código e tudo a ver com a pessoa rebasing seu código sem reexecução de testes bem diante de você! Se você é como eu, tem se encontrado em ambos os lados dessa equação. Com vários engenheiros contribuindo para a mesma base de código e sem investir pesadamente em ferramentas de integração contínua, você arrisca que as pessoas implementem diffs ao mesmo tempo, caindo em conflito umas com as outras. Isso significa um mestre quebrado e horas desperdiçadas.

Então, durante o período de tempo entre 2013 e 2014, fizemos uma série de mudanças para a transição para uma base de código multirepo, a fim de antecipar esses problemas. Em 2013, mudamos o nosso aplicativo do passageiro do Eclipse com o Ant para o IntelliJ com o Maven, o que nos permitiu puxar artefatos de um servidor e quebrar nossa base de código de biblioteca em mais de 20 repositórios menores desacoplados. Por exemplo, a rede foi movida para seu próprio repositório, mais tarde para ser puxada para os aplicativos do consumidor através de um repositório Maven em tempo de compilação. Simultaneamente, nós transicionamos nossos scripts de construção para Gradle, marcando a nossa primeira incursão em um mundo multirepo.

Uma série de caixas de papelão representam um multirepo, a organização de base de código da Uber rodou em 2013 para acomodar a nossa crescente base de usuários.

A nossa base de código multirepo da Uber consistia de várias pequenas bases de código, cada uma representando uma única e discreta ideia armazenada como artefatos que são puxados em tempo de compilação pelos aplicativos do passageiro e do motorista. Cada repositório é como uma caixa menor de código, com seu próprio projeto IDE, repositório git e script de construção. Movendo-se para multirepo, nós asseguramos uma arquitetura sólida, de futuro garantido, contornando problemas como longos tempos de construção, acoplamento de recursos e mestre quebrado.

Então a questão agora é: por que não começamos com um multirepo desde o início? Em uma palavra: sobrecarga. Quebrar recursos em seus próprios repositórios requer uma quantidade significativa de tempo e experiência para configurar. Requer um conhecimento aprofundado tangenciando várias áreas, como Maven, Gradle, VPNs e gerenciamento de artefatos. Esse conhecimento adiantado só se torna útil à medida que a escala da empresa aumenta.

Por quase três anos nós operamos, escalamos e prosperamos com uma organização multirepo. Mas, até 2016, nossa configuração multirepo começou a atingir seus limites e nossos desenvolvedores tropeçaram nos seguintes novos problemas:

  • Arquitetura silos: Com forte dissociação de recursos, a arquitetura silos começou a se formar. Nós construímos um sistema uniforme lint no início que impediu silos estilísticos, mas isso não fez nada para impedir equipes diferentes de usar uma grande variedade de padrões de Atividades e Fragmentos, à arquitetura MVC e nossa própria arquitetura caseira. Em algum nível, silos arquitetônicos são esperados e até mesmo bons: os engenheiros devem ser capazes de escolher uma arquitetura que se encaixa em seu caso de uso. Mas à medida que escalamos, nossa equipe começou a trabalhar entre bibliotecas cada vez mais. Isso, por sua vez, significava aprender novas arquiteturas em uma base regular, o que significava uma curva de aprendizado íngreme e consistente. Como um corolário, a arquitetura de uma biblioteca específica implementada incorretamente poderia tornar a integração com os aplicativos do consumidor ou outros recursos difícil, ou mesmo impossível sem refatoração de código significativa.
  • Inferno da dependência: Ao longo do tempo, nosso gráfico de dependência cresceu em complexidade e, eventualmente, construímos uma ferramenta para garantir que os novos diffs não estavam causando mudanças de quebra. Essa solução definitivamente reduziu os efeitos do inferno da dependência, mas dependendo de quantas bibliotecas sua mudança afetou, mesmo executando a ferramenta sobre uma base de código poderia ser frustrantemente longo. Além disso, corrigir quaisquer problemas encontrados poderia exigir dias de trabalho de engenharia: identificar a dependência do problema, corrigir o código ofensivo e cortar novas versões dos repositórios afetados.
  • Longos tempos de construção: Nosso tamanho de base de código começou a atingir os limites do que o Gradle poderia construir rapidamente. Uma nova construção de aplicativo pode demorar mais de 15 minutos; uma série de pequenos ajustes XML poderia cumulativamente desperdiçar horas de tempo de construção ao longo de um dia de desenvolvimento.

Então, o que nós fizemos?

A resposta aos nossos problemas: investir em um monorepo, um único repositório contendo projetos múltiplos independentes. Um monorepo existe em uma base de código, assim como o nosso aplicativo inicial do passageiro. Mas, ao contrário desse aplicativo, essa nova instanciação de todo o código em uma caixa continha vários componentes lógicos que operam de forma independente. Assim, podemos agora investir o tempo e os recursos em ferramental e arquitetura necessários para corrigir muitos inconvenientes de um repo grande e único:

  • Suporte para IDE: O Android Studio é agora considerado a plataforma leve de desenvolvedor Android padrão, mas dificilmente é a única. Com um monorepo grande, descobrimos que IntelliJ funciona muito bem para o nosso tamanho atual – com alguns ajustes tornados possíveis pelo fato de IntelliJ ser de código aberto.
  • Builds demoradas: A Uber recentemente mudou de Gradle para Buck, que é um sistema de construção modular. Buck era fácil de integrar porque nosso código já estava dividido em componentes discretos. Nosso plugin de Gradle desenvolvido em casa, OkBuck, nos deu uma transição suave que resultou em tempos de construção de mais de quinze minutos, um aplicativo reduzido a menos de cinco minutos para uma nova construção e menos de um minuto para uma construção incremental.
  • Mestre quebrado: Nós recentemente introduzimos um sistema chamado Submit Queue que rebases alterações no mestre e executa um conjunto personalizável de testes antes de fundi-los. Isso impede que engenheiros empurrem código que quebra a construção, mantendo o mestre absolutamente limpo para todos os outros que o estejam usando.

Isso pode parecer um monte de sobrecarga, e é. Não há nenhuma maneira pronta para uso de criar um monorepo, porque há somente um punhado das companhias agora que têm a escala que o exige. Mas, para cada ponto de dor que previmos em 2013, agora temos tempo, experiência e recursos para construir ferramentas para evitar que prejudiquem nossa produtividade.

Como você pode ver, a Uber levou muitos anos para chegar a essa fase mais recente de desenvolvimento. Quando éramos uma equipe pequena e desconexa de apenas alguns engenheiros, não tínhamos tempo ou recursos para criar Submit Queue ou configurar Buck. Mas a nossa antecipação incentivou as decisões arquitetônicas que nos permitiram escalar tão rapidamente como temos feito. Agora que temos escalado ainda mais, podemos investir em um adiantamento de desenvolvimento para garantir que o futuro crescimento do serviço seja integrado e eficiente.

A produtividade do desenvolvedor é um trabalho difícil, mas importante e, com cada melhoria adicional, não estamos apenas aprimorando a Uber, mas a comunidade Android em geral. Se o crescimento do monorepo Android da Uber soa interessante para você, considere se juntar à nossa equipe de engenharia móvel – estamos contratando!

***

Este artigo é do Uber Engineering. Ele foi escrito por Aimee Lucido. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em:https://eng.uber.com/android-monorepo/.