DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Princípios do Clean Code

logotech

## Código Limpo: A Arte da Legibilidade e Manutenibilidade no Backend

No dinâmico mundo do desenvolvimento backend, onde a complexidade pode crescer exponencialmente, a qualidade do nosso código é um fator crítico para o sucesso. Um código bem escrito não apenas funciona corretamente, mas também é fácil de entender, manter e escalar. Este post explora três pilares fundamentais do \"Clean Code\": legibilidade, nomeação significativa e funções pequenas, demonstrando como aplicá-los com exemplos em TypeScript/Node.js.

Introdução: O Custo do Código \"Sujo\"

Imagine herdar uma base de código obscura, cheia de nomes de variáveis crípticos e funções gigantescas que parecem caixas-pretas. Aumentar a produtividade se torna uma tarefa árdua, e a introdução de novos bugs é quase inevitável. Esse cenário é o resultado direto da negligência com as boas práticas de codificação. Investir tempo na escrita de código limpo é um investimento no futuro do projeto, reduzindo o tempo de depuração, facilitando a colaboração entre desenvolvedores e diminuindo o custo total de manutenção.

Desenvolvimento: Os Pilares do Código Limpo

1. Legibilidade: Escrevendo para Humanos

O código é lido com muito mais frequência do que é escrito. Portanto, a prioridade máxima deve ser a clareza para quem o lê.

  • Formatação Consistente: Use um formatador de código (como Prettier) para garantir um estilo uniforme.
  • Espaçamento Adequado: Utilize espaços em branco para separar blocos de código e tornar a leitura mais fluida.
  • Comentários Estratégicos: Comente apenas o \"porquê\" de uma decisão complexa, não o \"o quê\" o código faz (isso deve ser claro pela própria leitura do código).

2. Nomenclatura Significativa: Nomes que Falam por Si

Nomes de variáveis, funções, classes e módulos devem revelar sua intenção. Evite abreviações ambíguas e nomes genéricos.

  • Seja Descritivo: userCount é melhor que uc. calculateTotalPrice é melhor que calc.
  • Evite Abstrações Enganosas: Não nomeie uma variável list se ela contém um Map.
  • Nomes de Funções Verbo-Substantivo: getUserById(userId: number) ou saveOrder(order: Order).

3. Funções Pequenas: Dividir para Conquistar

Funções devem fazer uma coisa e fazê-la bem. Funções pequenas são mais fáceis de entender, testar e reutilizar.

  • Princípio da Responsabilidade Única (SRP): Cada função deve ter uma única responsabilidade clara.
  • Tamanho Ideal: Idealmente, uma função não deve ultrapassar 15-20 linhas. Se uma função está ficando longa, provavelmente está fazendo demais.
  • Refatoração: Quebre funções grandes em unidades menores e com nomes descritivos.

Exemplos de Código (TypeScript/Node.js)

Vamos refatorar um exemplo hipotético para ilustrar essas práticas.

Antes (Código com Potenciais Problemas):

// Função que processa um pedido e envia email
function processOrder(data: any) {
  // Validação básica
  if (!data || !data.userId || !data.items || data.items.length === 0) {
    console.error(\"Invalid data\");
    return false;
  }

  let total = 0;
  for (const item of data.items) {
    total += item.price * item.quantity;
  }

  // Simula persistência no DB
  console.log(`Saving order for user ${data.userId} with total ${total}`);
  // Simula envio de email
  console.log(`Sending confirmation email to user ${data.userId}`);

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Depois (Código Refatorado com Boas Práticas):

/**
 * Representa um item em um pedido.
 */
interface OrderItem {
  productId: string;
  quantity: number;
  price: number;
}

/**
 * Representa a estrutura de dados de um pedido.
 */
interface Order {
  userId: number;
  items: OrderItem[];
  // outras propriedades relevantes como data, status, etc.
}

/**
 * Valida a estrutura básica de um objeto de pedido.
 * @param order - O objeto de pedido a ser validado.
 * @returns True se o pedido for válido, False caso contrário.
 */
function isOrderValid(order: Order | null | undefined): boolean {
  if (!order) return false;
  if (!order.userId) return false;
  if (!order.items || order.items.length === 0) return false;
  // Poderia adicionar validações mais específicas para cada item aqui
  return true;
}

/**
 * Calcula o valor total de um pedido com base nos seus itens.
 * @param items - Um array de itens do pedido.
 * @returns O valor total calculado.
 */
function calculateOrderTotal(items: OrderItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

/**
 * Simula a persistência de um pedido no banco de dados.
 * @param orderDetails - Detalhes do pedido a serem salvos.
 */
async function saveOrderToDatabase(orderDetails: { userId: number; totalAmount: number }): Promise<void> {
  console.log(`Persistindo pedido para usuário ${orderDetails.userId} no valor de ${orderDetails.totalAmount}`);
  // Lógica real de persistência no banco de dados iria aqui.
  // Exemplo: await db.orders.insert(orderDetails);
  await new Promise(resolve => setTimeout(resolve, 50)); // Simula latência I/O
}

/**
 * Envia um email de confirmação para o usuário.
 * @param userId - O ID do usuário.
 * @param totalAmount - O valor total do pedido.
 */
async function sendOrderConfirmationEmail(userId: number, totalAmount: number): Promise<void> {
  console.log(`Enviando email de confirmação para o usuário ${userId} com total ${totalAmount}`);
  // Lógica real de envio de email iria aqui.
  // Exemplo: await emailService.sendConfirmation(userId, totalAmount);
  await new Promise(resolve => setTimeout(resolve, 50)); // Simula latência I/O
}

/**
 * Processa um pedido, orquestrando as etapas de validação, cálculo e persistência.
 * @param orderData - Os dados brutos do pedido.
 * @returns Uma Promise que resolve para true se o processamento for bem-sucedido, false caso contrário.
 */
async function processOrderClean(orderData: Order): Promise<boolean> {
  if (!isOrderValid(orderData)) {
    console.error(\"Falha ao processar pedido: Dados inválidos.\");
    return false;
  }

  const totalAmount = calculateOrderTotal(orderData.items);

  try {
    // Concorrência para otimizar I/O
    await Promise.all([
      saveOrderToDatabase({ userId: orderData.userId, totalAmount }),
      sendOrderConfirmationEmail(orderData.userId, totalAmount),
    ]);
    console.log(`Pedido processado com sucesso para o usuário ${orderData.userId}.`);
    return true;
  } catch (error) {
    console.error(`Erro ao processar pedido para o usuário ${orderData.userId}:`, error);
    return false;
  }
}

// Exemplo de uso:
const sampleOrder: Order = {
  userId: 123,
  items: [
    { productId: \"abc\", quantity: 2, price: 10.50 },
    { productId: \"def\", quantity: 1, price: 25.00 },
  ],
};

processOrderClean(sampleOrder)
  .then(success => console.log(\"Resultado final do processamento:\", success))
  .catch(err => console.error(\"Erro inesperado:\", err));
Enter fullscreen mode Exit fullscreen mode

No código refatorado:

  • Usamos interface para definir estruturas de dados claras.
  • A função processOrder foi decomposta em isOrderValid, calculateOrderTotal, saveOrderToDatabase, sendOrderConfirmationEmail.
  • Os nomes são descritivos (orderData, totalAmount, userId).
  • A função principal processOrderClean agora orquestra chamadas a funções menores e com responsabilidades únicas.
  • Utilizamos async/await e Promise.all para otimizar operações de I/O, demonstrando um padrão comum em Node.js.
  • Comentários explicam o \"porquê\" (ex: otimização de I/O) e não o \"o quê".

Conclusão: Um Investimento Contínuo

Adotar práticas de código limpo, como a legibilidade, nomeação significativa e a criação de funções pequenas, não é uma tarefa única, mas um compromisso contínuo. Ao priorizar esses princípios, construímos sistemas backend mais robustos, fáceis de manter e que promovem um ambiente de desenvolvimento colaborativo e produtivo. Lembre-se: um código limpo é um código que respeita o tempo e o esforço dos seus futuros mantenedores.

Top comments (0)