Princípios de Código Limpo (Clean code)
Durante a carreira profissional, muitas vezes trabalhamos em algoritmos complexos ou arquiteturas distribuídas; e, no meio da correria do dia-a-dia, esquecemos que o código é lido muito mais vezes do que é escrito. Com isto em mente, Robert C. Martin – conhecido como “Uncle Bob” – elaborou uma série de sugestões para elaboração de código com o intuito de melhorar a legibilidade. Essa abordagem ficou conhecida como Código Limpo (do inglês, Clean Code).
O que é clean code?
Clean code é uma abordagem de escrita de software voltada para legibilidade, simplicidade, manutenção e clareza de intenção. Em termos práticos, significa produzir código que outras pessoas — e o próprio autor, no futuro — consigam ler, entender, corrigir e evoluir com menor esforço. Robert C. Martin afirma que código limpo deve ser fácil de ler e de modificar, reduzindo o custo de manutenção e o acúmulo de complexidade técnica (Martin, 2009).
Apesar das ideias serem comumente atribuídas apenas ao Uncle bob – afinal, o livro dele chama-se Clean Code -, é importante destacarmos que elas não nasceram de um único autor. Ela é resultado de uma tradição de engenharia de software que valoriza clareza, abstração, coesão, baixo acoplamento e boa comunicação por meio do código. Entre os autores mais influentes estão Robert C. Martin (obviamente), Martin Fowler, Kent Beck, Steve McConnell, Andrew Hunt e David Thomas. Cada um enfatiza aspectos distintos, mas convergentes: código é um ativo intelectual e sua qualidade interna importa tanto quanto seu funcionamento externo (Fowler, 2018; McConnell, 2004; Hunt; Thomas, 1999).
Propriedades fundamentais do clean code
Um código limpo costuma apresentar algumas propriedades recorrentes. A primeira é a legibilidade. O leitor deve conseguir entender rapidamente o que o código faz e por quê. A segunda é a simplicidade. Soluções desnecessariamente sofisticadas elevam o custo cognitivo. A terceira é a coesão: cada módulo, função ou classe deve concentrar uma responsabilidade clara. A quarta é o baixo acoplamento, isto é, a redução de dependências rígidas entre partes do sistema. A quinta é a capacidade de evolução: código limpo facilita testes, refatorações e extensão futura (Martin, 2009; Fowler, 2018).
Em JavaScript, esses princípios são particularmente importantes porque a linguagem é flexível e permissiva. Essa flexibilidade é poderosa, mas também facilita a produção de código inconsistente, ambíguo ou excessivamente implícito.
Nomes significativos
Um dos princípios mais conhecidos de Robert C. Martin é que nomes devem revelar intenção. De acordo com Martin (2009), um nome de variável ideal deve possuir três características essenciais:
- Revelar a intenção: o nome deve responder por que a variável existe, o que ela faz e como é usada.
- Evitar desinformação: nomes não devem sugerir um tipo ou propósito diferente do real.
- Ser pronunciável e buscável: se você não consegue pronunciar o nome, não consegue discutir o código. Além disso, se o nome é curto demais (como
e), ele se torna impossível de localizar em uma base de código extensa.
A partir das características, percebemos que nomes das variáveis, funções e classes não devem exigir interpretação excessiva do desenvolvedor. Logo, um bom nome reduz a necessidade de comentários explicativos (Martin, 2009).
Técnicas e princípios práticos
- Use nomes que revelam intenção: evite nomes genéricos ou codificados. Se um nome requer um comentário para explicar seu significado, então o nome falhou em sua missão original.
- Evite codificações e notação húngara: antigamente, era comum prefixar variáveis com seu tipo (ex:
strName,iCount). Em linguagens modernas e com IDEs potentes, essa prática é considerada ruído visual. Conforme Martin (2009) descreve, nomes de variáveis devem focar no domínio do problema, não na implementação técnica. - Use nomes de classes com substantivos e métodos com verbos: as classes devem nomeadas com substantivos ou frases substantivadas (ex:
Customer,WikiPage,Account), evitando-se nomes genéricos comoDataouInfo. Enquanto isso, os métodos devem conter verbos ou frases verbais (ex:postPayment,deletePage,save). - Selecione uma palavra por conceito: mantenha a consistência léxica. Se você usa
fetchpara buscar dados em um serviço, não useretrieveougetem outro serviço para a mesma finalidade. Escolha um termo e atenha-se a ele em todo o projeto.
Agora, vejamos alguns exemplos de códigos ruins e bons, segundo a filosofia do clean code. Apenas, uma rápida observação: apesar dos devs brasileiros falarem a língua portuguesa como via de regra, acreditamos que seja uma boa prática apresentar os códigos em inglês, pois consideramos que treinar a programação na língua inglesa pode ajudar nas buscas por empregos fora do país (o famoso, “trampo na gringa”).
O Código 1 é bem simples e funcionará num ambiente de javascript. Contudo, ele não comunica claramente o significado das variáveis a, b, c e r.
function p(a, b, c) {
if (c) {
return a * b;
}
return a + b;
}
const r = p(50, 3, true);
console.log(r);Refatorando o Código 1 para melhorar a legibilidade, obtemos o Código 2.
function calcularValorItem(preco, quantidade, deveMultiplicar) {
if (deveMultiplicar) {
return preco * quantidade;
}
return preco + quantidade;
}
const totalValorItem = calcularValorItem(50, 3, true);
console.log(totalValorItem);A refatoração acima ainda não é ideal, mas já melhora a semântica inicial. Vamos melhorar isso um pouco mais, gerando o Código 3.
function calcularSubtotalItemCarrinho(precoUnitario, quantidade) {
return precoUnitario * quantidade;
}
function calcularValoresCombinados(primeiroValor, segundoValor) {
return primeiroValor + segundoValor;
}
const carrinhoSubtotal = calcularSubtotalItemCarrinho(50, 3);
console.log(carrinhoSubtotal);Aqui o problema foi melhor resolvido: em vez de uma função genérica controlada por uma flag, criam-se funções com propósito explícito. Fowler (2018) observa que remover generalizações desnecessárias costuma melhorar o design e facilitar a manutenção.
Seguir esses passos simples nos oferecem algumas vantagens tais como: manutenibilidade, reduzindo drasticamente o tempo que um desenvolvedor leva para entender um sistema legado; redução de bugs, pois nomes claros expõem inconsistências lógicas de forma mais evidente; e documentação viva, tornando o código autoexplicativo, reduzindo a necessidade de documentação externa que frequentemente fica desatualizada.
Em contrapartida, o tempo Inicial de desenvolvimento tende a aumentar, pois escrever código limpo exige mais reflexão inicial, o que pode parecer “lento” em ambientes de extrema pressão por entrega. No entanto, o custo de manutenção de um código “rápido e sujo” é exponencialmente maior a longo prazo. Além disso, nomes descritivos tendem a ser mais longos. Então, o desafio do engenheiro de software é encontrar o equilíbrio entre verbosidade e concisão, ou seja, o ponto onde o nome é longo o suficiente para ser claro, mas não tão extenso que torne a linha de código ilegível.
Funções pequenas e com responsabilidade única
Uncle Bob defende que funções devem fazer uma única coisa, fazê-la bem e fazê-la apenas. Quando uma função mistura validação, cálculo, persistência e exibição, ela se torna mais difícil de entender, testar e modificar (Martin, 2009).
A nomenclatura de funções também é algo muito importante, pois ela é a primeira camada de documentação do seu sistema. Uma função bem nomeada deve ser reveladora de intenção (assim como as variáveis). Se você precisa ler o corpo da função para entender o que ela faz, o nome da função falhou em legibilidade.
Segundo os princípios estabelecidos no clean code, um nome ideal possui as seguintes propriedades:
- Verbos e frases verbais: funções representam ações; logo, devem conter verbos.
- Abstração coerente: o nome deve refletir o nível de abstração em que a função opera.
- Evitar desinformação: nomes não devem sugerir algo que a função não entrega.
- Pronunciabilidade: se você não consegue discutir o nome em uma reunião de design sem parecer que está tendo um espasmo, o nome está errado.
Analisemos o Código 4 para entender porque ele é considerado um código ruim. Observe que a função processarOrdem valida, calcula, imprime, salva e envia e-mail para o consumidor. Ela é responsável por várias responsabilidades.
function processarOrdem(ordem) {
if (!ordem.items || ordem.items.length === 0) {
console.log("Pedido inválido");
return;
}
let total = 0;
for (const item of ordem.items) {
total += item.preco * item.quantidade;
}
console.log(`Total: R$ ${total}`);
salvarOrdemNoBancoDeDados(ordem, total);
enviarEmailConfirmacao(ordem.emailConsumidor, total);
}Refatorando o código original, obtemos uma separação das responsabilidades em várias funções, de forma que uma função chame outra para realizar aquela parte do processo e as responsabilidades sejam únicas.
function OrdemValida(ordem) {
return Array.isArray(ordem.items) && ordem.items.length > 0;
}
function calcularTotalOrdem(items) {
return items.reduce((total, item) => total + item.preco * item.quantidade, 0);
}
function processarOrdem(ordem) {
if (!OrdemValida(ordem)) {
console.log("Pedido inválido");
return;
}
const total = calcularTotalOrdem(ordem.items);
console.log(`Total: R$ ${total}`);
salvarOrdemNoBancoDeDados(ordem, total);
enviarEmailConfirmacao(ordem.emailConsumidor, total);
}O Código 5 ainda pode ser melhorado, mas a lógica principal ficou bem mais clara que a versão original. McConnell (2004) argumenta que decompor rotinas em partes menores reduz complexidade mental e facilita revisão e testes.
Evitar comentários que escondem código ruim
No senso comum, comentários parecem sempre positivos. Contudo, Martin sustenta que muitos comentários existem apenas para compensar nomes ruins, funções confusas ou estruturas mal organizadas. O ideal é que o código seja autoexplicativo, usando comentários apenas quando realmente agregarem contexto relevante, como decisões arquiteturais, restrições de negócio ou advertências importantes (Martin, 2009).
Exemplo ruim
// verifica se o item tem quantidade válida
if (cartItem.quantity > 0) {
// adiciona o item ao carrinho
addItemToCart(cartItem);
}Os comentários são redundantes.
Exemplo melhor
const hasValidQuantity = cartItem.quantity > 0;
if (hasValidQuantity) {
addItemToCart(cartItem);
}O nome da variável já comunica a intenção.
Quando comentário faz sentido?
// O sistema legado do caixa aceita valores monetários apenas em centavos inteiros.
function convertPriceToCents(price) {
return Math.round(price * 100);
}Nesse caso, o comentário registra uma restrição externa do sistema, não apenas repete o que o código faz.
Evitar duplicação
A duplicação é um dos sinais mais claros de design frágil. Hunt e Thomas observam, no princípio DRY (Don’t Repeat Yourself), que conhecimento duplicado tende a gerar inconsistência quando uma das cópias é modificada e outra não (Hunt; Thomas, 1999).
Exemplo com duplicação
function calculateElectronicsSubtotal(items) {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
return total;
}
function calculateGrocerySubtotal(items) {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
return total;
}A estrutura algorítmica é a mesma.
Exemplo refatorado
function calculateItemsSubtotal(items) {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}Agora a lógica está centralizada. Fowler mostra que remover duplicações é um dos passos mais importantes da refatoração, porque duplicação quase sempre indica oportunidade de abstração (Fowler, 2018).
Estruturas condicionais mais claras
Código limpo também envolve reduzir complexidade de decisões. Muitas cadeias longas de if...else prejudicam a leitura. Em JavaScript, isso pode ser melhorado com retornos antecipados, objetos de mapeamento e polimorfismo quando apropriado.
Exemplo ruim
function getShippingFee(customerType) {
if (customerType === "regular") {
return 20;
} else if (customerType === "premium") {
return 10;
} else if (customerType === "vip") {
return 0;
} else {
return 25;
}
}Exemplo melhor
function getShippingFee(customerType) {
const shippingFees = {
regular: 20,
premium: 10,
vip: 0
};
return shippingFees[customerType] ?? 25;
}Esse formato reduz ruído e facilita extensão. Beck associa design simples à remoção de estruturas desnecessariamente verbosas quando há uma forma mais clara de expressar a mesma ideia (Beck, 2003).
Tratamento de erros com clareza
Erros não devem ser ignorados nem misturados com fluxo normal. Martin argumenta que separar o tratamento excepcional do fluxo principal melhora a legibilidade do código (Martin, 2009).
Exemplo ruim
function parseCartItem(json) {
try {
const item = JSON.parse(json);
if (!item.name) {
return null;
}
return item;
} catch {
return null;
}
}O retorno null esconde causas diferentes: ou o JSON é inválido ou ocorreu ausência de nome.
Exemplo melhor
function parseCartItem(json) {
const item = JSON.parse(json);
if (!item.name) {
throw new Error("O campo 'name' do item é obrigatório.");
}
return item;
}Nesse cenário, o uso ficaria assim:
try {
const item = parseCartItem(jsonInput);
addItemToCart(item);
} catch (error) {
console.error("Falha ao processar item do carrinho:", error.message);
}Esse modelo distingue erro de validação de fluxo normal. Em sistemas maiores, isso melhora observabilidade e depuração.
Funções com poucos parâmetros
Muitos parâmetros tornam o uso da função mais difícil de entender e aumentam a chance de erro. Martin considera que listas longas de parâmetros são um indicativo de design inadequado, especialmente quando vários deles são do mesmo tipo ou opcionais (Martin, 2009).
Exemplo ruim
function adicionarItem(carrinho, nome, preco, quantidade, categoria, desconto, importado) {
const subtotal = preco * quantidade * (1 - desconto);
carrinho.push({
nome,
preco,
quantidade,
categoria,
desconto,
importado,
subtotal
});
}
const carrinho = [];
adicionarItem(carrinho, "Mouse", 80, 2, "Informática", 0.1, false);Exemplo melhor
function calcularSubtotal(item) {
return item.preco * item.quantidade * (1 - item.desconto);
}
function adicionarItem(carrinho, item) {
const subtotal = calcularSubtotal(item);
carrinho.push({
...item,
subtotal
});
}
const carrinho = [];
const produto = {
nome: "Mouse",
preco: 80,
quantidade: 2,
categoria: "Informática",
desconto: 0.1,
importado: false
};
adicionarItem(carrinho, produto);
console.log(carrinho);Em JavaScript, o uso de objeto como parâmetro melhora legibilidade, evita erros de ordem e favorece extensibilidade.
Consistência importa mais do que esperteza
Um erro comum em JavaScript é escrever código “engenhoso”, curto demais ou excessivamente implícito. Em muitos contextos, a clareza deve prevalecer sobre a concisão. McConnell destaca que código deve ser escrito para seres humanos, não apenas para o compilador ou interpretador (McConnell, 2004).
Exemplo menos claro
const r = cart.items.filter(i => i.q > 0).map(i => i.n);Exemplo melhor
const availableItemNames = cart.items
.filter(item => item.quantity > 0)
.map(item => item.name);O segundo caso é mais verboso, mas a intenção é muito mais evidente.
Organização de módulos e separação de responsabilidades
Clean code não se limita à linha de código. A organização de arquivos, módulos e camadas também importa. Fowler e Martin convergem na ideia de que sistemas sustentáveis dependem de responsabilidades bem distribuídas, evitando que regras de negócio, acesso a dados e apresentação fiquem misturados (Martin, 2009; Fowler, 2018).
Exemplo ruim
app.post("/cart/items", async (req, res) => {
const { name, price, quantity } = req.body;
if (!name || !price || !quantity) {
return res.status(400).json({ error: "Dados inválidos" });
}
const existingItem = await db.collection("cartItems").findOne({ name });
if (existingItem) {
return res.status(409).json({ error: "Item já existe no carrinho" });
}
await db.collection("cartItems").insertOne({ name, price, quantity });
res.status(201).json({ message: "Item adicionado ao carrinho com sucesso" });
});Aqui a rota concentra validação, regra de negócio e persistência.
Exemplo melhor
// arquivo cartRepository.js
export async function findCartItemByName(db, name) {
return db.collection("cartItems").findOne({ name });
}
export async function createCartItem(db, item) {
return db.collection("cartItems").insertOne(item);
}// arquivo cartService.js
import { findCartItemByName, createCartItem } from "./cartRepository.js";
export async function registerCartItem(db, { name, price, quantity }) {
if (!name || !price || !quantity) {
throw new Error("Dados inválidos");
}
const existingItem = await findCartItemByName(db, name);
if (existingItem) {
throw new Error("Item já existe no carrinho");
}
return createCartItem(db, { name, price, quantity });
}// arquivo cartController.js
import { registerCartItem } from "./cartService.js";
export async function createCartItemHandler(req, res) {
try {
await registerCartItem(req.db, req.body);
res.status(201).json({ message: "Item adicionado ao carrinho com sucesso" });
} catch (error) {
res.status(400).json({ error: error.message });
}
}Essa separação favorece testes, manutenção e reuso.
Testes também sustentam código limpo
Embora clean code não seja sinônimo de testes automatizados, há forte relação entre ambos. Beck e Martin associam código limpo à facilidade de teste. Código muito acoplado, extenso ou ambíguo tende a ser difícil de testar. Já um design mais simples e modular favorece TDD e testes unitários claros (Beck, 2003; Martin, 2009).
Exemplo de função fácil de testar
export function calculateCartItemTotal(price, quantity, discountRate) {
if (price < 0) {
throw new Error("Preço inválido");
}
if (quantity <= 0) {
throw new Error("Quantidade inválida");
}
if (discountRate < 0 || discountRate > 1) {
throw new Error("Taxa de desconto inválida");
}
return price * quantity * (1 - discountRate);
}Exemplo de teste:
import { calculateCartItemTotal } from "./cart.js";
test("deve calcular corretamente o total do item com desconto", () => {
expect(calculateCartItemTotal(100, 2, 0.1)).toBe(180);
});A função é pura, pequena e previsível. Isso é um bom indício de design limpo.
Vantagens do clean code
A principal vantagem é a manutenibilidade. Sistemas reais vivem muito mais tempo sendo mantidos do que sendo escritos do zero. Código limpo reduz o custo de alteração, correção e evolução. Outra vantagem é a comunicação entre desenvolvedores, pois o código passa a funcionar também como documentação viva. Há ainda impacto direto sobre testes, depuração, onboarding de novos membros e redução de defeitos causados por interpretações equivocadas do código (Martin, 2009; McConnell, 2004).
Em equipes, clean code também melhora previsibilidade do projeto. Quanto menor a complexidade acidental, mais fácil estimar, revisar e evoluir funcionalidades.
Desvantagens, limites e trade-offs
É importante evitar um discurso ingênuo. Clean code não significa “código bonito” em sentido subjetivo. Também não significa abstrair tudo, criar muitas camadas ou fragmentar excessivamente o sistema. O excesso de zelo pode gerar abstrações prematuras, microfunções sem contexto e arquitetura desnecessariamente sofisticada.
Outro trade-off está no tempo de desenvolvimento. Em contextos de pressão, pode parecer mais rápido “resolver logo”. No entanto, Fowler mostra que ignorar refatoração apenas empurra custo para o futuro, normalmente com juros altos na manutenção (Fowler, 2018). O ponto de equilíbrio está em aplicar limpeza com critério, priorizando legibilidade e evolução real, não perfeccionismo estético.
Em JavaScript, há ainda um dilema adicional: recursos modernos da linguagem podem produzir código elegante, mas também mais opaco para quem não domina certas construções. Assim, o melhor código nem sempre é o mais curto ou mais moderno; é o mais claro para a equipe e para o contexto do projeto.
Conclusão
O design de código com clean code não deve ser considerado uma receita rígida e infalível, mas sim uma lista de propostas para engenharia de sistemas. Apesar de diferenças aqui e ali, os autores clássicos convergem na ideia de que código deve comunicar intenção, minimizar complexidade, evitar duplicação e facilitar mudanças futuras.
O ponto central ao desenvolver-se sistemas complexos não é apenas “funcionar”, mas funcionar de uma forma que preserve a capacidade de compreensão e evolução do software. Em termos profissionais, escrever código limpo é assumir que manutenção não é acidente; é parte central do desenvolvimento.
Obrigado pela leitura e bons estudos!
Referências
BECK, Kent. Test-driven development: by example. Boston: Addison-Wesley, 2003.
FOWLER, Martin. Refactoring: improving the design of existing code. 2. ed. Boston: Addison-Wesley, 2018.
HUNT, Andrew; THOMAS, David. The pragmatic programmer: from journeyman to master. Boston: Addison-Wesley, 1999.
MARTIN, Robert C. Clean code: a handbook of agile software craftsmanship. Boston: Prentice Hall, 2009.
MCCONNELL, Steve. Code complete. 2. ed. Redmond: Microsoft Press, 2004.


