Desenvolvimento

5 nov, 2018

Por que decidimos reescrever o aplicativo do motorista Uber

Publicidade

Este artigo é o primeiro de uma série sobre como a equipe de engenharia móvel da Uber desenvolveu a versão mais recente do aplicativo do motorista: codinome Carbon – um componente essencial do nosso negócio de compartilhamento de viagens.

Entre outros novos recursos, o aplicativo permite que nossa população de mais de três milhões de motoristas parceiros encontrem tarifas. Obtenha rotas e acompanhe seus ganhos. Começamos a projetar o novo aplicativo em conjunto com o feedback de nossos motoristas parceiros em 2017 e começamos a implementá-lo para produção em setembro de 2018.

No início de 2017, a Uber tomou a decisão de reescrever o aplicativo do motorista. Esse é o tipo de decisão que Joel Spolsky, CEO da StackOverflow, uma vez chamou de “o pior erro estratégico que qualquer empresa de software pode cometer”.

As reescritas são incrivelmente arriscadas – exigem muitos recursos e demoram muito para oferecer benefícios tangíveis aos usuários. Para essa reescrita em particular, centenas de engenheiros contribuíram com alguma capacidade, sem falar em designers, gerentes de produto, cientistas de dados, operações, jurídico e marketing. Na prática, nossa reescrita levou um ano e meio para ser implementada e lançada globalmente.

Nosso caso é um exemplo extremo de uma pergunta que os engenheiros de todas as organizações enfrentam. Se você é um engenheiro que está trabalhando para uma startup e está pensando em reescrever algum código ou recurso, talvez pergunte: “Quanto de nossa pista estamos queimando?”.

Se você estiver trabalhando em uma pequena equipe em uma grande organização, pode perguntar: “Essas mudanças valem os recursos que não estamos construindo?”. Um bom engenheiro e uma boa equipe analisarão essas questões mais amplas antes de aceitarem o desafio de uma reescrita.

Assim, embora o processo de reescrita envolvesse várias decisões técnicas importantes (a serem abordadas em artigos futuros), a decisão de reescrever envolveu uma combinação de considerações técnicas e de questões de negócios mais amplos. Mesmo que essas perguntas sejam difíceis de responder, as boas respostas às perguntas acima ajudarão você a justificar uma reescrita para sua organização ou equipe.

Em última análise, essas decisões não são feitas no vácuo. Não tomamos a decisão de reescrever o aplicativo como resultado do pensamento arquitetônico teórico (“nosso código pode ser melhor, se apenas nós …”), mas sim como resultado de um processo de pesquisa intensivo de três meses que envolveu centenas de páginas de documentação e amplo buy-in interorganizacional.

Nas seções a seguir, discutimos nossa decisão de reescrever o aplicativo do motorista da Uber e o que descobrimos como resultado desse processo.

Configurando o palco

A necessidade de uma reescrita nem sempre decorre naturalmente do simples reconhecimento da necessidade de uma nova arquitetura. As reescritas são caras e, embora as organizações de engenharia geralmente desejem reescrever o código, há outras demandas no tempo dos engenheiros que não envolvem reescrever os mesmos recursos repetidas vezes com estruturas arquitetônicas mais brilhantes. Para o aplicativo do motorista, havia três tendências que ajudaram a impulsionar a decisão de uma reescrita:

Dívida técnica

Para começar, havia uma dívida técnica real no próprio aplicativo do motorista. Essa dívida foi resultado do ritmo acelerado do crescimento da Uber, bem como da alteração dos requisitos do produto (discutidos na próxima seção).

Além disso, a dívida de tecnologia surgiu do desejo de consertar as dívidas de tecnologia anteriores: o aplicativo em si estava atolado em várias migrações em andamento, que faziam com que os recursos parecessem cada vez mais complicados.

Também vale ressaltar que a dívida de tecnologia que existia no aplicativo do motorista no momento não era teórica. Vimos um impacto real nos negócios como resultado de interrupções contínuas e custos de manutenção em termos de acertos de produtividade do desenvolvedor.

No final de 2016, tivemos que pausar o desenvolvimento no aplicativo para corrigir várias regressões de recursos. Até resolvermos esses problemas, tornou-se difícil implementar e lançar novos recursos.

Qualquer interrupção para o nosso aplicativo do motorista é um grande problema, pois os usuários contam com esse aplicativo para ganhar a vida. Em nosso mundo, nada menos que 99,99% de tempo de atividade não é aceitável, mas estamos enviando regularmente criações que experimentaram grandes regressões nos principais fluxos do aplicativo.

Desafios do produto

Um dos maiores problemas que enfrentamos com a versão anterior do aplicativo do motorista era que o produto não estava sendo bem dimensionado para novos casos de uso de negócios.

Embora a iteração mais antiga do aplicativo do motorista da Uber tenha sido projetada para viagens simples do UberX, nossos serviços aumentaram para incluir o Uber Pool, o Uber Eats e também experiências específicas do mercado, como viagens pagas em dinheiro, entre outras.

Além de fazer viagens, descobrimos que os motoristas precisavam de recursos adicionais para gerenciar suas próprias finanças e questões de negócios pessoais. Por exemplo, a transparência de ganhos e classificações é fundamental para a experiência do motorista, e algo que foi subinvestido nas primeiras iterações do aplicativo do motorista da Uber. À medida que ampliamos a experiência do produto, precisamos fornecer espaço para esses tipos de recursos.

Figura 1: Em nosso aplicativo do motorista anterior, as guias na parte inferior ficaram esticadas além da intenção original (esquerda). A exibição do mapa também ficou sobrecarregada com sobreposições e anotações de recursos que não prevíamos inicialmente (à direita).

Tomamos algumas medidas iniciais para atenuar essas questões em 2015 e 2016, enviando uma iteração do aplicativo. Infelizmente, incluímos peças da interface do usuário para diferentes equipes construírem, em vez de projetar em torno de necessidades e fluxos de trabalho dos motoristas.

Se você consultasse nossa interface do usuário nessa época, veria uma guia para ganhos, uma guia para classificações, uma guia para configurações e uma guia inicial para todos os outros recursos.

O intervalo de todos os outros recursos tornou-se maior, e as guias de ganhos e classificações foram frequentemente reaproveitadas/adaptadas para recursos para os quais eles não tinham sido originalmente projetados.

As lições que aprendemos com essa iteração do aplicativo, juntamente com nossa visão de longo prazo sobre o produto, na verdade já nos levaram a repensar completamente como o aplicativo do motorista deveria se apresentar aos nossos parceiros-motorista. Mesmo que uma reescrita não fosse inevitável, o redesenho era.

Alinhamento de engenharia

Nossa equipe de engenharia já havia feito investimentos em uma nova direção. Em particular, com a reescrita do aplicativo do motorista em 2016, introduzimos uma nova arquitetura móvel, que chamamos de RIBs (uma variação do VIPER), para nos ajudar a lidar com a nossa escala crescente.

Ele forneceu soluções para a maioria dos problemas que reconhecemos no aplicativo do motorista: uma framework para pontos de extensão escaláveis, uma estrutura de aplicativo convincente e um modelo de gerenciamento de memória eloquente. Lançamos a arquitetura RIBs para a comunidade de código aberto em 2017.

Enquanto a arquitetura RIBs certamente melhorou nosso aplicativo do motorista, ele também representou uma nova direção de engenharia para a nossa organização móvel. Os investimentos futuros do nosso principal grupo de plataformas envolveriam principalmente melhorias nos RIBs. O suporte a vários aplicativos com diferentes arquiteturas seria mais caro do que padronizar os RIBs.

Fazendo nossa decisão

Dado o contexto de um novo design da interface de usuário e uma nova arquitetura, tínhamos essencialmente três opções diferentes sobre como proceder: redesenhar o aplicativo do motorista sem RIBs; migrar o aplicativo do motorista existente para a arquitetura RIBs; ou fazer uma reescrita completa do aplicativo com base em RIBs.

Arquitetura sem RIBs

A primeira abordagem que analisamos foi uma reformulação/novo design sem RIBs. A razão pela qual consideramos essa primeiro foi que sabíamos que a migração para os RIBs exigiria muitos recursos.

Os RIBs vêm com várias novas bibliotecas, mas também uma nova abordagem para criar aplicativos com uma estrutura de escopo hierárquico que desvincula a lógica de negócios da lógica de apresentação. A arquitetura RIBs oferece um sistema de gerenciamento de memória eloquente, mas altamente opinativo.

Primeiro, consideramos se o aplicativo existente poderia ou não lidar com as principais mudanças de produto que estávamos considerando. O que descobrimos é que, como nosso aplicativo suportava uma pequena variação na contenção do controlador de visualização, grande parte da nossa lógica de negócios estava fortemente acoplada à apresentação do View. Isso significou que um novo design da interface de usuário envolveria, inevitavelmente, muitas mudanças na lógica de negócios.

Em segundo lugar, conforme discutido anteriormente, a arquitetura do aplicativo do motorista existente tinha problemas que precisavam ser resolvidos. Esses problemas, em parte relacionados à própria lógica do aplicativo, que em alguns lugares (particularmente no Android), se tornaram um padrão muito comum aos desenvolvedores de dispositivos móveis: uma versão diferente do MVC, um Massive View Controller, onde a maioria do nosso código principal estava contido em um arquivo controlador multi-mil linhas.

Consequentemente, não estávamos dispostos a criticar a arquitetura móvel existente, que estava se tornando cada vez mais complicada e difícil de desenvolver.

Por fim, mesmo que a arquitetura do aplicativo do motorista antigo que possuíamos fosse perfeita, talvez ainda faça sentido, do ponto de vista estratégico, adotar RIBs para evitar uma situação na qual dividimos as arquiteturas de aplicativos na Uber.

Com uma arquitetura única e convincente, nossos investimentos em nível de plataforma tornaram-se duas vezes mais valiosos e o código escrito em uma parte da organização (por exemplo, passageiro) poderia ser reutilizado em outra parte (por exemplo, motorista).

Se íamos adotar RIBs de qualquer maneira, como faríamos isso?

Migração

Muitas organizações preferem migrações cuidadosamente estimuladas, permitindo que continuem o desenvolvimento de recursos enquanto a arquitetura subjacente de um sistema muda. Embora perfeitamente válido na maioria dos casos, no passado descobrimos alguns problemas com essa abordagem na Uber.

Primeiro, analisamos as 10 principais migrações móveis que tentamos na Uber nos últimos anos e descobrimos que elas tinham uma alta taxa de falhas. Ou seja, começaríamos a migrar uma determinada biblioteca subjacente, mas não o faríamos inteiramente.

Novos recursos foram criados com a nova biblioteca e alguns recursos antigos foram migrados, mas tínhamos um código legado que ainda era executado na base de código.

Depois de mais investigações, descobrimos que a causa raiz de grande parte da nossa dívida de tecnologia dentro do aplicativo do motorista era o resultado de tais migrações. Por exemplo, tínhamos condições de corrida porque nosso modelo de pub/sub de aplicativo foi bifurcado no Android.

Nossa principal estrutura de aplicativos, que começou a usar fragmentos no Android, foi parcialmente migrada para uma framework interna. Esse tipo de migração incompleta levou a camadas de adaptador e confusão geral do desenvolvedor. Esses impulsos arquitetônicos incompletos acabariam levando a interrupções que impactavam diretamente nossos usuários.

Segundo, muitas vezes descobrimos que as migrações criam uma grande quantidade de instabilidade enquanto elas acontecem. Tivemos inúmeras interrupções causadas por migrações que foram destinadas a melhorar as frameworks de aplicativos subjacentes, como o nosso protocolo de rede. Estes tecnicamente não devem ter impacto imediato e tangível em nossos usuários, mas acabaram quebrando os principais recursos do aplicativo.

Finalmente, até mesmo a promessa de desenvolvimento contínuo de recursos foi, em nossa experiência, muitas vezes não cumprida. Se uma equipe dependesse de uma migração em andamento, ela geralmente seria bloqueada até que a migração fosse concluída. Isso também levou a um mundo em que reverter uma migração geralmente significaria que também precisaríamos reverter vários recursos.

Consequentemente, à medida que avaliamos se deveríamos ou não avançar com uma reformulação completa do produto e a adoção da arquitetura RIBs, o risco de uma migração incompleta ou intermináveis camadas e fachadas de adaptador que aumentam muito a instabilidade da aplicação era alto demais.

Reescrever

Até certo ponto, tomamos essa decisão por negação (as outras opções, sem arquitetura RIBs e migração, não eram sustentáveis), mas houve benefícios positivos em uma reescrita que aumentou nossa confiança na decisão final.

Primeiro, uma reescrita iria desbloquear nossa capacidade de trabalhar em um redesenho do aplicativo sem ser limitado por entendimentos pré-definidos de como ele já estava construído. Isso significava que seu design estava aberto a uma gama maior de fluxos possíveis.

Em segundo lugar, optar por reescrever o aplicativo significava que nossa arquitetura seria muito mais limpa, já que derivaria de uma direção estratégica convincente desde o início. Se tivéssemos escolhido uma migração, provavelmente estaríamos presos ao código legado que reutilizamos por oportunismo ou conveniência.

Em terceiro lugar, reescrever o aplicativo nos permitiu voltar à prancheta de desenho e pensamos mais detalhadamente em que direção queríamos que o produto fosse inserido. Como resultado, certas frameworks principais do aplicativo acabaram sendo reescritas.

Para um engenheiro, uma reescrita é uma oportunidade de fazer um trabalho incrível e ficamos empolgados em começar.

Conclusão

Vale ressaltar que nossa decisão de reescrever nosso aplicativo de motorista não se baseou na ideia especulativa de que “seria melhor se pudéssemos fazer tudo de novo”. Na verdade, alguns engenheiros podem se surpreender ao ouvir isso, mesmo depois de uma reescrita, enviamos um aplicativo não apenas com novos recursos e uma nova arquitetura, mas também com um pouco de novas dívidas técnicas.

Ou seja, você nunca consegue essas coisas perfeitas. Um engenheiro que lê este artigo deve hesitar em chegar à conclusão de que “as migrações nunca funcionam, as reescritas levam a um código perfeito”. Em vez disso, é importante reconhecer que a decisão de reescrever vem dentro do contexto de necessidades de organização, negócios e técnica muito tangíveis.

Se não tivéssemos criado uma nova arquitetura móvel nos meses anteriores, talvez não tivéssemos reescrito o aplicativo. Se não tivermos uma equipe de produtos disposta a pesquisar a decisão, talvez não tivéssemos reescrito. Se as migrações na Uber tendiam a ser mais bem sucedidas no passado, talvez não tivéssemos reescrito.

Certamente, o que nos levou a reescrever não foi que as reescritas fossem inerentemente boas ou, em geral, uma boa ideia.

Em vez disso, a reescrita do aplicativo do motorista surgiu no contexto do desejo de criar uma experiência de produto mais confiável e mais forte para nossos usuários e, ao mesmo tempo, ampliar a capacidade da nossa organização de executar essa visão.

Esse processo de tomada de decisão talvez seja menos estimulante do que simplesmente o desejo de inventar a próxima melhor camada de abstração, mas também é o cálculo de decisão que impulsionou a criação de um aplicativo móvel bem-sucedido e totalmente aprimorado.

Interessado em desenvolver a próxima geração de aplicativos móveis? Considere se juntar à nossa equipe!

***

Este artigo é do Uber Engineering. Ele foi escrito por Nandhini Ramaswamy e Adam Gluck. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em: https://eng.uber.com/rewrite-uber-carbon-app/