Refatorar ou Afundar: Por Que Sua Pressa Está Deixando o Código Podre Sem Você Perceber?
Todo código que você melhora um pouco hoje, é uma dor que você evita amanhã. Refatorar não é parar de entregar, é garantir que você continue conseguindo entregar.
Vivemos em um mundo acelerado. A cultura da pressa tomou conta de tudo: da nossa alimentação, do nosso consumo… e, claro, do nosso desenvolvimento de software.
É fácil cair na armadilha da velocidade a qualquer custo. A entrega virou a única métrica visível. Só que, por trás da pressa, existe um preço invisível: o acúmulo silencioso de dívidas no código.
E aí entra o ponto central desse artigo: a refatoração não é mais uma etapa opcional, um luxo para quando der tempo.
Ela precisa se tornar parte natural da rotina de desenvolvimento, assim como escrever código ou testar.
É como escovar os dentes: você não faz quando sobra tempo, você faz todo dia, porque sabe que se não fizer, o estrago virá.
Mas o desafio é maior do que parece. Refatorar não é só uma responsabilidade dos programadores.
Líderes técnicos, gestores, pessoas de produto e até clientes precisam compreender que refatoração é investimento, não desperdício.1
É o que permite que o software continue evoluindo, sem que cada nova entrega vire uma corrida de obstáculos.
Se você lidera pessoas, é seu papel criar um ambiente onde refatorar não seja visto como perda de tempo, mas como uma parte essencial da entrega de valor.
Se você programa, é seu papel explicar e praticar refatoração todos os dias, sem esperar uma janela mágica que nunca chega.
Neste artigo, vamos explorar por que refatorar é uma prática urgente em um mundo que só sabe correr, e como quebrar a barreira cultural que faz as empresas ignorarem o código que está apodrecendo bem debaixo do seu nariz.
Vivemos na era do Fast Software
Hoje, vivemos a era do Fast Software. Empresas, startups, negócios tradicionais… todos querem uma coisa: software rápido.
Mas não é porque são más, ou porque desprezam a qualidade técnica. É porque o jogo do mercado mudou.
O tempo de mercado (o famoso time to market) se tornou uma das moedas mais valiosas.
Quem chega primeiro não apenas conquista usuários, mas muitas vezes consolida uma fatia importante do mercado antes que os concorrentes sequer entendam o que está acontecendo.
E isso não é teoria. É a realidade do mundo corporativo. Os investidores, as lideranças, os sponsors… todos colocaram dinheiro em cima de uma ideia com a expectativa clara: retorno rápido.
E o software é apenas o meio. Pouco importa (na visão de negócios) se por trás da interface existe uma arquitetura robusta ou um castelo de areia. O que importa é: está no ar? Está entregando valor? Está vendendo?
Estamos, sim, vivendo a era do imediatismo. É tudo pra ontem. Não adianta romantizar. Não é drama. É o mercado. Ou você entrega, ou alguém entrega no seu lugar.
Como disse Eric Ries em The Lean Startup, o maior risco de um produto digital não é falhar tecnicamente, mas não encontrar mercado antes de gastar todos os recursos disponíveis.
E é esse medo silencioso que move empresas a querer software rápido. Mas… e o código? É aí que mora a bomba relógio invisível.
Na corrida para entregar o MVP, a funcionalidade, o hotfix… o código vira apenas um meio para chegar ao fim.
A dívida técnica vai sendo empurrada, as decisões técnicas viram atalhos perigosos, e o código começa a apodrecer.
Só que ninguém percebe no começo. A entrega aconteceu. A meta foi batida. A feature está no ar. E o código?
Ele vira um campo minado escondido atrás de uma interface linda.
Aos poucos, a equipe começa a desacelerar, mesmo sem perceber:
Features que antes levavam dias, agora levam semanas.
Bugs que pareciam simples, começam a virar maratonas de investigação.
O medo de mexer em qualquer parte do sistema começa a paralisar o time.
Esse é o paradoxo do Fast Software:
Na busca pela velocidade de entrega, você cria um software que vai te fazer andar cada vez mais devagar.
E não estou dizendo que não devemos correr. Correr faz parte. Empresas precisam ser rápidas, sim. Só que ser rápido não pode ser sinônimo de ser imprudente.
O código precisa ser visto como ativo estratégico, não como um fardo ou um detalhe técnico irrelevante.
E é aí que a refatoração entra como o mecanismo silencioso que mantém essa corrida saudável. É ela que impede que a pressa de hoje destrua a velocidade de amanhã.
Se líderes, negócios e até investidores entendessem que refatorar é proteger o investimento, e não atrasar a entrega, teríamos menos software que começa como foguete e termina como Titanic.
O que é (de verdade) Refatoração?
Vamos direto ao ponto. Refatoração não é deixar o código bonito.
Não é passar o SonarQube, acertar o code smell, ou rodar o linter para deixar as variáveis no padrão.
Essas práticas são legais, claro, mas não são o coração da refatoração.
O termo refatoração foi cunhado e consolidado principalmente pelo Martin Fowler, em seu livro clássico Refactoring: Improving the Design of Existing Code.
É uma obra que muita gente cita, muita gente tem na estante… mas poucos realmente aplicam a essência que Fowler apresenta.
E qual é essa essência?
De forma objetiva, refatoração é o processo de alterar a estrutura interna do código sem alterar seu comportamento externo.
Ou seja, o código continua entregando exatamente o mesmo resultado — o que muda é como ele faz isso.
Esse ponto é chave. A refatoração não é reescrever código por vaidade técnica.
Ela tem um objetivo prático: tornar o código mais simples, mais compreensível, mais seguro de manter e evoluir.
E Fowler deixa isso muito claro: refatorar sem testes de unidade é como realizar uma cirurgia no escuro.
Você pode até sobreviver… mas é uma roleta-russa.
Então vamos tirar da frente algumas confusões comuns:
Refatoração não é:
Melhorar a performance do código (isso é otimização).
Mudar regras de negócio (isso é evolução funcional).
Escrever código novo sem validar que o antigo continua íntegro (isso é risco).
Refatoração é:
Reestruturar.
Tornar o código mais legível.
Reduzir complexidade.
Eliminar duplicações.
Tornar o design mais limpo, modular, desacoplado.
Mas sempre com uma regra de ouro:
Sem testes de unidade cobrindo o comportamento, você não está refatorando. Você está apenas mexendo no código.
E agora a pergunta que não quer calar:
Refatoração é pra ser feita depois da entrega?
A resposta honesta? Idealmente, não. Refatoração não deveria ser um evento especial, separado, com pompa e cerimônia.
Ela deveria ser parte natural do fluxo de desenvolvimento. Todo commit que toca um pedaço do código deveria trazer junto pequenos ajustes que vão limpando o terreno.
Quando deixamos para “refatorar depois da entrega”, normalmente esse depois nunca chega.
Porque o time vai sempre estar pressionado pela próxima entrega.
E quando o cenário exige uma grande refatoração atrasada, o custo é exponencialmente maior, além do risco que acompanha a falta de testes, o acoplamento, as dependências escondidas…
Martin Fowler defende isso no livro: refatoração contínua é o antídoto contra o código apodrecido.
Não como algo que fazemos depois do problema estar grande demais, mas como um hábito preventivo que mantém o sistema saudável.
E, aqui, vale uma reflexão que poucos gostam de ouvir:
Se seu time não tem cultura de testes, ele não tem segurança para refatorar. E sem refatoração, seu código já está em processo de deterioração silenciosa.
Refatorar exige disciplina. Exige testes. Exige coragem.
Mas, mais que tudo, exige consciência de que não é um capricho, é uma prática essencial em sistemas vivos.
O que precisamos considerar antes de refatorar um código?
Essa é uma pergunta que parece simples, mas é aqui que muita gente já começa errando.
Refatoração não exige um conhecimento avançado, mirabolante ou uma super experiência em padrões de design sofisticados.
Ela exige, antes de tudo, atenção, disciplina, consciência do que está fazendo — e, claro, respeito pelo código que está em produção. Vamos por partes…
1. Você precisa ter certeza que o código está coberto por testes confiáveis
Esse é o primeiro, o mais básico, e que muitos ignoram.
Como o próprio Martin Fowler reforça no livro Refactoring, sem testes de unidade garantindo o comportamento externo, você não está refatorando. Você está fazendo uma alteração arriscada.
E não estou falando de cobertura de 100%.
Estou falando de testes que te deem confiança de que o que estava funcionando continuará funcionando após a mudança estrutural.
Se não tem testes?
A primeira refatoração é, muitas vezes, escrever os testes que garantem o comportamento atual antes de qualquer outra mudança.
2. Você precisa parar e pensar: qual o objetivo da refatoração?
Refatorar por refatorar, sem um porquê claro, vira vaidade técnica.
Antes de começar, pare e pergunte:
Por que esse código precisa ser refatorado?
Ele está dificultando a manutenção?
Ele está impedindo a evolução de uma funcionalidade?
Ele está arriscado e gera medo de alterações?
Se você não consegue responder claramente a essas perguntas, talvez o momento de refatorar ainda não chegou.
Refatoração deve ter propósito, não ser movida apenas pela estética ou pela insatisfação com o código legado.
3. Você não precisa saber tudo, mas precisa saber o suficiente
Muita gente tem medo de refatorar porque acha que é preciso ser um guru em design patterns, arquitetura limpa, DDD … Isso é um erro comum.
Refatoração começa com o básico:
Nomear melhor funções e variáveis.
Extrair métodos.
Dividir responsabilidades.
Reduzir duplicações.
Quebrar métodos gigantes.
Isso qualquer programador pode e deve fazer no dia a dia.
Você não precisa ser um arquiteto experiente para começar.
Como Fowler coloca:
Refatoração é uma habilidade que se constrói com prática constante, e começa com pequenos passos, não com grandes reescritas.
4. Você precisa analisar o contexto do código
Outro erro comum é querer refatorar sem entender o porquê o código chegou naquele ponto.
Antes de sair “limpando”, olhe para o histórico:
Qual o histórico de mudanças?
Quais áreas do código geram mais bugs?
Que partes são mais tocadas pelo time?
Refatorar o código certo, na hora certa, gera retorno imediato. Refatorar o que ninguém usa, ou o que não traz risco, é desperdício de energia.
5. Você precisa alinhar com o time e o negócio (quando necessário)
Refatoração não pode ser uma aventura solo, escondida em um branch secreto.
Se a refatoração vai impactar outras partes do sistema, envolver mudanças maiores ou afetar prazos, precisa ser comunicada e, muitas vezes, negociada.
E aqui entra outro ponto importante:
Refatorar não deve feito as escondidas. Deve ser discutido e valorizado como parte do trabalho sério de engenharia.
Quando líderes e negócios entendem isso, o ambiente se torna mais saudável para a prática.
É importante para e pensar antes de refatorar, pare e pense:
Tem testes de unidade?
Qual o objetivo dessa refatoração?
Entendi o contexto do código?
Estou buscando clareza, simplicidade e segurança e não perfeição utópica?
Se a resposta for sim para essas perguntas, você já tem o básico para começar.
O resto é prática. Como diz Fowler:
Refatoração não é heroísmo técnico. É trabalho diário, disciplinado e consciente.
Quais os benefícios práticos (de verdade) da refatoração?
Vamos falar sem rodeios:
Código legado é chato de mexer. Todo programador já sentiu isso na pele. Aquele sistema que ninguém quer encostar. Aquela parte que todo mundo fala “deixa quieto”. Aquele código que você lê e não entende se foi escrito às 3 da manhã ou se foi gerado por um compilador alienígena.
Michael Feathers tem uma definição que é como um tapa na cara:
Código legado é qualquer código sem testes... — Michael Feathers, Working Effectively with Legacy Code
Essa frase resume muito do problema: Sem testes, o medo domina. E o medo trava a evolução. E é aqui que a refatoração aliada aos testes vira uma ferramenta poderosa e prática.
Benefícios reais que você sente no seu dia a dia como programador:
Confiança para mexer no código sem medo de quebrar tudo
Refatorar com testes é como ter um airbag em cada linha de código.
Você consegue mudar, melhorar, limpar… sem aquela sensação de que está mexendo em uma bomba prestes a explodir.
Redução de bugs ocultos
Código sujo esconde problemas.
Refatoração com testes revela essas áreas escuras.
Como diz Fowler: “A refatoração é como limpar o quarto. Você descobre coisas que não lembrava que estavam lá.”
Manutenção mais rápida
Código limpo e testado não te faz perder meia hora tentando entender uma função com 500 linhas.
Você chega, entende, altera e vai embora.
Facilidade de onboarding
Código refatorado com testes facilita a vida de quem chega no time.
Ninguém gosta de passar semanas decifrando código enigmático.
Liberdade criativa
Refatoração bem feita elimina aquele pavor de mexer no sistema.
Permite que o time proponha novas ideias, experimente, sem medo de quebrar o que já existe.
Como fazer da refatoração parte da rotina?
Esse é o pulo do gato que precisamos conversar. Porque não adianta saber dos benefícios se a refatoração continuar sendo vista como uma tarefa no board, uma tarefa de luxo, ou uma dívida eterna.
Quer um caminho prático?
Refatore enquanto entrega: toda vez que tocar no código, faça pequenas melhorias.
Nunca mexa sem testes cobrindo: criou um teste? Melhorou a clareza? Ótimo. Avance.
Quebre o ciclo do medo: mexer em código legado nunca será confortável, mas com testes e refatoração, você transforma o medo em confiança.
Defenda a prática com seu time e líderes: mostre, com exemplos reais, como a ausência de refatoração já está custando caro em bugs, retrabalho e lentidão.
Como Martin Fowler reforça:
Refatoração contínua não é uma fase. É um hábito. É o jeito certo de trabalhar com código que precisa viver por anos, e não apenas sobreviver ao primeiro deploy.
Refatorar não é pra quando sobrar tempo. É pra cada commit. É pra cada vez que você mexe em uma função e ela te dá aquela sensação de “nossa, isso aqui tá dificil de entender”.
É nesses momentos que você, sem cerimônia, com disciplina e segurança (testes!), faz a pequena refatoração.
E essas pequenas melhorias diárias são o que evitam a dor acumulada que faz times entrarem em modo zumbi.
Minha jornada com a refatoração (o que eu aprendi, pratico e evito)
Quero abrir um parêntese aqui para contar um pouco da minha experiência pessoal com refatoração. Quem me conhece sabe que eu gosto de trazer o lado prático, sem floreio.
Quem, de fato, me abriu os olhos para o poder da refatoração foi o Martin Fowler, especialmente com a segunda edição do seu livro Refactoring: Improving the Design of Existing Code.
Foi ali que eu entendi, com profundidade, os padrões de refatoração, as técnicas, os sinais de alerta.
E, principalmente, que refatoração não é sobre fazer bonito, mas sobre melhorar o design interno do código de maneira segura e contínua.
Mas foi com o Rodrigo Manguinho (Manguinho), em seus treinamentos práticos, que eu vi como aplicar isso no dia a dia real de projetos, onde não temos tempo sobrando, onde as entregas apertam, onde o código legado é o que temos.
Manguinho me mostrou na prática como pensar de maneira estratégica, como identificar padrões ruins (code smells), como usar testes como aliados da refatoração sem medo de quebrar o código.
Essas duas fontes mudaram a minha cabeça.
E como eu, aplico isso no meu dia a dia?
Eu tenho uma regra pessoal, que virou hábito:
Antes de refatorar qualquer código, seja legado ou recém-escrito, eu paro, penso e analiso o contexto.
Perguntas que sempre me faço:
Quem vai manter esse código amanhã?
Como eu posso deixar mais claro e fácil de estender?
Tem duplicação desnecessária?
O nome desse método reflete de verdade o que ele faz?
A complexidade está escondida em uma função gigante?
E o ponto mais importante. Eu não começo a refatorar sem ter as bases sólidas da programação bem afiadas.
Refatoração exige entender os fundamentos:
.Programação Orientada a Objetos.
Lógica de Programação.
Princípios SOLID.
Design patterns básicos.
Modularização.
Clareza sem mágica.
Sem isso, a refatoração vira uma reescrita sem direção, cheia de riscos e ego técnico.
O que eu evito durante uma refatoração?
Evito cair na armadilha do “já que estou aqui…”
Refatorar deve ser cirúrgico, pontual, consciente.
Evitar sair mexendo em tudo ao redor sem objetivo claro.
Evito confundir refatoração com reescrita.
Refatorar é melhorar o que já existe.
Reescrever é outra conversa.
Evito fazer refatoração sem cobertura de testes.
Isso pra mim é ponto zero. Sem testes, eu estou só chutando.
Tá legal… muito bonito tudo isso 😂, ma vamos para o código?
Vamos parar e olhar de verdade para esse código?
Antes de sair refatorando qualquer coisa, precisamos, como Martin Fowler sempre reforça, entender o propósito do código.
Então, pare um minuto, olhe com calma:
public List<OrderDto> getOrdersWithDiscount(String customerType, int year) {
List<OrderDto> result = new ArrayList<>();
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
if (order.getCustomer() != null) {
if (order.getCustomer().getType().equals(customerType)) {
if (order.getDate().getYear() == year) {
double total = 0;
for (OrderItem item : order.getItems()) {
Product product = productRepository.findById(item.getProductId());
if (product != null) {
if (product.isActive()) {
double price = product.getPrice() * item.getQuantity();
if (product.getCategory().equals("VIP")) {
price = price * 0.9; // desconto VIP
}
total += price;
}
}
}
OrderDto dto = new OrderDto(order.getId(), order.getCustomer().getName(), total);
result.add(dto);
}
}
}
}
return result;
}
Agora te faço uma pergunta sincera, leitor:
O propósito dessa função está claro pra você?
Você consegue, batendo o olho, explicar exatamente o que ela faz e por quê?
Provavelmente, não.
E é normal. Esse tipo de código é o clássico que encontramos em sistemas vivos, sistemas que já passaram por muitas mãos, por muitas mudanças rápidas, correções, adaptações. Sistemas que estão sobrevivendo!
E o que antes era uma função simples virou um monstro de ifs, loops, chamadas diretas ao banco dentro de loops… É um código que grita por refatoração.
Vamos analisar com olhos críticos:
Você se sentiria confiante de alterar esse código sem ter testes automatizados?
Eu, sinceramente, não.
O medo de quebrar alguma regra de negócio escondida ali seria enorme.
É fácil de entender o que está acontecendo?
Não.
O fluxo de lógica está enterrado em múltiplos ifs aninhados.
O cálculo do desconto VIP está perdido no meio do cálculo geral.
Se amanhã o time pedisse para adicionar mais uma regra de desconto, você conseguiria?
Consegue, claro… com esforço, com medo, com insegurança.
E provavelmente, o código ficaria ainda mais confuso.
Por que é difícil raciocinar sobre esse código?
Porque ele mistura lógica de negócio, lógica de persistência e cálculo em um só lugar.
Porque a profundidade dos ifs obriga você a manter muito contexto na cabeça ao mesmo tempo.
Porque ele tem dependências explícitas com o banco de dados dentro do loop, o que aumenta o risco de performance (e de bugs escondidos).
Porque não há nenhuma separação clara das regras: tudo está emaranhado.
Esse é o tipo de código que gera medo, retrabalho, bugs escondidos. É o tipo de código que faz o time perder confiança no próprio sistema.
Antes de refatorar, eu sempre paro, penso e analiso:
O que essa função quer entregar no final?
Quem são os verdadeiros atores do fluxo?
Onde eu posso quebrar responsabilidades de maneira segura?
O que posso testar primeiro, antes de mudar qualquer linha?
E aí, com calma, usando as técnicas aprendidas com Martin Fowler, Michael Feathers e reforçadas na prática com o Rodrigo Manguinho, começo uma refatoração gradual, segura, sem heroísmo técnico, mas com foco na clareza e na manutenção futura.
Como pensar nos cenários de teste antes de refatorar?
Aqui entra uma das partes mais importantes e que, infelizmente, muitos pulam na pressa:
Nunca refatore um código sem antes ter segurança. E segurança, nesse contexto, são testes cobrindo o comportamento atual.
Primeiro passo: entender o fluxo de verdade (sem confiar em nomes)
O primeiro erro clássico é olhar para o nome do método e tentar deduzir tudo o que ele faz.
Nesse nosso exemplo, o nome é:
getOrdersWithDiscount(String customerType, int year)
Aparentemente simples, né?
Mas, se você olhar só o nome, vai pensar que ele apenas retorna pedidos com desconto.
Só que quando você lê o código, percebe que ele faz muito mais:
Filtra pedidos por tipo de cliente.
Filtra por ano.
Calcula o total do pedido.
Aplica desconto apenas em produtos VIP.
Ignora produtos inativos.
Ignora pedidos sem cliente.
E tudo isso misturado num código denso.
Ou seja:
O nome ajuda, mas não podemos confiar cegamente. Precisamos ler o código com atenção, traçar o fluxo e entender todos os comportamentos que precisam ser garantidos.
Segundo passo: identificar os inputs e outputs reais
Inputs claros:
customerType: tipo de cliente (ex: “CORPORATE”, “INDIVIDUAL”).
year: ano dos pedidos.
Outputs esperados:
Uma lista de OrderDto, contendo:
ID do pedido.
Nome do cliente.
Total calculado com possíveis descontos.
Terceiro passo: mapear os fluxos e cenários que precisam de testes
Aqui é onde precisamos pensar como investigadores.
Vamos dividir em cenários críticos que precisam ser cobertos antes da refatoração:
Quando não há pedidos no banco.
Quando há pedidos, mas o cliente é de outro tipo.
Quando há pedidos com o cliente correto, mas de outro ano.
Quando há pedidos com o cliente correto e ano correto, mas:
O pedido tem itens com produtos inativos.
O pedido tem itens com produtos ativos, mas não VIP.
O pedido tem itens com produtos VIP (deve aplicar desconto de 10%).
Quando há pedidos com cliente nulo (deve ignorar com segurança).
Quando há produtos que não existem (o repositório retorna uma lista vazia? null?).
Quando há múltiplos pedidos que atendem ao filtro.
Quarto passo: validar comportamento e não implementação
O foco dos testes não é validar “como” o código faz, mas “o que” ele entrega.
Está filtrando corretamente?
Está calculando o total certo?
Está aplicando o desconto corretamente?
Quinta dica prática: comece escrevendo testes no estado atual (antes da refatoração)
Michael Feathers reforça muito isso em seu livro:
Quando você lida com código legado sem testes, seu primeiro passo não é refatorar. É criar testes que validem o comportamento atual, mesmo que o código esteja horrível. Só depois você refatora com segurança.
Mesmo que seja dolorido, mesmo que você tenha que escrever testes que parecem estranhos, é o seguro a se fazer.
Não confie em nomes de métodos.
Leia o código linha por linha.
Mapeie todos os fluxos possíveis.
Escreva testes cobrindo os comportamentos, mesmo que você odeie o código atual.
Só depois, com segurança, comece a refatoração gradual.
Bora escrever alguns testes agora?
Escrevendo testes de unidade antes da refatoração
Agora que entendemos o fluxo (escondido) da nossa função, é hora de escrever os primeiros testes de unidade.
Aqui vale uma dica importante:
Não é necessário (nem saudável) tentar cobrir todos os fluxos de uma vez.O importante é começar pelos cenários críticos, aqueles que garantem o comportamento observado mais importante.
Como o código está denso e complexo, vou mostrar alguns testes didáticos que cobrem os principais fluxos:
Filtragem correta por tipo de cliente e ano.
Cálculo correto de total com e sem desconto VIP.
Ignorar produtos inativos.
Ignorar pedidos sem cliente.
Vamos lá. Não vou criar para todos afinal isso é um post e não um curso na Udemy 😅
@ExtendWith(MockitoExtension.class)
@DisplayName("Unit tests for getOrdersWithDiscount method (before refactoring)")
class OrderServiceTest {
@InjectMocks
private OrderService orderService;
@Mock
private OrderRepository orderRepository;
@Mock
private ProductRepository productRepository;
@Test
@DisplayName("Should return empty list when no orders match the customer type and year")
void shouldReturnEmptyListWhenNoOrdersMatchFilters() {
// Arrange
when(orderRepository.findAll()).thenReturn(List.of(
createOrder(1L, "OTHER", 2024)
));
// Act
List<OrderDto> result = orderService.getOrdersWithDiscount("VIP", 2023);
// Assert
assertTrue(result.isEmpty());
verify(orderRepository).findAll();
}
@Test
@DisplayName("Should calculate total with 10% discount when product category is VIP and active")
void shouldCalculateTotalWithDiscountForVipProduct() {
// Arrange
Order order = createOrder(1L, "VIP", 2023);
order.getItems().add(new OrderItem(1L, 2)); // 2 units of product 1
Product product = createProduct(1L, true, 100, "VIP");
when(orderRepository.findAll()).thenReturn(List.of(order));
when(productRepository.findById(1L)).thenReturn(product);
// Act
List<OrderDto> result = orderService.getOrdersWithDiscount("VIP", 2023);
// Assert
assertEquals(1, result.size());
assertEquals(1L, result.get(0).getOrderId());
assertEquals("Customer 1", result.get(0).getCustomerName());
assertEquals(180.0, result.get(0).getTotal()); // 200 - 10%
verify(orderRepository).findAll();
verify(productRepository).findById(1L);
}
@Test
@DisplayName("Should ignore products that are inactive")
void shouldIgnoreInactiveProducts() {
// Arrange
Order order = createOrder(1L, "VIP", 2023);
order.getItems().add(new OrderItem(1L, 2)); // 2 units of product 1
Product inactiveProduct = createProduct(1L, false, 100, "VIP");
when(orderRepository.findAll()).thenReturn(List.of(order));
when(productRepository.findById(1L)).thenReturn(inactiveProduct);
// Act
List<OrderDto> result = orderService.getOrdersWithDiscount("VIP", 2023);
// Assert
assertEquals(1, result.size());
assertEquals(0.0, result.get(0).getTotal()); // Produto inativo não soma nada
verify(orderRepository).findAll();
verify(productRepository).findById(1L);
}
@Test
@DisplayName("Should safely ignore orders with null customer")
void shouldIgnoreOrdersWithNullCustomer() {
// Arrange
Order order = new Order();
order.setId(99L);
order.setCustomer(null);
when(orderRepository.findAll()).thenReturn(List.of(order));
// Act
List<OrderDto> result = orderService.getOrdersWithDiscount("VIP", 2023);
// Assert
assertTrue(result.isEmpty());
verify(orderRepository).findAll();
verifyNoInteractions(productRepository);
}
// Helpers
private Order createOrder(Long id, String customerType, int year) {
Customer customer = new Customer();
customer.setId(1L);
customer.setName("Customer 1");
customer.setType(customerType);
Order order = new Order();
order.setId(id);
order.setCustomer(customer);
order.setDate(LocalDate.of(year, 1, 1));
order.setItems(new ArrayList<>());
return order;
}
private Product createProduct(Long id, boolean active, double price, String category) {
Product product = new Product();
product.setId(id);
product.setActive(active);
product.setPrice(price);
product.setCategory(category);
return product;
}
}
O que validamos nesses testes?
Estamos validando comportamentos observáveis, ou seja:
O método retorna pedidos apenas quando as regras de filtragem são atendidas.
O cálculo do total é feito corretamente, com ou sem desconto.
Produtos inativos são ignorados com segurança.
Pedidos com cliente nulo não explodem, apenas são ignorados.
O que evitamos:
Validar detalhes de implementação (ex: a ordem das chamadas no loop).
Validar coisas irrelevantes (como o nome do método interno, etc.).
Dicas que aplico:
Use nomes de teste descritivos e legíveis, tanto no nome do método quanto no @DisplayName.
Use verify para garantir que as dependências foram chamadas como esperado.
Prefira escrever poucos testes claros a tentar cobrir tudo em um único teste gigante e confuso.
Mas obviamente esses testes não cobrem todos os cenários! Aqui estamos rabiscando apenas para mostrar o passo a passo de uma refatoração.
Refatoração gradual — Etapa 1: Só limpando os ifs que nos fazem sofrer!
Agora que você já entendeu o caos desse método, vamos com calma, sem pressa, focar apenas na parte mais confusa: os ifs aninhados lá no começo.
Se você parar e olhar bem, vai perceber que esse trecho é um convite ao erro:
if (order.getCustomer() != null) {
if (order.getCustomer().getType().equals(customerType)) {
if (order.getDate().getYear() == year) {
// lógica...
}
}
}
Notou? Você tem que manter na cabeça pelo menos três condições ao mesmo tempo para entender o que está acontecendo.
Agora imagine um time júnior mexendo nisso… ou você mesmo voltando daqui seis meses. Provavelmente, vai perder um tempão tentando lembrar o porquê de cada uma dessas condições.
Como deixar isso mais saudável?
Eu comecei simples:
Usei guard clauses, aquelas condições no começo que já eliminam os casos que não queremos.
Depois, criei um método auxiliar
isEligibleOrder
. Ele tem um nome claro, objetivo e isola todas as regras de filtragem do pedido.
Olha só como ficou mais fácil de ler:
for (Order order : orders) {
if (!isEligibleOrder(order, customerType, year)) {
continue;
}
// lógica permanece igual por enquanto
}
E o método:
private boolean isEligibleOrder(Order order, String customerType, int year) {
if (order.getCustomer() == null) {
return false;
}
if (!order.getCustomer().getType().equals(customerType)) {
return false;
}
return order.getDate().getYear() == year;
}
Notou, pessoal?
Como já ficou mais fácil entender o que está acontecendo?
O método principal ficou mais limpo, mais fluido.
E o mais importante: não mudei a lógica.
Só organizei as condições em um lugar mais claro.
Agora, antes de qualquer outra mudança, é hora de fazer o que todo programador responsável faz:
Rodar os testes e garantir que tudo continua funcionando exatamente como antes.
Lembra da regra de ouro?
Refatorar com segurança. Nunca no escuro. Teste primeiro, refatore depois, teste de novo.
Uma dúvida que pode estar na sua cabeça agora…
“Mas Rafael… você negou a expressão, por que fez assim? Por que não deixou do jeito tradicional, com if positivo?”
Ótima pergunta. Vamos olhar para a condição em questão:
if (!order.getCustomer().getType().equals(customerType)) {
return false;
}
Sim, eu estou negando a expressão logo de cara. E faço isso de propósito. Isso se chama guard clause invertida, ou seja:
Se a condição NÃO for verdadeira, ou seja, se o tipo de cliente não for o que esperamos, já encerramos o método retornando false imediatamente.
Essa abordagem tem um propósito muito simples:
Deixa o fluxo mais direto.
Evita aninhamentos desnecessários.
Tira da cabeça as exceções o mais rápido possível.
Se eu tivesse escrito assim:
if (order.getCustomer().getType().equals(customerType)) {
if (order.getDate().getYear() == year) {
return true;
}
}
return false;
Percebe como já ficaria mais aninhado de novo?
Eu teria que manter mais contexto mentalmente.
Ao negar logo a exceção:
if (!order.getCustomer().getType().equals(customerType)) {
return false;
}
Eu deixo o código mais linear:
Elimino o caso errado.
Sigo direto para o próximo passo apenas se for o caso correto.
Isso ajuda muito na leitura e no raciocínio.
Você vai perceber que é um padrão que aparece em códigos limpos e em refatorações de Fowler.
Dica prática:
Sempre que puder, negue as exceções logo no começo, usando guard clauses. Isso mantém seu método enxuto, sem degraus mentais desnecessários.2
E claro:
Se você achar que está começando a ter muitas negações confusas, use nomes de métodos que já expressem a intenção.
Ex: isNotEligibleOrder()
(mas nesse caso nosso isEligibleOrder
ficou bem claro e direto.)
Por que eu mostrei esse exemplo?
Talvez você esteja pensando:
“Mas Rafael, esse código nem é tão monstruoso assim… Eu já vi coisas piores.”
E eu concordo com você.
Esse exemplo foi propositalmente didático, simples o suficiente para conseguirmos refletir em cima dele com segurança e clareza. Mas o ponto aqui é... Na vida real, em empresas grandes, o cenário é muito pior.
Eu mesmo já me deparei com classes de 3 mil, 8 mil linhas, onde missões críticas do negócio estavam sendo sustentadas por código com cheiro de morte.
E o mais perigoso: ninguém mais tem coragem de mexer nesses códigos com confiança e sem demorar semanas.
Vira aquele famoso “não toca nisso, por favor”. E não pense que são apenas sistemas antigos, legados de 20 anos. Mesmo softwares pequenos, modernos, com equipes ágeis e cheias de post-its, muitas vezes escondem bugs que custam milhares ou até milhões de reais, dólares, euros… para empresas.
Vou contar um caso real que ouvi de um programador que trabalhou em um grande banco internacional:
Ele me contou que um dos sistemas mais antigos da área de câmbio tinha um bug conhecido que custava ao banco entre 2 e 3 milhões de dólares por ano.
Sim, por ANO. E ninguém mexia no código. Por medo. Pela complexidade. Pela bagunça.
O código tinha virado um monstro tão grande que as equipes aceitavam esse cenário ao invés de enfrentar a dor de mexer no sistema.3
E é exatamente isso que NÃO queremos nos sistemas que criamos e mantemos.
Por isso, refatoração e testes não são luxo. Eles são aliados estratégicos. Eles são a vacina contra o código que paralisa o negócio. Eles são o que nos permite continuar evoluindo, corrigindo, melhorando, sem medo.
Refatoração + testes = coragem, segurança e sustentabilidade do software.
Não estamos falando de código bonito. Estamos falando de sistemas vivos, que precisam ser mantidos saudáveis para não se transformarem em fontes de prejuízo invisível.
Como conversar com o time e gerentes sobre refatoração?
Esse é um ponto delicado, mas muito importante. Refatorar não é só uma decisão técnica. É uma questão de cultura de time.
Muita gente se pergunta:
“Mas como eu explico pro meu gerente que estou refatorando e não só enrolando?”
E aqui vai uma resposta que pode soar simples demais e até parecer clichê, mas não é:
Você não precisa convencer com discurso. Você convence com consistência. Alguns podem pensar:
“Lá vem papo de coach…”
Mas não, caros leitores. Isso não é papo da boca pra fora. Pense comigo:
Todo mundo já viu isso acontecer em algum momento da vida, um parente, um amigo, alguém no trabalho que simplesmente começou a dar o exemplo.
Sem falar muito. Só fazendo o certo. E, naturalmente, as pessoas ao redor começaram a notar. O comportamento chamou atenção. Ganhou respeito.
Com código é a mesma coisa. Você não precisa parar uma reunião para defender a refatoração. Você começa pequeno, no seu dia a dia:
Em cada commit, você melhora um trecho do código.
Em cada pull request, você entrega uma lógica mais clara, mais enxuta, com testes.
Você mostra que não está apenas entregando funcionalidade, você está deixando o sistema melhor do que encontrou.
E aí o resultado vem, naturalmente:
Você começa a entregar com menos bugs.
Seu código começa a ser mais fácil de entender.
Você começa a ser notado.
Com o tempo:
O time percebe que o seu trabalho não quebra nada, pelo contrário, fortalece o sistema.
O gerente percebe que você não é só mais um entregador de tarefa, você é alguém que cuida da base técnica do produto.
E aí, sem precisar fazer discurso, refatoração vira parte da cultura. Não porque você forçou. Mas porque você deu o exemplo certo, com constância.
Refatoração não é luxo. É maturidade.
Nesse artigo, a gente foi além da superfície. Não falamos de refatoração como algo bonito ou elegante.
Falamos como ela realmente é: uma ferramenta de sobrevivência em um ambiente de pressão, prazos e sistemas vivos.
A gente viu que:
Vivemos a era do Fast Software, e a velocidade mal direcionada pode destruir a sustentabilidade do produto.
Refatoração não é só para depois da entrega.
Ela deve ser parte do fluxo, da rotina, da cultura.
Testes são o que nos dão segurança para refatorar.
Sem testes, a refatoração vira risco.
Mesmo sistemas pequenos escondem dívidas técnicas caras.
E grandes sistemas muitas vezes viram intocáveis por medo.
A forma como escrevemos código hoje define a coragem que teremos amanhã.
E não precisamos convencer ninguém com teoria.
Basta começar a mostrar, no seu código, nos seus commits, que refatorar funciona.
Se tem uma coisa que você deve levar daqui é isso:
Todo código que você melhora um pouco hoje, é uma dor que você evita amanhã. Refatorar não é parar de entregar, é garantir que você continue conseguindo entregar.
Agradeço de coração por ler até o final! ❤️
Na prática, caro leitor, sabemos que essa compreensão muitas vezes esbarra em outras prioridades. É humano, afinal. As pressões por resultados rápidos, a busca por projetos de sucesso que brilhem aos olhos de investidores e diretores, e a natural aspiração por avanço na carreira... tudo isso molda a visão de quem está em posições de liderança ou gestão. Mesmo para aqueles que um dia estiveram "na trincheira" do código e sentiram na pele o peso do legado, a perspectiva muda ao assumir novas responsabilidades, focadas em outras métricas e objetivos. Não é má vontade, mas sim a complexidade de alinhar visões sob diferentes chapéus e metas, num sistema que muitas vezes valoriza mais a entrega visível de novas funcionalidades do que a saúde interna do que já existe. É um ciclo difícil de quebrar, e reconhecer isso já é um passo.
Pra deixar esse assunto mais claro quero deixar outro exemplo. Sem guard clause:
public void ProcessarPedido(Pedido pedido) {
if (pedido != null) {
if (pedido.TemItens()) {
if (pedido.ClienteValido()) {
// Lógica principal do processamento
System.out.println("Pedido processado.");
} else {
System.out.println("Erro: Cliente inválido.");
}
} else {
System.out.println("Erro: Pedido sem itens.");
}
} else {
System.out.println("Erro: Pedido nulo.");
}
}
Com guard clauses:
public void ProcessarPedido(Pedido pedido) {
if (pedido == null) {
System.out.println("Erro: Pedido nulo.");
return;
}
if (!pedido.TemItens()) {
System.out.println("Erro: Pedido sem itens.");
return;
}
if (!pedido.ClienteValido()) {
System.out.println("Erro: Cliente inválido.");
return;
}
// Lógica principal do processamento
System.out.println("Pedido processado.");
}
No segundo exemplo, a lógica principal fica no nível mais baixo de indentação e é muito mais direta.
Essa situação, por mais surreal que pareça à primeira vista (quem aceitaria perder 2-3 milhões por ano?!), revela uma lógica, cruel mas comum, no mundo corporativo em grande escala como o bancário. Para um banco com faturamentos na casa dos bilhões, 2 ou 3 milhões anuais, embora significativo, pode ser visto como um "custo operacional" previsível e até "gerenciável". A alternativa… encarar uma refatoração profunda ou uma migração de um sistema crítico e complexo e isso envolveria riscos e custos potencialmente muito maiores e menos previsíveis. Pense em meses ou anos de trabalho de equipes grandes, interrupções em operações essenciais, e a altíssima probabilidade de introduzir novos bugs (quem sabe, ainda mais caros) num sistema tão delicado. Nessa balança fria de custo vs. risco, aceitar a perda anual conhecida pode ser, do ponto de vista estratégico e financeiro do banco, a opção "menos pior". Uma lição amarga sobre como a saúde técnica vira uma métrica secundária frente ao balanço financeiro imediato e à aversão a riscos imprevistos em sistemas legados.