Refatorando React: além do almanaque

Gabriel Ullmann
8 min readJun 4, 2024

--

Este ano de 2024 é especial para mim, pois completo 10 anos desenvolvendo software. Ao longo destes anos, trabalhei em várias ocasiões na manutenção ou refatoração de aplicações Web legadas, tanto em software houses no Brasil quanto em projetos acadêmicos e industriais no Canadá. Trabalhar com a mão na massa neste tipo de aplicação é algo extremamente enriquecedor, pois a prática te permite não apenas consolidar o que você aprendeu estudando, mas também observar o que funciona (ou não) em cada caso e desenvolver sua própria opinião sobre diferentes abordagens de desenvolvimento.

Quando estudamos desenvolvimento de software, somos ensinados, na maior parte do tempo, a criar coisas. Aprendemos a criar diagramas UML para modelar nossa aplicações, e somos orientados a construir nosso código utilizando boas práticas e design patterns. Contudo, quando se trata de refatoração, precisamos às vezes inverter nosso fluxo de pensamento, pois refatoração é desconstrução. Como diz o velho ditado, é impossível fazer um omelete sem quebrar ovos. Para refatorar, precisamos destruir partes da nossa arquitetura para então reconstruí-las, e executar isso da forma correta é um desafio imenso.

Neste post, compartilho com você algumas dicas “de almanaque” que me ajudaram em projetos de refatoração, acompanhadas de exemplos que você pode aplicar em seus projetos. Recentemente, trabalhei na refatoração de um front-end JavaScript em React, e, portanto, destaco este framework em meus exemplos. Contudo, menciono também dicas de organização e gerenciamento que podem ser aplicadas em qualquer projeto, do mais simples ao mais complexo. Entre uma dica e outra, faço também algumas sugestões de leitura para que você possa aprender, refletir e desenvolver sua própria maneira de pensar sobre refatoração.

Não mergulhe em um poço sem fundo

A refatoração é um processo iterativo. Mesmo depois de você refatorar a sua aplicação inteira para aderir a certos design patterns ou a um estilo arquitetural, há sempre espaço para que alguém levante a mão e diga “poderia ter sido feito diferente” ou “deveríamos re-escrever tudo neste framework mágico que fulano lançou ontem”. Sendo assim, antes de começar um projeto de refatoração é fundamental se reunir com seu time e stakeholders para definir os objetivos do projeto. Por exemplo:

  • Melhorar a legibilidade do código (ex.: remover código duplicado/morto, uniformizar identação e espaçamentos)
  • Melhorar a semântica do código (ex.: dar nomes mais significativos para arquivos, pastas, componentes, funções e variáveis)
  • Aumentar a coesão (ex.: agrupar no mesmo arquivo funções com a mesma finalidade, agrupar na mesma pasta componentes com a mesma finalidade)
  • Diminuir o acoplamento (ex.: evitar dependências desnecessárias entre componentes, remover estados locais/globais não utilizados)

Definir objetivos é importante não apenas para guiar nossas escolhas de arquitetura e implementação, mas também para que possamos saber quando refatoramos o suficiente. No contexto de Agile Scrum, essa é a chamada “definition of done” (“definição de feito”, em uma tradução literal). Como explica o manual oficial do Scrum:

A “definição de feito” cria transparência pois provê a todos um entendimento compartilhado sobre o trabalho que foi executado e sobre os padrões que foram atendidos como parte do Incremento [iteração do projeto]. (…) Pense na “definição de feito” como um conjunto de padrões definidos para o produto a ser entregue.

Siga a estrada mais trilhada, mas esteja pronto para o “off-road”

É difícil ilustrar posts sobre código, então aí vai mais uma paisagem. Arquivo pessoal.

Ao refatorar, comece sempre pelas estratégias testadas e aprovadas para deixar seu código mais elegante, mais fácil de ler, manter e evoluir. Um exemplo bem conhecido são os design patterns GoF, descritos no livro Design Patterns: Elements of Reusable Object-Oriented Software, de Gamma, Helm, Johnson e Vlissides. Caso queira se aprofundar mais no assunto ou buscar mais exemplos, recomendo também a leitura das seguintes obras:

  • Clean Code: A Handbook of Agile Software Craftsmanship, de Robert Martin (2008)
  • Clean Architecture: A Craftsman’s Guide to Software Structure and Design, de Robert Martin (2017)
  • AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis, de William Brown (1998)
  • Patterns of Enterprise Application Architecture, de Martin Fowler (2002)

Embora essas obras sejam verdadeiros almanaques, elas têm um pequeno problema: não foram escritas com refatoração de front-end JavaScript em mente. Sendo assim, se você estiver refatorando um projeto React, como foi o meu caso, vai acabar aplicando os design patterns que essas obras propõe com bem menos frequência do que gostaria. Por exemplo, os patterns Factory e Observer são frequentemente implementados com o uso de interfaces, que não existem em JavaScript. Classes abstratas e controle de visibilidade (public/private) também não existem em JavaScript, o que nos impede de ditar regras e padrões que os desenvolvedores devem seguir ao utilizar nossas classes e componentes.

E antes que alguém pergunte “por que não usar TypeScript?”, lembre-se que estamos em um projeto de refatoração! A ideia não é converter a aplicação para outra linguagem e sim melhorar o código que temos. Mas se não podemos usar os conselhos da literatura consagrada, o que fazer então? Bom, meus colegas e eu acabamos cruzando diversas referências para construir nosso próprio almanaque de padrões e boas práticas. Isso exige, é claro, muita responsabilidade e um entendimento substancial das tecnologias com as quais você está trabalhando. Sendo assim, arregaçamos as mangas e começamos a construção de nosso almanaque partindo da fonte: a documentação oficial do React.

Após muitos debates com colegas e releituras dos artigos na seção Learn acabei descobrindo algo que acabou se tornando “norte” dos nossos esforços de refatoração: a tendência do React de separar a renderização dos componentes de todo o resto. Por exemplo, como explica o artigo Thinking in React:

Quando você constrói uma interface de usuário com React, você irá primeiro quebrá-la em partes chamadas de componentes. (…) Em React, dados que mudam com o passar do tempo são chamados de estados (States). Você pode adicionar estados a qualquer componente, e atualizá-los conforme necessário.

Em resumo, os componentes React vão sendo desenhados (ou renderizados) na tela à medida que os estados desse componente mudam. Mas nem só de renderização vive nossa aplicação front-end. E se quisermos executar um trecho de código que faz uma request HTTP para o back-end? E se quisermos ler o localStorage? Nesse caso, utilizamos os Effects:

Effects permitem que você execute código após a renderização. Por exemplo, você pode controlar um componente não-React com base em um estado React, estabelecer conexão com um servidor, ou enviar um log de análise quando um componente aparece na tela.

Sendo assim, colocamos como prioridade número 1 em nossa refatoração a tarefa de separar Effects de Components (com seus respectivos States). Embora o React nos permita declarar múltiplos Effects e Components no mesmo arquivo .js, decidimos dividir os mais longos em Hooks, uma estrutura do React que é utilizada para encapsular lógica de UI, cumprindo um papel semelhante às classes e módulos em outras linguagens, embora de forma mais simplificado. De quebra, acabamos identificando e eliminando vários trechos de código duplicado ou morto durante o processo de “extração” dos Effects e criação de Hooks, o que nos permitiu enxugar a base de código em alguns milhares de linhas.

Deixe as preferências pessoais de lado

Além da falta de classes abstratas, interfaces e outras estruturas que nos permitam controlar e uniformizar o uso de módulos, classes e funções, o JavaScript tem também outro ponto fraco: sua sintaxe extremamente permissiva. Enquanto em linguagens como Java, C e Python há somente uma maneira de declarar classes e métodos, o JavaScript nos permite fazer a mesma coisa de várias maneiras. Por exemplo, podemos declarar um componente React assim:

export default function MyComponent(props) {
return (<div data-custom={props.foo}>This is an example</div>)
}

Ou também assim:

const MyComponent = (props) => {
return (<div data-custom={props.foo}>This is an example</div>)
}
export default MyComponent

Ou ainda:

const MyComponent = ({ foo, …props }) => {
return (<div data-custom={foo}>This is an example</div>)
}
export {MyComponent}

Mas então, qual é a forma mais correta? Depois de muito debate em meu time, optamos pela última forma, pois ela destaca quais são as props mais importantes utilizadas pelo componente. Além disso, o uso de “export” em vez de “export default” encoraja o uso do “verdadeiro” nome do componente/objeto a ser importado, evitando uma renomeação (intencional ou não). Por exemplo:

// se utilizássemos "export default" em nosso componente, ele poderia ser importado da seguinte forma
import QualquerNomeBemLegal from './MyComponent'

// utilizando somente "export", o import não vai funcionar a menos que o nome definido na implementação seja utilizado (entre chaves!)
import { MyComponent } from './MyComponent'

Colocar este tipo de definição em nosso almanaque pode parecer exagerado, afinal, se existem três jeitos que funcionam, por que escolher apenas um? Contudo, escrever nossos imports e componentes de maneira uniforme traz várias vantagens na manutenção do projeto, tais como:

  • Facilidade para ensinar novos desenvolvedores a criarem/alterarem componentes existentes.
  • Facilidade para explicar o funcionamento dos componentes a stakeholders ou pessoas que não saibam programar.
  • Menos conflitos de merge e desentendimentos entre desenvolvedores no mesmo time.
  • Implementação imparcial: o componente não é escrito de tal maneira “pois eu gosto assim”, e sim por que é o padrão de codificação do projeto aceito por todos.

Deixe a arquitetura falar por você

Outro problema comum com projetos legados é o tamanho da estrutura de diretórios. Embora nem sempre possamos diminuir essa estrutura, podemos eliminar diretórios aninhados, vazios ou com nomes genéricos. Renomear diretórios e arquivos pode ser também uma boa pedida para facilitar a compreensão. Nesse aspecto, recomendo uma abordagem interessante que aparece no livro Clean Architecture: em vez de nomear os componentes com base em conceitos técnicos (ex.: controller, model, query), deveríamos nomeá-los com base nos casos de uso dos quais eles fazem parte. Robert Martin chama esse conceito de “screaming architecture” (arquitetura gritante, em uma tradução livre):

O que a arquitetura da sua aplicação está gritando? Quando você olha para o primeiro nível da estrutura de diretórios, e para os arquivos no pacote de mais alto nível, eles gritam “Sistema de Gestão de Hospital”, “Contabilidade” ou “Almoxarifado”? Ou será que eles gritam “Rails”, “Spring/Hibernate” ou “ASP”?

Considerações Finais

Embora tenhamos uma vasta literatura acadêmica sobre refatoração a nossa disposição, bem como várias fontes formais e informais (por exemplo, documentação, StackOverflow, etc.), não há fórmula mágica para um projeto de refatoração bem-sucedido. Contudo, se tem uma coisa que podemos levar como regra para qualquer projeto é a necessidade de uma boa comunicação. Como citei em minhas dicas, é fundamental:

  • Comunicar aos stakeholders desde o início quais são os objetivos e possível obstáculos para a refatoração.
  • No contexto do time de desenvolvimento, entrar em acordo quanto aos padrões de código e arquitetura a serem seguidos para a refatoração.
  • Em caso de desacordo, é preciso debater os pontos positivos e negativos de cada abordagem, baseando as escolhas sempre em referências e padrões “testados e aprovados” e não nas preferências pessoais de membros do time.
  • Assim como um texto, devemos escrever nosso código pensando no público alvo: outros desenvolvedores. Sempre que possível, devemos buscar comunicar nossas intenções através de nossas escolhas de nomenclatura, das funções e estruturas que utilizamos, etc.

E depois de terminar seu projeto de refatoração, comemore! Afinal, esse é apenas o início de mais uma etapa da evolução do seu software, e também do seu almanaque.

Fontes:

--

--

Gabriel Ullmann

Pesquisador de Engenharia de Software, sempre garimpando por coisas interessantes no código de video games e apps em geral. Atualmente em Montreal, Canada.