5 dicas para refatorar seu código de forma eficiente

Silvério Vale • 13 de dezembro de 2023

Ponte antiga ao lado de ponte nova

Sou apaixonado pelo processo de refatoração. Já não consigo mais desenvolver sem refatorar constantemente. Mas não foi sempre assim...

A primeira vez que me deparei com o livro "Refatoração", tive muita resistência. Pensei que iria me ensinar a modernizar sistemas legados - e por que eu investiria tempo nisso quando posso aprender alguma tecnologia novinha, que acabou de sair do forno?

Anos mais tarde, quando finalmente li a obra, percebi que se tratava de algo muito maior: era um novo estilo de programação, que eu acabei por adotar no meu dia a dia. Agora, gostaria de compartilhar algumas sugestões sobre como fazer isso de maneira eficiente.

O que significa refatoração?

Muitas pessoas usam o termo para se referir a qualquer adaptação ou correção no código, mas esse não é bem o seu significado.

Martin Fowler, autor do clássico "Refatoração", a define como:

"A refatoração é o processo de modificar um sistema de software de modo que não altere o comportamento externo do código, embora melhore a sua estrutura interna." [1]

Essa distinção pode parecer apenas uma formalidade, mas vamos entender melhor como ela se aplica nos próximos parágrafos.

Por que refatorar?

Qualquer sistema minimamente complexo cresce com o tempo, adquirindo novas funcionalidades, melhorias e sofrendo correções de bugs. Para que seja possível realizar essas atividades sem transtornos, é necessário que os desenvolvedores entendam o código e consigam trabalhar com ele.

O problema é que, à medida que o sistema cresce, alguns padrões que atendiam as necessidades iniciais já não são mais adequados para o porte que o sistema adquiriu. Também é comum que alguns code smells e más práticas se espalhem pelo código. Ainda, pode ser que a equipe não seja mais a mesma, dificultando ainda mais o entendimento de código confuso.

Sendo assim, se queremos evitar que a solução caduque e se torne aquele legado inoperável, que ninguém quer encostar a mão, é necessário que refatoremos o código com frequência para manter a organização e legibilidade.

Quando refatorar?

A resposta curta é: O tempo inteiro! A refatoração faz parte do processo de desenvolvimento. Como escreveu Fowler, "A refatoração não é uma atividade separada da programação - não reservo mais tempo para ela do que reservo para escrever instruções if." [2].

Tendo dito isso, Fowler separa as refatorações em oportunas e planejadas. As oportunas são aquelas que você realiza conforme necessário, durante o processo de desenvolvimento corriqueiro. Já as planejadas são, normalmente, aquelas que envolvem partes mais abrangentes do sistemas e precisam de ser realizadas com um pouco mais de cautela, além de necessitarem de maior esforço.

Dentre as oportunas, podemos ressaltar algumas situações comuns que podem clamar por uma refatoração:

  • Refatoração preparatória: Quando queremos facilitar o acréscimo de uma nova funcionalidade;
  • Refatoração para compreensão: Quando precisamos deixar o código mais fácil de entender;
  • Refatoração para coleta de lixo: Quando você se depara com um código muito ruim, não necessariamente atrelado à sua atividade atual, e percebe a necessidade de melhorá-lo.

Como refatorar - de forma eficiente?

Existem muitos tipos de refatoração. Algumas levam o seu código do estado A para o estado B, outras do estado B para o estado A. Cabe ao desenvolvedor decidir em qual direção caminhar em cada situação. Porém, existem algumas boas práticas que são comuns a todas elas e que as tornam mais eficientes.

O segredo está em minimizar o risco envolvido. Em outras palavras, você deve adotar estratégias que reduzam a chance de que alterações na estrutura interna do código afetem a sua funcionalidade. Espero que as dicas seguintes te ajudem a fazer isso.

Dica #1: Crie testes automatizados

"A primeira base para a refatoração é ter um código autotestável." [3]

Pela própria definição de refatoração, a funcionalidade não pode ser afetada após as mudanças no código. Isso quer dizer que, se você está refatorando uma tela, a UI visível ao usuário não pode sofrer alterações. Se você está refatorando um endpoint, a request e a response devem permanecer iguais. Se você está refatorando uma função, o retorno não pode ser alterado. E por aí vai...

Mas como vamos garantir isso?

A maneira mais prática é com testes automatizados, que possam ser executados com frequência para garantir que não houve impacto no comportamento esperado daquela unidade de código.

Mas, para que isso funcione, os testes devem ser rápidos e estáveis!

Como escreveu Uncle Bob, "um teste lento é um que não será rodado" [4]. E testes que não serão executados com frequência não auxiliam no processo de refatoração. E o mesmo vale para testes instáveis. Quando os testes são instáveis, é só uma questão de tempo até o time começar a ignorar os erros ocorridos nas execuções, poluindo a suite até ela se tornar apenas um fardo. Já vi isso acontecer algumas vezes - especialmente no front end.

No contexto da refatoração, sempre que você realiza alterações no código, é importante que essas alterações sejam testadas. E é uma mão na roda possuir uma suite de testes automatizados que possam ser executados em alguns segundos e garantam que o comportamento não foi afetado, te dando a segurança para a seguir em frente.

E isso é especialmente importante para que possamos dar passos pequenos durante a refatoração, assunto que vou abordar na próxima dica.

Dica #2: Dê pequenos passos

"O segredo para uma refatoração eficaz é reconhecer que você será mais rápido se der passos minúsculos" [5]

Quem já leu o "Refatoração" cansou de se deparar com etapas de teste nos procedimentos das refatorações. Geralmente, isso é feito isso entre cada pequeno passo. O objetivo é garantir que aquele passo foi bem sucedido e saber que o que foi feito até então está funcionando.

A vantagem de se fazer isso é reduzir muito o risco e o retrabalho. Primeiro, porque pequenos passos envolvem menos riscos pelo próprio fato de serem menores e menos complexos. Depois, porque, se aparecer algum erro durante os testes, você saberá que ele foi causado pela última alteração, e não por alguma das várias anteriores.

Imagine uma refatoração complexa que envolva várias alterações. Se você realiza todas de uma vez e, ao final, percebe que a funcionalidade foi alterada, o que você irá fazer? Jogar tudo fora e começar novamente? Investigar até descobrir qual das várias alterações provocou o erro? Ambos os caminhos acarretam desperdício de tempo.

Portanto, por mais que possa parecer esforço extra, quebrar a refatoração em pequenos passos, separados por etapas de testes, acaba por prevenir muito retrabalho e desperdício. A chance de um erro ocorrer é menor e, se ocorrer, é mais fácil e rápido de se encontrar a origem.

Voltando à dica anterior, os testes automatizados se encaixam como uma luva nesse processo - uma vez que eles permitirão etapas de testes mais rápidas entre os passos, permitindo que eles possam ser ainda menores.

Suponha que você deu um pequeno passo em uma refatoração, como renomear uma variável ou extrair uma função. Se você precisar de executar o sistema e rodar testes manuais, talvez você fique tentado a pular direto para o próximo passo e adiar os testes. Se, porém, você possuir uma suite de testes automatizados que te garanta, em alguns segundos, que a funcionalidade permanece intacta, você não irá se incomodar tanto de executá-la antes de seguir em frente.

Dica #3: Comece pelo mais simples

Há algum tempo, me deparei com uma tela extremamente complexa em um sistema React. Com diversos filtros que interagiam entre si e dependiam de parâmetros query string, dentre várias outras particularidades, a tela era um emaranhado de componentes, estados e efeitos, de diversos níveis, que nenhum desenvolvedor tinha a coragem de enfrentar.

Vendo aquilo, fiquei incomodado e resolvi encarar o desafio: Fiz um modelo mental de como a tela se comportava e resolvi reescrever toda a lógica do zero, de forma mais simples. Resumindo a história: depois de dois dias, percebi que estava numa estrada sem saída e descartei tudo o que tinha feito.

O motivo? Meu modelo mental era muito simples e não comportava a complexidade e os vários edge cases daquela tela. À medida que fui avançando, percebi que eu tinha que fazer mais e mais incrementos e ajustes, até que chegou num ponto em que o novo código já estava pior do que o anterior. Por fim, tive que abandoná-lo.

Apesar disso, ainda estava profundamente incomodado com o estado do código e queria contribuir de alguma forma, nem que fosse bem pouco, seguindo a regra dos escoteiros: sempre deixar o acampamento mais organizado do que foi encontrado.

Comecei renomeando algumas funções e variáveis para nomes mais significativos. Em seguida, eliminei algumas duplicações escancaradas. Daí, extraí funcionalidades isoladas para arquivos úteis. De repente, percebi que meu entendimento do funcionamento da tela já era muito superior ao de alguns dias atrás, e passei a notar alguns problemas com o fluxo e com o controle de estado. Comecei a unir e organizar alguns useEffect's, trocar alguns parâmetros por estados e vice-versa... ao final de um ou dois dias, eu estava surpreso com o resultado alcançado: O código estava muito mais organizado e legível do que antes - e muito mais elegante do que eu sequer tinha imaginado no meu modelo mental.

Ocorre que, ao começar pelas refatorações mais simples, a legibilidade do código foi ficando cada vez melhor e o meu entendimento do fluxo também. Isso permitiu que mais e mais refatorações fossem aplicadas, cada vez mais complexas.

Essa experiência me fez aprender duas valiosas lições:

  1. Fazer do zero é tentador, mas muito perigoso;
  2. As pequenas refatorações podem parecer insignificantes, mas não são, e muitas vezes abrem caminho para outras maiores. "(...) à medida que o código se torna mais claro, percebo que consigo enxergar detalhes do design que eu não conseguia ver antes." [6]

Da próxima vez que você se deparar com um código muito complexo, lembre-se daquela frase do Uncle Bob: "Nomes em softwares são 90% responsáveis pela legibilidade" [7]. Que tal começar renomeando algumas variáveis e ver aonde isso vai te levar?

Dica #4: Aproveite as funcionalidades da IDE

O objetivo de se fazer uma refatoração sistemática e em pequenos passos é reduzir o risco de alterações na funcionalidade. E que melhor maneira de se fazer isso do que deixando a máquina fazer o trabalho manual e repetitivo para você?

As IDEs modernas fornecem muitas ferramentas de refatoração. Algumas mais complexas e outras menos. Mas, mesmo as refatorações mais simples, como a que te permite renomear uma variável em todos os locais em que ela é referenciada, já são de grande valia.

Quantas vezes já vi desenvolvedores renomeando variáveis no CTRL + C / CTRL + V, no "Replace All" da busca, ou até mesmo digitando uma a uma... Não preciso dizer que várias dessas vezes o código não compilou e o dev teve que voltar atrás para corrigir um erro de digitação ou um import faltando. Isso provavelmente não teria ocorrido se ele tivesse usado a funcionalidade de refatoração da IDE.

Este foi apenas um exemplo simples, mas sugiro que você procure saber quais funcionalidades de refatoração a sua IDE disponibiliza e passe a usá-las quando possível. Talvez seja necessário instalar alguma extensão - ou, quem sabe, escolher uma IDE mais adequada? De qualquer maneira, conhecer e saber usar esse tipo de comando pode trazer grandes ganhos de velocidade para o seu dia a dia.

Contudo, devemos estar atentos ao resultado do comando. A máquina não é perfeita e algumas vezes podem ocorrer falhas durante o processo. Portanto, é sempre bom revisar e testar o código gerado ou modificado.

Dica #5: Para refatorações planejadas, crie uma planilha

Ok. Não precisa ser exatamente uma planilha, mas um artefato que te permita visualizar, priorizar e acompanhar as refatorações planejadas.

Normalmente, esse tipo de refatoração envolve ajustes estruturais ou sistêmicos na solução - ou seja, que afetam grande parte dela.

Nestes casos, é muito comum que os desenvolvedores caiam na tentação de refatorar impulsivamente, trazendo novos padrões para o projeto, muitas vezes adaptando apenas alguns módulos e deixando outros para trás. O resultado é um "Frankenstein" despadronizado - um estado pior do que o anterior. Além disso, os desenvolvedores podem acabar perdendo tempo melhorando aspectos não prioritários da solução.

Por isso mesmo, antes da implementação de qualquer refatoração planejada, sugiro que ela seja cadastrada em uma planilha (ou artefato de controle semelhante). Desta forma, o time terá a oportunidade de visualizar todas as refatorações planejadas de uma só vez, priorizando a ordem de execução de acordo com o valor, a urgência, o esforço e a complexidade, bem como definindo o novo padrão que deverá ser aplicado por toda a solução.

Além disso, será possível monitorar o status de cada refatoração, para garantir que ela não seja feita pela metade - e também definir o responsável por cada uma delas. Ainda, é comum que o time opte por despriorizar ou até mesmo cancelar várias delas.

Se você quiser seguir a sugestão, seguem algumas informações que devem estar presentes no artefato de controle (como, por exemplo, colunas em uma planilha). Para cada refatoração, registre:

  • Identificador. Pode ser um contador incremental;
  • Descrição. Uma breve descrição da refatoração, mencionando os pontos afetados;
  • Prioridade. Baixa; Média; Alta...
  • Status. Não iniciada; Em andamento; Concluída; Cancelada/Despriorizada...
  • Responsável. O nome do membro do time que ficou responsável pelo item;
  • Acompanhamento. É importante anotar o andamento para as refatorações mais demoradas. Por exemplo, se você estiver mudando aos poucos a estrutura de pastas do projeto, anote aqui quais partes já foram alteradas e quais ainda estão faltando.

Conclusão

Você já viu um código perfeito? Eu não... E mesmo um código que pareça muito bom à primeira vista, algum tempo depois, pode começar a parecer confuso e ultrapassado.

A verdade é que sempre podemos melhorar nosso código, no sentido de torná-lo mais legível, organizado, reutilizável ou performático. A refatoração nos permite fazer isso de forma sistemática, reduzindo o risco e os esforços envolvidos.



Referências:

[1] Fowler, Martin. Refatoração. Pág. 12.

[2] Fowler, Martin. Refatoração. Pág. 76.

[3] Fowler, Martin. Refatoração. Pág. 88.

[4] Martin, Robert C. Código Limp. Pág. 314.

[5] Fowler, Martin. Refatoração. Pág. 67.

[6] Fowler, Martin. Refatoração. Pág. 75.

[7] Martin, Robert C. Código Limp. Pág. 309.

© 2023 Cartas de um Dev. Todos os Direitos Reservados.