Todos os programadores já leram o "Clean Code" de Robert Martin. Poucas equipas aplicam de facto os seus princípios de forma consistente. É no fosso entre saber como é o código limpo e entregá-lo sob pressão de prazos que a maioria das bases de código se degrada.
Na Privum, destilámos anos de experiência em produção em 12 regras que aplicamos em todos os projetos — desde ferramentas internas a plataformas voltadas para o cliente que tratam milhões de pedidos. Não são diretrizes aspiracionais; são práticas concretas, sustentadas por revisões de código, linters e gates de CI.
1. As funções fazem uma só coisa
Uma função deve fazer uma coisa, fazê-la bem e fazer apenas isso. Se precisas da palavra "e" para descrever o que uma função faz, deviam ser duas funções.
Mau:
validateAndSaveUser(data) — isto valida E guarda. Se a validação falhar, tenta guardar na mesma? Se a gravação falhar, reverte o estado da validação? O acoplamento cria ambiguidade.
Bom:
validateUser(data) → saveUser(validatedUser) — cada função tem uma única responsabilidade, um único motivo para falhar e uma única coisa para testar.
A regra prática: se uma função tem mais de 20 linhas, provavelmente está a fazer mais do que uma coisa. Extrai a segunda.
2. Dá nomes às coisas pelo que fazem, não pelo que são
Os nomes de variáveis e funções devem revelar a intenção. Quem lê deve perceber o que o código faz sem ler a implementação.
Mau: const d = new Date() — o que é "d"? Uma data, mas qual? Porque é que precisamos dela?
Bom: const subscriptionExpiresAt = new Date(user.trialEnd) — agora qualquer pessoa que leia isto sabe exatamente o que representa e porque existe.
Para variáveis booleanas, usa prefixos que se leiam como perguntas: isActive, hasPermission, canDelete, shouldRetry. Código que se lê como inglês é código que não precisa de comentários.
3. Falha depressa, falha ruidosamente
Não engulas erros em silêncio. Não devolvas null quando algo corre mal. Não registes um erro no log e continues como se nada fosse.
Mau: Um try/catch que apanha tudo e devolve um array vazio — quem chama não faz ideia de que algo falhou, e depurar torna-se um pesadelo.
Bom: Valida as entradas na fronteira (handlers de API, submissões de formulários), lança erros tipados quando os invariantes são violados e deixa o erro propagar-se até um handler que saiba como responder (devolver um 400, mostrar um toast, tentar de novo).
Os erros silenciosos são os bugs mais caros. Não deitam a tua aplicação abaixo — corrompem os teus dados lentamente, ao longo de semanas, até alguém reparar.
4. Escreve testes de comportamento, não de implementação
Os testes devem verificar o que o código faz, não como o faz. Se refatorares o funcionamento interno e os teus testes partirem, os teus testes estão a testar a coisa errada.
Mau: Verificar que um método privado específico foi chamado com argumentos específicos — este teste está acoplado a detalhes de implementação e vai partir em qualquer refactor.
Bom: Dada uma entrada, verifica o resultado ou o efeito secundário. "Quando um utilizador submete um formulário válido, deve ver uma mensagem de sucesso e os dados devem ficar persistidos." Este teste sobrevive à refatoração porque testa comportamento.
As métricas de cobertura são úteis, mas enganadoras. 80% de cobertura com bons testes de comportamento é melhor do que 100% de cobertura com testes de implementação frágeis.
5. Mantém as dependências nas extremidades
A lógica de negócio não deve importar clientes HTTP, drivers de base de dados nem código específico de um framework. Empurra o I/O para as extremidades e mantém o núcleo puro.
Isto não é um conselho académico — tem consequências práticas: - A lógica de negócio pura é trivialmente testável (sem necessidade de mocks) - Mudar de PostgreSQL para MongoDB altera o adaptador, não o domínio - As atualizações de framework não se propagam em cascata por toda a tua base de código
O padrão: Controladores/Handlers → Casos de uso/Serviços → Lógica de domínio. As dependências fluem para dentro. O domínio nunca importa nada da infraestrutura.
6. Prefere composição a herança
A herança cria um acoplamento forte. Quando herdas de uma classe base, herdas todo o seu contrato — incluindo partes de que não precisas e bugs que não escreveste.
A composição dá-te flexibilidade: combina peças pequenas e focadas em vez de construíres hierarquias profundas. Em TypeScript/JavaScript, isto significa preferir funções e objetos simples a hierarquias de classes.
Se deres por ti a criar uma hierarquia de classes com mais de dois níveis, para e refatora. A complexidade não compensa a abstração.
7. Trata os erros ao nível certo
Nem todas as funções devem tratar todos os erros. Os erros devem ser tratados ao nível que tem contexto suficiente para tomar a decisão certa.
Uma função de consulta à base de dados não deve decidir que código de estado HTTP devolver — não sabe que está a ser chamada a partir de um handler HTTP. Deve lançar um erro tipado que o handler consiga interpretar.
Organiza o tratamento de erros por camadas: - Camada de domínio: lança erros específicos do domínio (UserNotFound, InsufficientBalance) - Camada de aplicação: apanha os erros de domínio e decide a estratégia de resposta - Camada de infraestrutura: apanha os erros de transporte, implementa novas tentativas e circuit breakers
8. Torna os estados ilegais irrepresentáveis
Usa o teu sistema de tipos para prevenir bugs em tempo de compilação. Se um utilizador pode estar "ativo" ou "suspenso", não uses um booleano — usa um tipo união ou um enum. Se uma encomenda tem de ter pelo menos um artigo, não uses um array — usa um tipo que garanta que não está vazio.
Exemplo em TypeScript: em vez de { status: string }, usa { status: 'active' | 'suspended' | 'deleted' }. Agora o compilador apanha gralhas e estados inválidos antes do runtime.
Quantos mais invariantes codificares nos tipos, menos verificações em tempo de execução precisas e menos bugs chegam a produção.
9. As revisões de código não são opcionais
Cada linha de código que chega a produção deve ser revista por pelo menos outro engenheiro. Sem exceções — nem para "correções rápidas", nem para "são só alterações de configuração", nem para o tech lead.
As revisões de código apanham bugs, partilham conhecimento, fazem cumprir os padrões e criam responsabilidade partilhada pelo código. São a prática de qualidade mais eficaz a seguir aos testes automatizados.
Mantém as revisões pequenas (menos de 400 linhas). Revê em menos de 24 horas. Comenta padrões, não preferências. Faz perguntas em vez de exigências.
10. Apaga o código morto
O código morto não é gratuito. Confunde os novos membros da equipa, aumenta a carga cognitiva e, de vez em quando, é reativado por acidente. Se o código não é chamado, apaga-o. O Git lembra-se.
Isto inclui: - Blocos de código comentados - Imports e variáveis não utilizados - Feature flags que estão permanentemente ligadas ou desligadas há meses - Endpoints de API que nada chama
Corre uma ferramenta de análise de código morto trimestralmente. Vais ficar surpreendido com o quanto se acumula.
11. Faz logging com propósito
Registar tudo é tão inútil como não registar nada. Bons logs respondem a: "o que aconteceu, quando, a quem e qual era o contexto?"
Estrutura os teus logs em JSON com campos consistentes: timestamp, level, service, requestId, userId, action, duration, error. Isto torna-os pesquisáveis e agregáveis.
Usa corretamente os níveis de log: - ERROR: algo falhou e precisa de atenção - WARN: aconteceu algo inesperado, mas foi tratado - INFO: eventos de negócio relevantes (um utilizador registou-se, um pagamento foi processado) - DEBUG: detalhes técnicos úteis durante o desenvolvimento (tempos de consulta, cache hits)
Em produção, corre ao nível INFO. Ativa o DEBUG apenas quando estiveres a investigar problemas específicos.
12. Automatiza os teus padrões
As regras que dependem da disciplina humana para serem cumpridas vão ser violadas sob pressão. Codifica os teus padrões nas ferramentas:
- Linting: ESLint, Prettier ou Biome para formatação e estilo
- Verificação de tipos: TypeScript em modo strict, sem
any - Pre-commit hooks: Husky + lint-staged para apanhar problemas antes de chegarem ao CI
- Gates de CI: testes, verificação de tipos e lint têm de passar antes do merge
- Análise de dependências: Renovate ou Dependabot para atualizações automatizadas
Se uma regra é suficientemente importante para ser imposta, é suficientemente importante para ser automatizada. Os revisores humanos devem focar-se na lógica, na arquitetura e na clareza — não na indentação nem na ordem dos imports.
Conclusão
Código limpo não é um luxo — é uma decisão económica. Cada hora investida em qualidade de código poupa dez horas em depuração, onboarding e manutenção. As equipas que aplicam estas práticas entregam mais depressa (não mais devagar), porque passam menos tempo a lutar contra a sua própria base de código e mais tempo a construir funcionalidades.
A melhor altura para adotar estas práticas é no início de um projeto. A segunda melhor altura é agora. Escolhe três regras desta lista, acrescenta-as à definition of done da tua equipa e aplica-as no teu próximo sprint. A melhoria é incremental — mas acumula-se.