https://a.storyblok.com/f/270183/144315/15e3a7840b/part-two.png

Melhore seu projeto de software – Parte II: Como fazer alterações

Publicado em November 28, 2022

Tempo de leitura: 15 minutos

Você já assumiu o controle de uma base de código e percebeu que não está satisfeito com a forma como o código está escrito ou organizado? É uma situação comum, mas que pode causar muitas dores de cabeça. A dívida técnica pode crescer como uma bola de neve, tornando exponencialmente mais difícil entender o código e adicionar novos recursos.

Nesta série de três partes, vou abordar alguns dos principais passos que você vai querer seguir para ficar mais satisfeito com seu (antigo) projeto. Para dar alguns exemplos concretos, vou integrar tudo explicando como refatorei e aprimorei o SDK do Vonage em Python de código aberto SDK do Vonage para Python, uma biblioteca que faz chamadas HTTP às APIs da Vonage, mas os princípios se aplicam a qualquer tipo de projeto de software.

Os exemplos desta postagem serão escritos em Python, mas esses princípios se aplicam a projetos em qualquer linguagem. Há também uma lista de verificação útil a ser seguida se você estiver tentando corrigir especificamente um projeto em Python.

A série, em seções

  1. Parte 1: Compreendendo uma base de código

  2. Parte II: Fazendo mudanças (este artigo)

  3. Parte III: Aprimoramentos de nível avançado

O que a Parte II aborda?

Na Parte II, falaremos sobre:

  • Ganhar confiança para fazer mudanças e lidar com a dívida técnica

  • Como construir confiança com seu chefe, sua equipe e seus usuários

Ao final do artigo, você estará pronto para melhorar seu projeto, mantendo as pessoas envolvidas satisfeitas.

Vamos começar!

Consertando a estrutura

Se você acompanhou a Parte Um desta série, você já deve ter uma boa compreensão do projeto do qual agora é responsável, além de saber como a base de código (e os testes, se tiver a sorte de contá-los) está estruturada e qual é a finalidade dessa estrutura. Agora é um ótimo momento para refletir sobre a arquitetura geral e começar a reestruturar o projeto de forma mais lógica, caso você ache que ele possa ser aprimorado.

Um exemplo concreto

O projeto que estou usando como exemplo é o SDK do Vonage para Python, que permite ao usuário acessar diversas APIs da Vonage. No meu caso, a macroestrutura do meu projeto era assim:

Image of the original structure of the Vonage Python SDK

Como você pode ver na figura acima, o código foi dividido em seis módulos principais (arquivos únicos — sim, o Python é bem compacto!):

  • Um __init__ módulo, geralmente um arquivo mínimo usado para empacotar o código

  • Um _internal módulo que continha métodos internos (e também uma das APIs que oferecemos suporte, por algum motivo)

  • sms, voice e verify módulos, cada um contendo código para chamar uma única API

  • Um errors módulo, que continha todos os erros personalizados

Minha primeira pergunta ao analisar isso foi: onde estão as outras APIs? Quando cheguei à Vonage, o SDK oferecia suporte a 12 APIs diferentes (adicionamos mais desde então!), mas havia apenas três módulos relacionados a essas APIs.

Descobriu-se que cerca de 90% do código estava, na verdade, dentro do __init__.py arquivo, incluindo implementações de muitas APIs, métodos obsoletos, a lógica para tarefas como geração de JWT, as solicitações básicas de API e outras configurações opcionais... o que era excessivo para um arquivo que deveria ser bem minimalista. Decidi refatorar isso criando uma Client classe que lidasse com a lógica central e, em seguida, classes com nomes de APIs (com um pequeno agrupamento lógico).

Acabamos com uma estrutura que ficou assim:

The structure of the source code, split up into the function each module serves

Fazer isso por conta própria

Analise a estrutura do seu projeto. Pergunte a si mesmo: será que eu poderia melhorar isso? Agrupe funcionalidades semelhantes em módulos que sejam pequenos o suficiente para serem compreendidos com facilidade, mas não menores do que isso — você ainda precisa conseguir ter uma visão geral do que o projeto faz.

Também recomendo criar classes para agrupar métodos semelhantes, bem como erros personalizados para cada classe, a fim de proporcionar a você e aos seus usuários mais clareza sobre o que está dando errado quando ocorrem falhas.

Filiais

Quando você inicia um novo projeto, provavelmente ele não estará no estado “concluído”. Os projetos de software são sempre um trabalho em andamento, e as melhorias iterativas constantes são uma das principais razões pelas quais usamos o controle de versão. Nessa situação, você pode perceber que há ramificações no seu sistema de controle de versão que contêm funcionalidades, aprimoramentos ou correções de bugs.

Em muitos casos, a situação ideal é a seguinte: um único ramo principal, com os recursos sendo desenvolvidos em ramos secundários dedicados a cada recurso, que depois são mesclados de volta ao ramo principal.

Main branch with branches created from it to deliver features - a "good" case

Em alguns projetos, pode fazer sentido manter um branch beta por mais tempo, caso haja um recurso beta que você queira testar e manter separado da base de código principal. No SDK Python da Vonage, atualmente temos um ramo beta com código que chama nossa Video API, que está em fase beta. Não queremos mesclar esse ramo, pois a API que ele chama ainda não foi lançada oficialmente. Nesse caso, sua estrutura de ramos pode ficar mais parecida com isto:

Main branch and a long-lived beta branch, both of which have features developed and merged.

Caso você precise desenvolver funcionalidades separadamente, pode ser uma boa ideia fazer o rebase do branch beta periodicamente para incorporar as alterações do branch principal — é isso que eu faço com o SDK do Vonage para Python.

Esses são os casos “bons”. Mas, quando você começa a trabalhar em um projeto, a situação pode ser mais parecida com esta:

A branch structure that needs to be simplified

Ou seja, há ramificações demais e falta de consistência. Normalmente, as coisas acabam ficando assim quando vários desenvolvedores trabalham em muitos recursos ao mesmo tempo; embora também possa ser o caso de um engenheiro anterior ter usado novas ramificações para planejar várias tarefas e experimentar coisas novas, mas nunca ter conseguido concluí-las.

Nessa situação, esses branches podem ser uma ótima fonte de informações sobre como o responsável anterior pelo projeto o conduzia, além de servirem como fonte de inspiração — alguns dos recursos ou correções podem ser bons o suficiente para você mesmo implementar. No entanto, assim que você entender o que um ramo extra contém, geralmente é uma boa ideia fechá-lo — o objetivo é ter o máximo possível de liberdade para implementar suas próprias ideias.

A cerca de Chesterton — Entenda o que você está removendo!

Parece que agora é um bom momento para lembrar a todos (incluindo a mim mesmo) sobre A Cerca de Chesterton — o aforismo que diz que não se deve remover algo sem entender por que isso foi colocado ali em primeiro lugar. Esforce-se para entender o propósito de um ramo ou de uma parte do código antes de removê-lo! E o mais importante: não remova algo só porque você não sabe por que está ali; tente entender primeiro.

Escolhendo as dependências corretas

Quase todo projeto de software depende do código de outras pessoas. A beleza do código aberto está no fato de que muitos problemas e desafios já foram resolvidos antes mesmo de seu código existir, por pessoas muito inteligentes que estão dispostas a compartilhar seu trabalho com você. No entanto, é importante entender quais são as dependências do seu código, para que servem e se são realmente as melhores ferramentas para o trabalho.

Uma dependência que era adequada no passado também pode se tornar menos adequada com o passar do tempo, o que nos leva a fazer a seguinte pergunta:

O que caracteriza uma boa dependência?

Ao decidir manter (ou substituir) uma dependência, esta seção deve ajudá-lo a avaliar se vale a pena utilizá-la.

Primeiro, analise a licença. Certifique-se de que você tem permissão para usar a dependência no seu projeto. Se não tiver, a resposta é não. Se você não tiver certeza do que estou falando quando digo isso, este artigo da Snyk deve ajudar a explicar o que você deve observar ao avaliar a licença de uma dependência.

Em seguida, a dependência é mantida ativamente? É possível consultar o histórico de commits de projetos de código aberto. Alguns são tão simples que exigem pouquíssima manutenção, e isso é aceitável. Projetos mais complexos devem ser mantidos ativamente, ou seja, deve haver commits frequentes, e os issues e PRs não devem ficar ignorados por muito tempo. Isso é importante, pois você quer que a dependência ofereça suporte a novas versões da linguagem e seja compatível com novos recursos e outras dependências que você estiver usando.

Vale a pena levar em conta a popularidade da dependência. Todas as dependências podem apresentar vulnerabilidades de segurança, e quanto mais amplamente utilizada for uma dependência, maior será a probabilidade de que essas vulnerabilidades sejam identificadas por ferramentas automatizadas, pesquisadores de segurança e outros usuários, aumentando assim a chance de que quaisquer vulnerabilidades sejam corrigidas rapidamente, mantendo seus usuários seguros. Escolher uma opção amplamente utilizada também significa que um número maior de pessoas poderá responder às suas dúvidas caso você tenha alguma dificuldade!

Por fim, considere os recursos de apoio — a documentação é clara? Existem exemplos confiáveis na internet que utilizam essa dependência? Esses aspectos podem ajudar você a dar os primeiros passos ou a solucionar problemas rapidamente, por isso vale a pena levá-los em consideração.

Então você encontrou o conjunto perfeito de dependências? Ótima notícia... por enquanto. Uma biblioteca bem mantida que atenda às suas necessidades pode não continuar assim — os mantenedores podem abandonar o projeto ou seus requisitos podem mudar, tornando-a inadequada. O melhor conselho que posso dar é: continue avaliando suas dependências ao longo do tempo! Certifique-se de que elas ainda atendam às suas necessidades. E nunca se sabe: uma nova biblioteca pode surgir no futuro que atenda às suas necessidades de forma ainda mais perfeita.

Testando código que não é de sua autoria

Se o seu projeto precisar se comunicar com outro código, surge o problema complexo de como testar a funcionalidade do seu código. No nosso exemplo, a principal tarefa do SDK do Vonage para Python é chamar diversas APIs. Não temos controle sobre as próprias APIs, nem sobre como elas se comportam.

Um exemplo disso é quando você usa o SDK para chamar SMS API da Vonage. Muitas coisas podem acontecer aqui, dependendo das suas necessidades específicas, mas vou dar um exemplo concreto. Enviar um SMS com o SDK do Python é um processo bastante simples:

import vonage

client = vonage.Client(key="API_KEY", secret="API_SECRET")
client.sms.send_message(
    {
        "from": "SENDER_NAME"
        "to": "RECIPIENT_PHONE_NUMBER",
        "text": "A text message sent using the Vonage SMS API",
    }
)

Mas, depois que você executar esse código, várias coisas acontecem. Aqui está uma versão (muito) simplificada:

  1. O SDK envia uma solicitação a um servidor da Vonage

  2. O servidor analisa a solicitação e envia uma resposta de volta ao SDK.

  3. Se a solicitação for bem-sucedida, o servidor envia um SMS para o número de telefone do destinatário

  4. As informações de status (por exemplo, confirmação de entrega) são enviadas de volta ao servidor da Vonage

  5. Opcionalmente, essas informações são enviadas de um servidor da Vonage para um webhook especificado pelo usuário, para que ele possa verificar se a mensagem SMS foi entregue com sucesso.

The (simplified) chain of events that happen when you want to send an SMS with our codebase

Nessa situação, a execução do nosso código faz com que uma grande quantidade de outro código seja executada. Não podemos testar esse outro código a partir do SDK; portanto, precisamos partir de uma suposição ao escrever nossos testes: se enviarmos as informações corretas, faremos com que as ações certas ocorram e receberemos as respostas corretas de volta.

No nosso exemplo, a única parte do código que podemos testar é esta:

The only part of the above workflow we can test

Portanto, os únicos critérios pelos quais nosso código pode ser avaliado são: 1. as solicitações que enviamos estão bem formadas? e 2. as respostas são tratadas de maneira adequada?

Em resumo, limite seus testes a verificar apenas como seu código envia e recebe dados, e presuma que o outro software cumpre sua função corretamente, enviando respostas de sucesso e de erro para que seu código as processe. Ao testar, considere capturar as solicitações de API e retornar respostas simuladas no formato correto para a solicitação, para que seu código possa consumir e processar esses dados simulados ao executar seus testes, sem precisar chamar de fato os serviços externos que você não pode controlar.

Lembre-se de que seus testes comprovam que seu código está em conformidade com as especificações do seu conjunto de testes, não necessariamente com o comportamento real da API. Ao adicionar um novo recurso, é uma boa ideia criar um aplicativo de demonstração que realmente use seu código para fazer solicitações em tempo real, além de apenas executar seus testes (essa abordagem também fornece uma ótima resposta para armazenar em cache e usar em seus testes no futuro!)

Lançando sua primeira versão

Então, você organizou o código-fonte, fez algumas reestruturações, aprimorou seus testes e talvez até tenha adicionado novos recursos ao seu projeto. É hora de lançar uma nova versão!

Esse processo tem tudo a ver com construir confiança — mostrar aos seus usuários e à sua equipe que você sabe o que está fazendo. O objetivo é demonstrar aos usuários que eles podem confiar nas suas alterações e que a atualização não prejudicará nenhuma parte do fluxo de trabalho deles sem que eles saibam. A transparência é fundamental aqui — você não quer causar surpresas desagradáveis aos seus usuários.

Utilize o controle de versão semântico

A recomendação mais importante seria esta: use o controle de versão semântico! Com o versionamento semântico, você segue uma estrutura x.y.z para seus números de versão, em que:

  • x é a versão principal, que você deve aumentar ao fazer alterações que causam incompatibilidade,

  • y é a versão secundária, que você deve aumentar ao adicionar funcionalidades compatíveis com versões anteriores, e

  • z é a versão do patch, que você deve aumentar ao corrigir um bug, atualizar uma dependência etc., de forma compatível com versões anteriores.

Para dar um exemplo prático, ao adicionar suporte para a Messages API for Vonage (sem alterar nada que tornasse a nova versão incompatível com versões anteriores), lancei uma versão secundária, passando de v2.7.0 -> v2.8.0. Quando quis remover alguns métodos obsoletos e alterar a forma como os objetos são instanciados no SDK, sabia que isso prejudicaria a funcionalidade existente, então lancei uma versão principal, v2.8.0 -> v3.0.0. Quando encontrei um bug no novo código, corrigi-o e atualizei o SDK com uma versão de correção, v3.0.0 -> v3.0.1. Seguir essas regras permite que o usuário saiba exatamente o que esperar ao atualizar para a versão mais recente. Se você lançar uma versão principal, ele saberá que deve esperar uma mudança que quebra a compatibilidade!

Atualizar materiais complementares

Sempre que lançar uma nova versão, atualize o registro de alterações, para que o usuário saiba o que esperar. Geralmente, esse é o primeiro lugar que o usuário consulta, pois normalmente detalha todas as diferenças da nova versão e pode ajudá-lo a decidir se deve atualizar. Se você não atualizar o registro de alterações, os usuários não saberão o que esperar da nova versão e, consequentemente, ficarão muito mais cautelosos.

Da mesma forma, ao lançar uma nova versão, mantenha a documentação, os arquivos README e os exemplos de código atualizados com a versão mais recente. Se você não fizer isso, seus usuários não saberão necessariamente como usar todos os seus novos e excelentes recursos; além disso, caso tenha feito uma alteração que cause incompatibilidade, sua documentação e seus exemplos de código podem deixar de funcionar completamente.

Lembre-se: a transparência é fundamental — seja previsível, para que os usuários de longa data do projeto possam confiar de que o código do qual dependem está em boas mãos e para que os novos usuários possam aprender a usar seu software.

Implementação gradual das funcionalidades obsoletas

Quando você começar a alterar o código, provavelmente vai querer reestruturar ou refatorar boa parte dele. Desde que isso não altere a forma como o código é chamado pelo usuário, você pode fazer isso à vontade. No entanto, se quiser alterar a forma como algo é chamado, você precisará marcar o método antigo como obsoleto e adicionar o novo em uma versão secundária; depois, remover o antigo em uma versão principal posteriormente.

Vou dar um exemplo específico aqui. No SDK do Vonage para Python, eu queria alterar a forma como o get_standard_number_insight método era chamado pelo usuário. Originalmente, esse era um método associado à Client classe. Eu queria alterar a estrutura, criando uma NumberInsight classe que contivesse esse método, de modo que a Client classe instanciava e utilizava. Isso tornaria essa forma de chamar a Number Insight API igual à maneira como um usuário enviaria mensagens SMS ou faria chamadas de voz.

The method I wanted to move from the Client class

Primeiro, criei uma NumberInsight classe e adicionei uma versão do get_standard_number_insight método a ela.

The new version of the method inside the new NumberInsight class

Em seguida, marquei o método como obsoleto na Client classe. Em Python, isso pode ser feito adicionando um decorador que exibe um aviso de obsolescência ao usuário quando ele utiliza o método.

Deprecating the method I wanted to move from this class, and the warning it prints

Depois disso, atualizei os trechos de código e a documentação para refletir a alteração.

Updated docs to show the new way to call the get_standard_number_insight method

Agora consegui lançar uma versão secundária. Após um lançamento em que se marca uma funcionalidade como obsoleta, é boa prática deixar as partes obsoletas como estão por um tempo antes de removê-las, para dar tempo suficiente para que as pessoas façam a transição. Sugiro manter a funcionalidade obsoleta por pelo menos uma versão ou um mês — o que for mais longo. Mais uma vez, tudo isso tem a ver com construir confiança.

Deixei esse código antigo por alguns meses e, depois, lancei uma versão importante na qual removi as formas antigas de chamar esses métodos. Ao ser metódico e transparente quanto às minhas intenções, meus usuários sabiam o que esperar ao atualizarem para a versão mais recente.

Changelog describing the major release that removed old deprecated methods, including the old version of get_standard_number_insight

Equilibrando melhorias e novas tarefas

O último aspecto a se levar em conta ao começar a trabalhar em uma base de código legada é que você provavelmente não terá tempo ilimitado para aperfeiçoar o código da maneira que gostaria antes de precisar adicionar novos recursos, corrigir bugs etc. Provavelmente, você terá outras prioridades com as quais lidar, como prazos para adicionar novas funcionalidades, ao mesmo tempo em que tenta fazer grandes alterações no código antigo.

Nesse caso, você precisa se sentir à vontade para defender a si mesmo e o seu trabalho. Deixe claro que parte do seu tempo precisará ser dedicada a melhorar a base de código legada, o que significa que você não será tão rápido no desenvolvimento de novos recursos quanto seu chefe gostaria. Enfatize que o tempo que você dedicar à refatoração agora o ajudará a entender a base de código e trará benefícios mais adiante, pois ela ficará muito mais fácil de manter.

Em última análise, seu trabalho em um projeto legado deve melhorar a experiência do usuário, bem como a facilidade de uso e de manutenção a longo prazo. Talvez você não receba elogios imediatos por lidar com a dívida técnica, já que o trabalho não fica visível para seus usuários ou sua equipe de imediato, mas gerenciar a dívida técnica pode impedir que a situação piore muito para todos no futuro — vale muito a pena fazer esse trabalho!

Algo que ajudou muito nesse processo foi a criação de tickets de trabalho para a fase de descoberta e aprendizado, a fim de mostrar que eu estava investindo na eficiência futura. Também criei tickets para a dívida técnica, a fim de dar à minha equipe uma visão do trabalho que eu estava realizando, juntamente com os tickets para o desenvolvimento de novos recursos. Essa abordagem me ajudou a construir confiança dentro da minha empresa e permitiu que as pessoas entendessem como eu estava gastando meu tempo; não posso deixar de recomendá-la.

E agora?

Se você seguiu as sugestões deste artigo, seu projeto já deve estar bem melhor! As versões que você lançar vão estabelecer as bases para todas as coisas incríveis que você quer fazer com seu projeto.

Clique aqui para ler a Parte 3 da série, onde explicarei maneiras de levar seu projeto a um novo patamar. Falaremos sobre melhorias que beneficiam seus usuários, bem como sobre aspectos que facilitam o trabalho com a base de código. Por fim, explicaremos as melhores práticas para a transferência de um projeto.

Tem alguma dúvida ou quer compartilhar suas ideias? Você pode entrar em contato conosco na nossa Slack da Comunidade Vonage ou nos enviar uma mensagem no Twitter.

Compartilhar:

https://a.storyblok.com/f/270183/400x400/92109caf6a/max-kahan.png
Max KahanEx-funcionários da Vonage

Max é um ex-membro da equipe da Vonage. Ele atuou como Promotor de Desenvolvedores Python e Engenheiro de Software, com interesse em APIs de comunicação, aprendizado de máquina, experiência do desenvolvedor e dança! Ele é formado em Física, mas atualmente trabalha em projetos de código aberto e cria soluções para facilitar a vida dos desenvolvedores.