Executando verificação de segurança...
0

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 "porquê" 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;
}

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));

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 "porquê".

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.

Carregando publicação patrocinada...
8

Cada vez mais está caindo o conceito de código limpo. e aqui vou fazer algumas críticas ao seu código:

/**
 * 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);
}

Esse comentário é totalmente desnecessário.

function calculateOrderTotal(items: OrderItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

Somente o código sozinho já deixa claro o que o método está fazendo.

A assinatura da função dispensa completamente o uso do comentário, dá pra entender completamente o que é feito.

Em um projeto eu vou perder 5 a 10 minutos comentando todas as funções? Jamais.

Agora trago exemplos de casos reais:

 public SignContractService(
    SignContractRepository scRepository,
    ValidationResponseTransformer transformer,
    OracleObjectStorageService oracleOsService,
    UploadService uploadService,
    ContractRenderPdfService contractRenderPdfService,
    CompanySignatureRepository companySignatureRepository,
    AppDbContext dbContext)
{
    this.scRepository = scRepository;
    this.transformer = transformer;
    this.oracleOsService = oracleOsService;
    this.uploadService = uploadService;
    this.contractRenderPdfService = contractRenderPdfService;
    this.companySignatureRepository = companySignatureRepository;
    this.dbContext = dbContext;
}

public async Task<DefaultResponseDTO> SignContract(
    Guid eventId,
    Guid eventRoleCollaboratorId,
    Guid contractId,
    SignContractDTO signContractDTO)
{
    var roleInvalid = await dbContext.CollaboratorEventRoles
        .Where(cer => dbContext.EventRoleCollaborators
            .Where(erc => erc.CollaboratorId == cer.CollaboratorId)
            .Where(erc => erc.EventEventRole.EventRoleId == cer.EventRoleId)
            .Any(erc => erc.Id == eventRoleCollaboratorId)
        ).AnyAsync(cer => cer.Status != Enums.Collaborator.CollaboratorEventRoleStatus.Active);

    if (roleInvalid) return transformer.GenerateDefaultErrorResponse("Cargo com pendências, não é possível assinar o contrato");

    var contract = await scRepository.GetContractAsync(eventRoleCollaboratorId, contractId);
    if (contract == null) return transformer.GenerateDefaultErrorResponse("Contrato não encontrado");

    var signaturePath = $"event/{eventId}/collaborator/{eventRoleCollaboratorId}/contract/{contractId}/signature.png";
    var contractPath = $"event/{eventId}/collaborator/{eventRoleCollaboratorId}/contract/{contractId}/contract.pdf";
    await oracleOsService.MoveFile(
        uploadService.GetUploadPath(signContractDTO.signatureId),
        signaturePath
    );

    var collaboratorSignature = await oracleOsService.DownloadFile(signaturePath);
    if (collaboratorSignature == null) return transformer.GenerateDefaultErrorResponse("Erro ao baixar a assinatura do colaborador");

    var currentCompanySignature = await companySignatureRepository.GetCurrentSignature();
    if (currentCompanySignature == null) return transformer.GenerateDefaultErrorResponse("A assinatura da empresa não está cadastrada");

    var companySignature = await oracleOsService.DownloadFile(currentCompanySignature.Path);
    if (companySignature == null) return transformer.GenerateDefaultErrorResponse("Erro ao baixar a assinatura da empresa");

    var pdfBytes = await contractRenderPdfService.RenderContract(contract, eventRoleCollaboratorId, collaboratorSignature, companySignature);
    if (pdfBytes == null) return transformer.GenerateDefaultErrorResponse("Erro ao gerar o PDF do contrato");

    var uploaded = await oracleOsService.UploadFile(contractPath, pdfBytes, "application/pdf");
    if (!uploaded) return transformer.GenerateDefaultErrorResponse("Erro ao salvar o contrato assinado");

    var signedContractModel = new EventRoleCollaboratorContract
    {
        EventRoleCollaboratorId = eventRoleCollaboratorId,
        ContractModelId = contractId,
        signatureImgPath = signaturePath,
        signedContractPath = contractPath,
    };
    await scRepository.AddEventRoleCollaboratorContract(signedContractModel);

    return new DefaultResponseDTO();
}

Tamanho Ideal: Idealmente, uma função não deve ultrapassar 15-20 linhas. Se uma função está ficando longa, provavelmente está fazendo demais.

Essa função está gigante: Validando entrada do usuário, comunicando com 3 serviços, comunicando com 2 repositórios, fazendo query, salvando.

Te desafio a refatorar ela com Clean Code!

Vai ficar péssimo! ela está legível, cumpre o que faz, você que nunca viu o código sabe o que ela faz!

Considerações pessoais

É bom saber clean code? SIM!

Devemos levar ao pé da letra? Não!

Devemos saber quando quebrar a regra!

7

Concordo e gostaria de complementar: detesto essas regras que determinam um limite para a quantidade de linhas de uma classe, método, ou seja lá o que for.

Pra isso vou contar um caso real. Uma vez uma função ultrapassou esse limite, não lembro o número exato mas vamos dizer que era 30 linhas e ela ficou com 31. Ou seja:

class blabla {
    // vários métodos...

    function fazAlgo() {
        // 31 linhas aqui dentro
    }

    // outros métdodos
}

Pra satisfazer a regra, tive que pegar um trecho qualquer (digamos que foi de 10 linhas) e jogar para outra função:

class blabla {
    // vários métodos...

    function fazAlgo() {
        // 21 linhas aqui dentro
        novaFuncao(); // nova função com 10 linhas tiradas daqui
    }

    function novoMetodo() {
        // 10 linhas que antes estavam em fazAlgo
    }

    // outros métodos...
}

Ou seja, o método original tinha 31 linhas e fiz o seguinte:

  • joguei 10 linhas para outro método (original fica com 21)
  • adicionei a chamada do novo método (original fica com 22)

Mas olha só, pra fazer isso eu tive que adicionar também a linha function novoMetodo() { e o respectivo fechamento }. E uma linha em branco antes e depois (regras de formatação do projeto). Ou seja, a classe como um todo ficou com mais linhas.

E ao fazer isso com vários métodos, o limite de linhas por classe também foi ultrapassado, ou seja, tive que criar uma classe auxiliar e mover alguns métodos para lá.

E no fim o código ficou pior porque não fazia sentido quebrar em métodos menores, já que tudo era parte da mesma operação. O mesmo vale para a classe auxiliar, que só ajudou a aumentar a complexidade desnecessariamente.

Tudo isso pra satisfazer uma regra arbitrária envolvendo um número mágico que alguém definiu.


Não me entenda mal, algum conjunto mínimo de regras e padrões devem existir para não virar bagunça. E tem vezes que faz todo sentido quebrar em métodos menores ou separar em outras classes (mas se vc fez isso somente por causa da quantidade de linhas, aí complicou à toa, na minha opinião).

E a partir do momento em que essas regras começam a atrapalhar, deve-se questionar se os benefícios compensam os problemas causados. Eu particularmente detesto essas regrinhas do tipo "máximo de linhas", "nunca use tal coisa", "quantidade máxima de parâmetros", etc. Tem que sempre avaliar caso a caso e não seguir cegamente só porque algum guru/influencer/livro famoso falou.