Uma cartilha rápida sobre refatoração para iniciantes
Marta Swiatkowska
Júnior Software Engineer
Talvez esteja a escrever sobre algo óbvio para muitos, mas talvez não para todos. Refactoring é, penso eu, um tópico complicado porque envolve a alteração do código sem afetar o seu funcionamento.
Por conseguinte, é incompreensível para alguns que refactorização é de facto uma área da programação e é também uma parte muito importante do trabalho do programador. O código está sempre a evoluir, será modificado enquanto houver a possibilidade de adicionar novas funcionalidades. No entanto, pode tomar uma forma que já não permita adicionar efetivamente novas funcionalidades e seria mais fácil reescrever todo o programa.
O que é a refactorização?
Normalmente, a resposta que se ouve é que se está a alterar a estrutura do código, aplicando uma série de transformações de refactoring sem afetar o comportamento observável do código. Isso é verdade. Recentemente, também me deparei com uma definição de Martin Fowler no seu livro "Melhorar a conceção do código existente" onde descreve refactorização como "fazer grandes mudanças em pequenos passos". Ele descreve refactorização como uma alteração de código que não afecta o seu funcionamento, mas sublinha que tem de ser feita em pequenos passos.
O livro defende também que refactorização não afecta o funcionamento do código e salienta que não tem qualquer efeito sobre a aprovação nos testes em qualquer altura. Descreve passo a passo como efetuar com segurança refactorização. Gostei do seu livro porque descreve truques simples que podem ser utilizados no trabalho quotidiano.
Porque é que precisamos de refactoring?
Na maioria das vezes, pode ser necessário quando se pretende introduzir uma nova funcionalidade e o código na sua versão atual não o permite ou seria mais difícil sem alterações ao código. Além disso, é útil nos casos em que adicionar mais funcionalidades não é rentável em termos de tempo, ou seja, seria mais rápido reescrever o código de raiz. Penso que por vezes se esquece que refactorização pode tornar o código mais limpo e mais legível. Martin escreve no seu livro como faz refactoring quando sente odores desagradáveis no código e, como ele diz, "deixa sempre espaço para o melhor". E surpreendeu-me ao ver a refacção como um elemento do trabalho quotidiano do código. Para mim, os códigos são muitas vezes incompreensíveis, a sua leitura é uma experiência um pouco difícil, pois o código é muitas vezes pouco intuitivo.
A caraterística distintiva de um programa bem concebido é a sua modularidadegraças à qual basta conhecer apenas uma pequena parte do código para introduzir a maioria das modificações. A modularidade também facilita a entrada de novas pessoas e o início de um trabalho mais eficiente. Para conseguir esta modularidade, os elementos do programa relacionados devem ser agrupados, sendo as ligações compreensíveis e fáceis de encontrar. Não existe uma regra única para o fazer. À medida que se conhece e compreende cada vez melhor a forma como o código deve funcionar, é possível agrupar os elementos, mas por vezes também é necessário testar e verificar.
Uma das regras de refactoring em YAGNIé um acrónimo de "You Aren't Gonna Need It" (Não vais precisar dele) e deriva de Programação eXtreme (XP) utilizado principalmente em Ágildesenvolvimento de software equipas. Resumindo, YAGNI diz que só se devem fazer coisas actualizadas. Isto significa, basicamente, que mesmo que algo possa vir a ser necessário no futuro, não deve ser feito neste momento. Mas também não podemos esmagar outras extensões e é aqui que a modularidade se torna importante.
Quando se fala de refactorizaçãoNo entanto, um dos elementos mais essenciais, ou seja, os testes, deve ser mencionado. Em refactorizaçãoprecisamos de saber que o código ainda funciona, porque refactorização não altera o seu funcionamento, mas sim a sua estrutura, pelo que todos os testes devem ser aprovados. É preferível executar testes para a parte do código em que estamos a trabalhar após cada pequena transformação. Isso dá nós uma confirmação de que tudo funciona como deve ser e encurta o tempo de toda a operação. É disto que o Martin fala no seu livro - executar testes com a maior frequência possível para não dar um passo atrás e perder tempo à procura de uma transformação que tenha quebrado alguma coisa.
Refactorização de código sem testes é uma dor de cabeça e há uma grande probabilidade de algo correr mal. Se for possível, seria melhor adicionar, pelo menos, alguns testes básicos que nos darão alguma garantia de que o código funciona.
As transformações listadas abaixo são apenas exemplos, mas são realmente úteis na programação diária:
Extração de funções e extração de variáveis - se a função for demasiado longa, verifique se existem funções menores que possam ser extraídas. O mesmo se aplica a linhas longas. Estas transformações podem ajudar a encontrar duplicações no código. Graças às Small Functions, o código torna-se mais claro e compreensível,
Renomeação de funções e variáveis - a utilização da convenção de nomeação correta é essencial para uma boa programação. Os nomes das variáveis, quando bem escolhidos, podem dizer muito sobre o código,
Agrupamento das funções numa classe - esta alteração é útil quando duas classes executam operações semelhantes, uma vez que pode encurtar o comprimento da classe,
Substituir a declaração aninhada - se a condição for verificada para um caso especial, emitir uma declaração de retorno quando a condição ocorrer. Esses tipos de testes são geralmente chamados de cláusula de guarda. Substituir uma declaração condicional aninhada por uma declaração de saída altera a ênfase no código. A construção if-else atribui o mesmo peso a ambas as variantes. Para a pessoa que está a ler o código, é uma mensagem de que cada uma delas é igualmente provável e importante,
Introduzir um caso especial - se utilizar muitas vezes algumas condições no seu código, pode valer a pena criar uma estrutura separada para elas. Como resultado, a maioria das verificações de casos especiais pode ser substituída por simples chamadas de função. Muitas vezes, o valor comum que requer um processamento especial é o nulo. Por isso, este padrão é frequentemente designado por Objeto nulo. No entanto, esta abordagem pode ser utilizada em qualquer caso especial,
Substituição do Polimorfismo de Instrução Condicional.
Exemplo
Este é um artigo sobre refactorização e é necessário um exemplo. Quero mostrar um exemplo simples de refactoring abaixo com a utilização de Substituir a declaração aninhada e Substituição do polimorfismo de instrução condicional. Digamos que temos uma função de programa que devolve um hash com informações sobre como regar plantas na vida real. Essa informação estaria provavelmente no modelo, mas para este exemplo, temo-la na função.
def rega_info(planta)
resultado = {}
if planta.is_a? Suculenta || planta.is_a? Cato
result = { water_amount: "Um pouco" , como fazer: "Do fundo", duração da rega: "2 semanas" }
elsif planta.is_a? Alocasia || planta.is_a? Maranta
result = { water_amount: "Grande quantidade", como_fazer: "Como preferires", duração da rega: "5 dias" }
elsif planta.is_a? Peperomia
resultado = { quantidade_de_água: "Quantidade dicente",
como_fazer: "A partir do fundo! não gostam de água nas folhas",
duração_da_rega: "1 semana" }
else
resultado = { quantidade_de_água: "Quantidade dicente",
como_fazer: "Como preferir",
duração_da_rega: "1 semana"
}
fim
devolver resultado
fim
A ideia é mudar se voltar:
se planta.isa? Suculenta || planta.isa? Cato
result = { wateramount: "Um pouco" , howto: "A partir do fundo",
retorno { águaquantidade: "Um bocadinho" , comopara: "A partir do fundo", regaduração: "2 semanas" } if plant.isa? Suculenta || planta.is_a? Cato
E assim por diante, até chegarmos a uma função que se parece com isto:
def watering_info(plant)
return result = { wateramount: "Um pouco" , howto: "Do fundo", duração da rega: "2 semanas" } if planta.isa? Suculenta || planta.is_a? Cato
return result = { wateramount: "Grande quantidade", como: "Como preferires", duração da rega: "5 dias" } if planta.isa? Alocasia || planta.is_a? Maranta
return result = { water_amount: "Quantidade de água",
howto: "A partir do fundo! eles não gostam de água nas folhas",
duração da rega: "1 semana" } if planta.is_a? Peperomia
return result = { quantidade_de_água: "Quantidade de água",
como_fazer: "Como preferir",
duração da rega: "1 semana"
}
fim
No final, já tínhamos um resultado de retorno. E um bom hábito é fazer isto passo a passo e testar cada alteração. Podias substituir este bloco if por um caso de switch e ficaria imediatamente melhor, e não terias de verificar todos os ifs de cada vez. Ficaria assim:
def watering_info(plant)
swich plant.class.to_string
case Suculenta, Cato
{ wateramount: "Um pouco" , como: "A partir do fundo", duração da rega: "2 semanas" }
caso Alocasia, Maranta
{ wateramount: "Grande quantidade", como: "Como preferir", duração da rega: "5 dias" }
caso Peperomia
{ quantidade_de_água: "Quantidade de água",
como_fazer: "A partir do fundo! eles não gostam de água nas folhas",
duração_da_rega: "1 semana" }
else
{ water_amount: "Quantidade de dicente",
como_fazer: "Como preferir",
duração da rega: "1 semana" }
fim
fim
E depois pode aplicar o Substituição do polimorfismo de instrução condicional. Trata-se de criar uma classe com uma função que devolve o valor correto e os coloca nos seus devidos lugares.
classe Suculenta
...
def rega_info()
return { wateramount: "Um pouco" , como: "Do fundo", duração da rega: "2 semanas" }
fim
fim
classe Cactus
...
def rega_info()
return { wateramount: "Um pouco" , como: "Do fundo", duração da rega: "2 semanas" }
fim
fim
classe Alocasia
...
def rega_info
return { wateramount: "Grande quantidade", howto: "Como preferir", duração da rega: "5 dias" }
fim
fim
classe Maranta
...
def rega_info
return { wateramount: "Grande quantidade", howto: "Como preferir", duração da rega: "5 dias" }
fim
fim
classe Peperomia
...
def rega_info
return { water_amount: "Quantidade de água",
como_fazer: "A partir do fundo! eles não gostam de água nas folhas",
duração_da_rega: "1 semana" }
fim
fim
classe Planta
...
def rega_info
return { water_amount: "Quantidade de água",
como_fazer: "Como preferir",
duração da rega: "1 semana" }
fim
fim
E na função principal watering_infofunction, o código terá o seguinte aspeto:
def watering_info(plant)
planta.map(&:watering_info)
fim
Naturalmente, esta função pode ser removida e substituída pelo seu conteúdo. Com este exemplo, quis apresentar a função geral padrão de refactorização.
Resumo
Refactoring é um tema importante. Espero que este artigo tenha sido um incentivo para ler mais. Estes competências de refacção ajudam-no a detetar os seus erros e a melhorar a sua oficina de código limpo. Recomendo a leitura do livro de Martin (Improving the Design of Existing Code), que é um conjunto bastante básico e útil de regras de refactorização. O autor mostra várias transformações passo a passo com uma explicação completa e motivação e dicas sobre como evitar erros em refactorização. Devido à sua versatilidade, é um livro encantador para front-end e programadores backend.