Engenharia de Software: Princípios de Código Limpo

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:

  1. Revelar a intenção: o nome deve responder por que a variável existe, o que ela faz e como é usada.
  2. Evitar desinformação: nomes não devem sugerir um tipo ou propósito diferente do real.
  3. 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 como Data ou Info. 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 fetch para buscar dados em um serviço, não use retrieve ou get em 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.

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.