Design de código e Clean Code: como escrever um código limpo?

Publicado por

Publicado por

Publicado em

    Categorias:

    Desenvolvimento
    Qualidade

É comum que desenvolvedores passem mais tempo tentando entender um código ruim do que efetivamente escrevendo código novo. A proporção média de leitura e escrita de um código fonte é 10:1. Isto é, passamos 10 vezes mais tempo na tentativa de compreensão de um código já existente do que realizando um novo código devido a princípios mal estabelecidos.

Robert Cecil Martin, também conhecido como Uncle Bob, é uma referência de boas práticas na escrita de códigos. O autor atua desde a década de 70 na área tech e é um dos profissionais por trás do manifesto ágil (2001). Em 2008 ele lançou o livro Clean Code, no qual apresenta técnicas para desenvolvimento de software que se associa aos princípios agile.

O que pode-se destacar como grande descoberta da obra é: o gargalo principal de desenvolvimento de software está na manutenção. Ou seja: um código que funciona, mas está mal escrito desde a primeira versão, pode gerar prejuízos enormes.

Abaixo listamos alguns dos principais pilares para que o código se mantenha de fácil entendimento e limpo:

Nomenclatura: nomes são importantes!

Nomeie as variáveis de forma descritiva, indicando o que ela armazena. Por exemplo, uma variável chamada nc não diz nada. Porém, o nome de variável nomeCompleto é bem mais sugestivo para quem está olhando o código e realizará a manutenção.

Nomes de funções, classes e métodos também devem seguir os mesmos princípios de nomenclatura de variáveis.

// before
interface p {
  n: string;
  d: boolean;
  v: number;
}

function myFunc(array: p[]) {
  const x: p[] = [];
  for (const v of array) {
    if (v.d) {
      x.push(v);
    }
  }
  return x;
}
 
// after
interface Product {
  name: string;
  isDeleted: boolean;
  value: number;
}

function filterDeletedProducts(products: Product[]) {
  const deletedProducts: Product[] = [];
  for (const product of products) {
    if (product.isDeleted) {
      deletedProducts.push(product);
    }
  }
  return deletedProducts;
}
 
// more improvements
function filterDeletedProducts(products: Product[]) {
  return products.filter((product) => product.isDeleted);
}
 

Deixe o código mais limpo do que estava

Ao realizar manutenção/alterações em um código já existente, tente deixá-lo mais semântico do que o encontrou. Ao refatorar nomes de variáveis, quebrando funções grandes em funções menores, remova comentários obsoletos, por exemplo. O mais importante é nunca deixar em um estado pior do que estava.

// before
async function createUser(userData: UserDTO): Promise<User> {
  if (!userData.name){
    throw new Error('Name is required');
  }
  if (!userData.cpf){
    throw new Error('CPF is required');
  }
  if (!userData.phoneNumber){
    throw new Error('Phone number is required');
  }

  const createdUser = await User.query().insertAndFetch({
    ...userData
    createdAt: new Date().toISOString(),
    status: 'enabled',
  });
  return createdUser;
}
// after
async function createUser(userData: UserDTO): Promise<User> {
  await throwsErrorIfUserDataIsInvalid(userData);
  const createdUser = await insertAndFetchUser(userData);
  return createdUser;
}
 

Evite repetições

O ideal é não repetir códigos. Se você estiver repetindo código em locais diferentes, algo pode ser melhorado. As mudanças no código precisam ocorrer em todos os locais em que o código está se repetindo, evitando ambiguidade de ideias.

 

Funções pequenas e com uma responsabilidade

Os nomes de funções devem seguir o mesmo padrão de nomes de variáveis - semânticas e descritivas. As funções devem ter poucas e, se possível, apenas uma responsabilidade. Funções menores e com poucos fluxos alternativos podem ser reutilizadas em outros locais do código. Assim, repetições são evitadas.

// before
function emailClients(clients) {
  clients.forEach((client) => {
    const clientRecord = database.get(client);
    if (clientRecord.isActive) {
      sendEmail(client);
    }
  });
}
// after
function emailActiveClients(clients) {
  clients.filter(isActiveClient).forEach(sendEmail);
}

function isActiveClient(client) {
  const clientRecord = database.get(client);
  return clientRecord.isActive;
}
 

Comentários

Evite comentários ao máximo! Um código bem escrito não precisa de diversas linhas com comentários extensos. A questão principal é: geralmente, o código é atualizado, mas os comentários não, o que pode gerar muita confusão.

Um código limpo diz por si só o que está sendo realizado.

// before
// verifica os dados do usuario e lança erro se forem invalidos
function throwsErrorIfUserDataIsInvalid (userData: UserDTO) {
  // lanca erro se nao for um email valido
  if (!isValidEmail(userData.email)) {
    throw new Error('Invalid email');
  }
  // lanca erro se nao for um cpf valido
  if (!isValidCpf(userData.cpf)) {
    throw new Error('Invalid cpf');
  }
  // lanca erro se nao for um telefone valido
  if (!isValidPhoneNumber(userData.phoneNumber)) {
    throw new Error('Invalid phone number');
  }
}
// after 
function throwsErrorIfUserDataIsInvalid (userData: UserDTO) {
  if (!isValidEmail(userData.email)) {
    throw new Error('Invalid email');
  }
  if (!isValidCpf(userData.cpf)) {
    throw new Error('Invalid cpf');
  }
  if (!isValidPhoneNumber(userData.phoneNumber)) {
    throw new Error('Invalid phone number');
  }
}
 

Tratamento de erros

Idealmente, deve-se tratar de forma específica os locais onde é possível ocorrer erros. Uma vez que eles são imprevisíveis, lidar com eles de forma não genérica pode evitar horas e horas de 'debug' em busca de 'bugs'.

// before
try {
  functionThatThrow();
} catch (error) {
  console.log(error);
}
// after
try {
  functionThatThrow();
} catch (error) {
  console.error(error);
  notifyUserError(error);
  reportErrorToService(error);
}

Testes

Testes devem ser capazes de serem executados de forma rápida, independente e repetida, testando condições determinísticas e pré acompanhando o desenvolvimento do software. Desse modo, conseguimos garantir mais qualidade no que está sendo desenvolvido. A qualidade do que está sendo desenvolvido é mais possível de ser garantida

Assim, mesmo que um sistema esteja pronto e funcionando, ele não está finalizado. Sempre haverá necessidades de atualizações e implementação de novas funcionalidades, pois o código envelhece e pode se tornar obsoleto.

Em suma, falar sobre código limpo e de fácil manutenção, é criar um código com Baixo Acoplamento, Alta Coesão, usando SOLID, aplicando Design Patterns, minimizando Side Effects, maximizar o uso de Funções Puras e várias outros princípios. Desse modo, fazer um bom Design de Código é uma parte essencial para a manutenção de código.