Introdução

Código não é algo estático, a medida que novas demandas, regulamentos e tecnologias vão surgindo, a necessidade de ter alterações é natural. Escrever um código limpo não é só sobre criar, mas principalmente sobre manter.

A regra do escoteiro divulgada por Robert Martin em seus livros é fundamental para a escrita limpa de código (deixe o código um pouco melhor do que encontrou). É com esse princípio simples que se cria o hábito de pequenas melhorias contínuas, evitando que o software se torne confuso e difícil de evoluir.

Este é o último de uma série de três artigos que venho escrevendo sobre código limpo. Nele, irei abordar os temas referentes à manutenção e evolução do código, explorando como lidar com erros, a importância da refatoração, a necessidade de testes, a aplicação dos princípios SOLID e o conceito de código emergente.

Press enter or click to view image in full size

Tratamento de Erros: tornando falhas claras

Tratamento de erros é uma etapa comum no dia a dia de qualquer programador. Todo o código está fadado a erros, quando isso ocorre somos responsáveis por certificar que nosso código faça o que seja preciso.

Muitas vezes o tratamento de erros dominam muitos códigos fonte, fazendo com que fique difícil enxergar o que o código realmente está fazendo devido a quantidade de tratamentos espalhados. É claro que você deve utilizar desse recurso, mas se obscurecer a lógica, está errado.

Use exceções ao invés de códigos de erro

Abordei esse assunto brevemente no segundo artigo que postei sobre clean code. Algumas linguagens antigas não suportavam exceções, o que forçava os programadores a ter que utilizar técnicas para retornar códigos de erro.

Press enter or click to view image in full size

Figura 1: retorno de códigos de erro

O problema é que essa técnica joga muita responsabilidade para o chamador, que deve verificar esses códigos logo após a chamada. Isso deixa o código extremamente acoplado, onde caso seja inserido um novo código de erro você precisa adicionar uma validação para ele. Por esse motivo que a utilização de exceptions é muito valiosa para nós em linguagens mais modernas.

A utilização de exceções deixam o código muito mais claro, te permitindo separar melhor o que é fluxo de negócio e o que é tratamento de erros.

Figura 2: tratamento de erros via exceção

Separação de responsabilidades

Como já dito anteriormente, um ponto essencial é garantir que a lógica de negócio não fique misturada com a lógica de tratamento de erros (sejam eles códigos de erro ou estruturas try/catch). Quando misturamos tratamentos dentro de regras importantes, o fluxo principal acaba poluído e difícil de acompanhar.

O ideal é que cada parte do código cuide do que realmente é sua responsabilidade, o método que executa a regra deve focar no negócio e o tratamento do erro deve ficar no lugar certo. Assim, além de o código ficar mais claro, facilitamos sua manutenção e evolução.

Contexto claro

Uma exceção sem contexto não serve pra muita coisa. Por exemplo, uma mensagem genérica como “Erro no processo” não ajuda em nada ao tentar entender o problema.

Sempre forneça informações relevantes como o que falhou, onde falhou e se possível o porquê da falha. Isso não significa “logar” a stack de erro gigante para o usuário final, mas garantir que os logs e mensagens tragam clareza para o desenvolvedor que vai analisar o erro.

Exceções bem descritas economizam tempo e evitam que um simples bug se torne uma investigação demorada.

Evitar silenciamento

Um dos maiores “pecados” é capturar uma exceção e simplesmente ignorá-la como na figura abaixo:

Figura 3

Esse tipo de prática cria um “buraco negro” no sistema. Um erro ocorreu, mas desapareceu sem rastros. Isso pode comprometer a confiabilidade da aplicação e gerar bugs difíceis de rastrear.

Sempre trate as exceções de forma adequada, seja relançando, logando, ou convertendo para outro tipo de exceção mais específica.

Recuperabilidade

Nem todo erro precisa ser tratado na origem. Muitas vezes o ponto em que a falha ocorre não tem contexto suficiente para decidir o que fazer. Nesses casos, o melhor é lançar a exceção pra cima e deixar que os chamadores decidam como tratar (exibir uma mensagem, tentar novamente ou até encerrar a operação). O ponto é que você deve tratar o erro no nível certo, não antecipe responsabilidades que não cabem ao método atual.

Refatoração e Código Morto

Refatorar continuamente

Muitos desenvolvedores caem na armadilha de acreditar que um dia terão tempo para parar e refatorar o código, o famoso débito técnico que a equipe sempre usa como desculpa para entregar uma atividade atrasada (comento sobre isso no artigo "O dev limpo"). A verdade é que esse momento raramente chega. O código precisa ser melhorado de forma contínua, a cada alteração feita.

Esperar o cenário perfeito para limpar o código significa acumular débito técnico e tornar o sistema cada vez mais difícil de manter. A refatoração deve ser encarada como parte natural do processo de desenvolvimento.

Código morto

Funções nunca chamadas, variáveis não utilizadas, classes esquecidas no projeto, comentários aleatórios, etc. Esse tipo de código morto gera “lixo visual” e confundem na manutenção dele. Além disso, dá a falsa impressão de que algo ainda é importante, mesmo não sendo.

Manter apenas o que é realmente utilizado mantém o sistema mais claro, código morto sempre deve ser removido. Um bom exemplo é que já vi muitas situações onde códigos comentados foram deixados de forma proposital com a desculpa de que aquele trecho poderia ser utilizado novamente. Com ferramentas de versionamento de código não precisamos manter esses lixos, pois caso realmente seja necessário no futuro é muito fácil poder voltar para um commit onde aquele código estava em uso. Caso contrário aquele trecho ficará ali poluindo o arquivo até que alguém realmente o remova (o que pode demorar).

Pequenos passos

Refatorar não significa reescrever tudo de uma vez. Na verdade as melhores refatorações acontecem em pequenos passos, sempre acompanhado por testes automatizados.

Isso dá segurança para mudar sem medo de quebrar o sistema. Refatorar em blocos menores torna o processo mais sustentável e menos arriscado.

Melhoria constante

Seguindo a regra do escoteiro, se cada pessoa fizer pequenas melhorias ao mexer em uma parte do sistema o código inteiro evolui naturalmente. É essa mentalidade que evita a degradação e garante longa vida ao código deixando sempre fácil de manter.

Testes

Muita gente encara testes unitários como pequenos pedaços de código descartáveis, criados apenas para garantir que o programa “roda”. Nesse cenário, o foco acaba sendo apenas atingir um número alto de cobertura, como se isso por si só fosse sinônimo de qualidade.

Os testes vão muito além disso, eles são parte essencial do sistema, funcionam como documentação e servem como um segurança para permitir mudanças sem medo de quebrar funcionalidades já existente quando codificados corretamente.

Testes como rede de segurança

Se tem algo que fica claro ao ler os livros de Robert Martin é o quanto ele defende os testes como parte central do desenvolvimento. E não é sem base… Manter uma boa coleção de testes unitários é o que nos dá a confiança necessária para refatorar o código sem medo.

Com testes automatizados, cada mudança deixa de ser um tiro no escuro e passa a ser um passo seguro, porque qualquer efeito de erro será alertado no relatório de execução dos testes. Essa tranquilidade é o que torna possível evoluir um sistema sem paralisar o time com receio de “quebrar algo”.

Lembro que, no início da minha carreira, eu via testes como perda de tempo. Só depois de enfrentar bugs e retrabalhos percebi que os testes não eram um peso, mas sim um investimento de segurança a longo prazo.

Testes pequenos e rápidos

Testes unitários devem ser curtos, específicos e focados em um único comportamento. Quando um teste tenta validar muitas coisas ao mesmo tempo, ele perde clareza e se torna difícil de manter.

O ideal é que cada teste funcione como um exemplo de como aquela unidade de código deve se comportar. Além disso, quanto mais rápidos forem, mais fácil será rodar a coleção completa várias vezes no dia, facilitando práticas de integração e entrega contínuas.

Figura 4: exemplo teste unitário ruim

Press enter or click to view image in full size

Figura 5: exemplo teste unitário bom

Integração e sistema

Não basta ter apenas testes unitários, sistemas reais envolvem múltiplos módulos, serviços externos, banco de dados e APIs. É aí que entram os testes de integração e os testes de sistema (end-to-end).

Enquanto os unitários garantem o bom funcionamento de pequenas partes isoladas, os de integração e sistema verificam se tudo funciona em conjunto. Esse equilíbrio é o que dá confiança de que o software se comportará corretamente em produção.

Regra do teste em primeiro

O Test-Driven Development (TDD) é defendido com força pelo Uncle Bob. Essa metodologia pode parecer sem muito sentido no começo, mas traz benefícios claros:

  • Força a pensar primeiro no comportamento desejado.
  • Evita escrever código desnecessário.
  • Gera uma cobertura de testes mais completa desde o início.

Mesmo que nem sempre seja possível aplicar TDD em tudo, ter essa disciplina como princípio ajuda a manter o código mais limpo e orientado ao que é esperado.

Clareza nos testes

Testes também fazem parte do código do sistema e devem ser escritos com a mesma preocupação de legibilidade.

Nomes de métodos de teste precisam ser descritivos, deixando claro o que está sendo verificado. Além disso, é importante manter uma estrutura de fácil leitura (arrange, act, assert, etc).

Um teste mal escrito pode ser tão prejudicial quanto um código de regra de negócio mal escrito, isso porque testes confusos não transmitem confiança e acabam sendo ignorados com o tempo.

Princípios SOLID

O acrônimo SOLID representa cinco princípios de design orientado a objetos. A aplicação deles ajuda a manter o código mais flexível, coeso, testável e fácil de manter. São guias que ajudam a evitar que o software se torne acoplado. Passarei brevemente por este tópico, pois pretendo lançar futuramente um artigo aprofundando em cada um dos princípios.

S — Single Responsibility Principle

O Princípio da Responsabilidade Única diz que uma classe deve ter apenas um motivo para mudar. Isso significa que ela deve concentrar em uma única responsabilidade dentro do sistema. Quando misturamos várias responsabilidades na mesma classe aumentamos o acoplamento e dificultamos tanto a manutenção quanto os testes. Classes coesas tornam o código mais simples de entender e reduzem o risco de uma alteração em uma parte impactar outra.

O — Open-Closed Principle

Segundo o Princípio Aberto/Fechado, um sistema deve estar aberto para extensão, mas fechado para modificação. Isso quer dizer que não devemos alterar o código já existente sempre que surgir uma nova necessidade, devemos criar formas de estender o comportamento sem quebrar o que já funciona.

Para se fazer isso dá para utilizar as técnicas de polimorfismo, ao invés de inserir if/else para cada novo caso, criamos novas classes que implementam a mesma abstração (se conecta com o princípio DIP mais abaixo). Dessa forma, o código permanece estável e o sistema evolui com segurança

L — Liskov Substitution Principle

O Princípio da Substituição de Liskov afirma que subclasses devem poder substituir seus pais sem alterar o comportamento esperado do programa. Isso significa que usando uma classe filha no lugar da classe pai o sistema deve continuar funcionando de forma correta.

I — Interface Segregation Principle

O Princípio da Segregação de Interfaces diz que se deve ter interfaces específicas em vez de interfaces genéricas que forçam classes a implementar métodos que não usam (diretamente relacionado com LSP). Interfaces muito abrangentes criam dependências desnecessárias e geram implementações com métodos vazios. Interfaces bem definidas garantem que cada classe dependa apenas do que realmente precisa, mantendo o design limpo e flexível.

D — Dependency Inversion Principle

O Princípio da Inversão de Dependência recomenda que devemos depender de abstrações, não de implementações concretas. Isso significa que classes de alto nível não devem depender de classes de baixo nível diretamente, ambas devem depender de abstrações. Esse princípio reduz o acoplamento entre módulos e facilita em muito a manutenção do código.

Código Emergente

Regra do Design Simples

Kent Beck, um dos criadores do Extreme Programming, propôs a Regra do Design Simples que é uma forma prática de avaliar a qualidade do design do código, priorizando o que realmente importa no dia a dia.

As quatro regras são:

  1. O código passa em todos os testes: se o código não é testado não é confiável. Garantir que tudo esteja coberto por testes dá segurança para evoluir e refatorar sem medo.
  2. Não tem duplicação: código duplicado é sinal de má organização. Dificulta manutenção e aumenta o risco de erros.
  3. Expressa claramente suas intenções: deve ser legível e comunicar suas ideias de forma direta.
  4. Possui o menor número possível de classes e métodos: um bom design não é o mais complexo, mas o mais enxuto. Classes e métodos devem existir apenas se realmente agregarem valor.

Essas quatro regras funcionam como um guia simples e poderoso para manter o design limpo no dia a dia.

Design incremental

Um erro comum é tentar prever todo o design logo no início do desenvolvimento. A prática do design incremental sugere que a arquitetura deve ser resultado da aplicação contínua de boas práticas. Isso não significa que não precise de um planejamento, mas sim planejar o suficiente para começar e deixar que o design evolua à medida que o sistema cresce.

Evolução natural

Quando testamos e refatoramos constantemente, o design do sistema vai surgindo de forma saudável. Ao invés de tentar prever todas as mudanças, deixamos que o código evolua naturalmente.

Esse processo de evolução contínua é o que diferencia um código que envelhece mal de um sistema que se mantém robusto e fácil de adaptar mesmo após anos de manutenção.

Conclusão

Com o tempo fui percebendo que escrever código limpo não é só sobre seguir princípios, na verdade é sobre construir software que seja simples e que pode ser mantido e evoluído sem dor de cabeça.

Já vivi situações em que tive medo de mudar uma linha por medo de quebrar o sistema, enfrentei sistemas cheios de duplicações, li funções imensas, tentei entender funções com responsabilidades misturadas e sei bem como isso trava a evolução do código. Foi quando comecei a aplicar conceitos de refatoração, testes e princípios de design simples e senti que meu trabalho realmente fluía mais tranquilamente.

Hoje vejo que o mais importante não é buscar o “código perfeito”, mas sim ter disciplina para deixar o código um pouco melhor do que estava. Essa pequena atitude, quando repetida todos os dias, é o que faz um sistema crescer de forma saudável e sustentável. No fim, código limpo não é um destino, mas um caminho.