A clareza do código limpo

A clareza do código limpo

Como escrever códigos limpos e eficientes?

Escrever código limpo é semelhante a andar de bicicleta: podemos tentar aprender apenas estudando toda a mecânica para se andar de bicicleta, como gravidade, atrito, momento angular, centro de massa e assim por diante. Dado toda teoria e estudo, vamos concluir que andar de bicicleta é fácil. Porém, mesmo com toda a teoria o tombo é certo na primeira vez que tentarmos.

Programar não é diferente. Podemos por tudo no papel para escrever um código limpo e, então programar, certamente isso seria a mesma coisa que tentar andar de bicicleta a primeira vez, o tombo é certo!

Ou seja, sem teoria unida a prática, não existe um código limpo!

O Código

Para muitos, os códigos escritos simplesmente vão acabar! Que não serão mais necessários programadores e o código será gerado e não mais escrito. As pessoas criarão programas a partir de especificações.

As pessoas esperam descobrir um dia como criar máquinas que possam fazer o que desejamos em vez do que mandamos. Tais máquinas terão de ser capazes de nos entender tão bem de modo que possam traduzir exigências vagamente especificadas em programas executáveis perfeitos para satisfazer nossas necessidades.

Se nem os seres humanos, com toda a sua intuição e criatividade, têm sido capazes de criar sistemas bem sucedidos a partir das carências confusas de seus clientes, como uma máquina será capaz?

Lembre-se: Código é uma linguagem na qual expressamos nossos requisitos. Podemos até criar linguagens e ferramentas que consigam analisar as sintaxes e unir os requisitos em estruturas formais. Mas jamais vamos eliminar a precisão necessária, portanto, sempre haverá um código.

Código limpo

Para escrever um código limpo é necessário saber o que de fato é um código limpo. Segue algumas definições de figuras importantes para a história do desenvolvimento de software

"Gosto do meu código elegante e eficiente. A lógica deve ser direta para dificultar o encobrimento de bugs, as dependências mínimas para facilitar a manutenção, o tratamento de erro completo de acordo com uma estratégia clara e o desempenho próximo do mais eficiente de modo a não incitar as pessoas a tornarem o código confuso com otimizações sorrateiras. O Código limpo faz apenas uma coisa." - Bjarne Stroustrup, criador do C++ e autor do livro A linguagem de programação C++

"Um código limpo é simples e direto. Ele é tão bem legível quanto uma prosa bem escrita. Ele jamais torna confuso o objetivo do desenvolvedor, em vez disso, ele está repleto de abstrações claras e linhas de controle objetivas." - Grady Booch, autor do livro Object Oriented Analysis and Design with Applications

E como manter um código sempre limpo? Siga a regra do escoteiro: "Deixe a área do acampamento mais limpa do que você a encontrou". A refatoração não precisa ser grande, mudar nomes de métodos, variáveis e classes para nomes mais claros e objetivos já são melhorias significativas.

Escreva bons nomes

Talvez essa seja uma das coisas mais complicadas para a maioria dos desenvolvedores. Não é uma técnica, é questão de aprender. Dar bons nomes para as classes, métodos e variáveis exige muita prática e reflexão. Escolher bons nomes leva tempo, mas economiza mais. Portanto, cuide de seus nomes e troque-os quando encontrar melhores.

O nome de uma variável, função ou classe deve dizer exatamente porque aquele código existe, o que faz e como é usado. Se um nome requer um comentário, então não é um bom nome. Escolher bons nomes podem facilitar bastante o entendimento e a alteração do código.

//RUIM
const ddmmyyyy = format(date, 'dd/MM/yyyy')

//BOM 
const currentDate = format(date, 'dd/MM/yyyy')

"Você sabe que está criando um código limpo quando cada rotina que você lê é como você esperava".

Por fim, não tenha medo de escrever nomes longos, eles são melhores que um pequeno e enigmático. Um nome longo e descritivo é melhor que um comentário extenso e descritivo.

Utilize nomes pesquisáveis

Nomes apenas com uma letra ou números são complicados para buscar no código. Além de um número perdido não dizer absolutamente nada.

Para que serve o número 5?

wdpw = 5

É importante escrever um código legível e pesquisável. E constantes de escopo global devem ter letras maiúsculas.

const WORK_DAYS_PER_WEEK = 5

Evite o mapeamento mental

Apesar de parecer comum, muitas vezes por aprender a fazer laços de repetição com variáveis de apenas uma letra, como i,j ou k, sem dúvidas é uma péssima escolha. Na maioria das vezes é uma escolha ruim. É um mapeamento mental que o desenvolvedor que estiver lendo o código vai precisar fazer. Além disso, não há razão pior que usar o nome c só porque a e b já estão sendo utilizados.

const cars = ['gol', 'vectra', 'polo']

for (let i = 0; i < cars.length; i++) {
  text += cars[i] + "<br>";
}

Nomes de classes

Classes e objetos devem ser substantivos, como customer, wikipage e account. O nome de uma classe não deve ser um verbo.

Nomes de métodos

Os nomes de métodos devem ter verbos, como post, delete page ou save. Alguns prefixos são interessantes e até sugeridos por padrões como Javabeans. São eles set, get e is.

Uma palavra por conceito

Apesar de Fetch e Get serem equivalentes, uma boa prática e sempre utilizar o mesmo para todos os contextos e classes.

Contexto significativo

Ao ver essas palavras todas juntas, certamente você verá um contexto: FirstName, lastName, houseNumber, city, state e zipcode.

Claro é um endereço. Mas e se você visse somente state, saberia dizer que é parte de um endereço?

Sem dúvidas a melhor solução seria criar uma classe que contém essas variáveis como atributos vinculados ao contexto. Mas outra alternativa é escolher um bom prefixo, como addrFirstName, addrLastName, addrState, etc.

class Address {
  firstName: String;
  lastName: String;
  houseNumber: Number;
  city: String;
  state: String;
  zipcode: Number;
}

Muito cuidado com prefixos, não é por que sua aplicação se chama GAS STATION DELUXE que toda classe ou método precisa de um GSD na frente. Por mais bizarro que pareça para alguns, para outros pode ser comum, apesar de ser uma péssima prática.

Por mais que algumas vezes seja cansativo e trabalhoso, não deixe de pensar em bons nomes. E sempre que puder, melhore um nome já existente.

Funções

A primeira regra para funções é que elas devem ser PEQUENAS! Não temos um número exato de quantas linhas no máximo uma função deveria ter, mas acredito que com as abstrações e recursos das linguagens atuais, 20 linhas no máximo, seria um bom número.

Cada função deve possuir uma obviedade transparente. Cada função deve contar uma história. E cada uma, deve levar você a próxima em uma ordem atraente.

Funções não devem ser grandes e ter estruturas aninhadas. Portanto, o nível de indentação de uma função deve ser de, no máximo, um ou dois. Isso, é claro, facilita a leitura e compreensão das funções.

"As funções devem fazer uma coisa. Devem fazê-la bem. Devem fazer apenas ela!"

Uma estratégia para saber se uma função faz mais de "uma coisa" é se você pode extrair outra função dela. Quando uma função faz apenas uma coisa, por isolar apenas uma ação no código, ela se torna mais fácil de testada e refatorada.

//RUIM
function emailClients(clients) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

//BOM
function emailActiveClients(clients) {
  clients
    .filter(isActiveClient)
    .forEach(email);
}

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

Parâmetros de Funções

A quantidade ideal de parâmetros para uma função é zero (nulo). Caso não seja possível, utilize apenas um ou dois. Sempre que possível devem-se evitar três parâmetros. Os parâmetros dificultam os testes. Também evite parâmetros lógicos. Eles falam que a função está fazendo mais de uma coisa. Funções devem fazer apenas uma coisa.

//RUIM
function createFile(name, temp) {
  if (temp) {
    fs.create(`./temp/${name}`);
  } else {
    fs.create(name);
  }
}

//BOM
function createFile(name) {
  fs.create(name);
}

function createTempFile(name) {
  createFile(`./temp/${name}`);
}

Pode até parecer uma trapaça reduzir o número de parâmetros através de objetos, mas não é. Linguagens como JavaScript lhe permite criar objetos instantaneamente, sem precisar escrever muita coisa. Além de ser possível utilizar apenas as propriedades úteis para aquele contexto de código, utilizando a sintaxe de desestruturação do ES6.

//RUIM
function createMenu(title, body, buttonText, cancellable) {
  // ...
}
createMenu('Foo', 'Bar', 'Baz', true);

//BOM
function createMenu({ title, body, buttonText, cancellable }) {
  // ...
}

createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
});

Evite efeitos colaterais

Efeitos colaterais são mentiras. Uma função não pode prometer fazer apenas uma coisa e fazer outras coisas escondidas.

function checkPassword(userName, password) {
	const user = this.userGateway.findByName(userName);
  if (!user)
	  return false;
  
  
  const validPassword = this.verifyPasswordWithUser(user.password, password);
  if (validPassword) {
		this.setCookie();
    this.createAccessLog();
    return true;
  }

  return false;
	  
}

O efeito colateral é a chamada aos métodos setCookie e createAccessLog. O nome da função, diz que verifica a senha. O nome não indica que ela grava um cookie e insere um log de acesso. O efeito colateral cria um acoplamento temporário. Como deveria apenas validar uma senha, poderia ser chamado em qualquer lugar do código. Mas da forma como está estruturado, quando chamado fora de ordem, pode inserir mais logs que o necessário ou reescrever o cookie quando não deveria. Neste caso poderíamos renomear para checkPasswordAndSetCookieAndCreateAccessLog, embora isso violaria o principio que uma função deve fazer apenas uma coisa.

Prefira exceções a retorno de códigos de erro

Fazer funções retornarem códigos de erros é uma violação de boas práticas. Além de gerar uma estrutura aninhada e que obriga o chamador lidar imediatamente com o erro.

//RUIM
if (deletePage(page) === E_OK) {
	if (deleteReference(page.name) === E_OK) {
		if (deleteKey(page.name.makeKey()) === E_OK) {
			console.log("página excluída");
		} else {
			console.log("configKey não foi excluída")
		}
	} else {
		console.log("delete References não fo excluído do registro");
	}
} else {
	console.log("a exclusão falhou");
	return E_ERROR;
} 

//BOM
try {
	deletePage(page);
	deleteReference(page.name);
	deleteKey(page.name.makeKey());
} catch(e) {
	console.log(e.message);
}

Sempre ao iniciar o desenvolvimento, lembre-se: As funções são os verbos e as classes os substantivos do seu código.

Classes

Assim como as funções, elas devem ser PEQUENAS! E qual seria o tamanho máximo de uma classe? Para mensurar o tamanho de classes usamos uma medida diferente, devemos contar as responsabilidades. O principio da Responsabilidade Única (SRP - Single Responsibility Principle) afirma que uma classe ou módulo deve ter um, e apenas um, motivo para mudar. Este princípio nos dá uma definição de responsabilidade e uma orientação para o tamanho da classe. Estas devem ter apenas uma responsabilidade e um motivo para mudar.

Seguindo algumas convenções, uma classe deve começar com uma lista de variáveis. As públicas (public), estáticas (static) e constantes (constants), se existirem, devem vir primeiro. As funções públicas devem vir após a lista de variáveis.

Existem boas razões para usar herança e muitas para composição. Mas sempre prefira composição ao invés de herança. Um exemplo prático:

// RUIM
class Employee {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // ...
}

class EmployeeTaxData extends Employee {
  constructor(ssn, salary) {
    super();
    this.ssn = ssn;
    this.salary = salary;
  }
}

O exemplo é ruim porque Employees (Empregados) "tem" dados de impostos. EmployeeTaxData não é um tipo de Employee

// BOM
class EmployeeTaxData {
  constructor(ssn, salary) {
    this.ssn = ssn;
    this.salary = salary;
  }

  // ...
}

class Employee {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  setTaxData(ssn, salary) {
    this.taxData = new EmployeeTaxData(ssn, salary);
  }
  // ...
}

Comentários

Nada pode ser ser tão útil quanto comentários bem feitos. Nada consegue ser tão inútil quanto um comentário mal escrito e amontoado. Quando estiver em uma situação que acredite ser necessário um comentário, pense bem e veja se não há como se expressar através do código em si. Códigos claros e expressivos são de longe superiores a um amontoado de comentários. Ao invés de gastar tempo criando comentários para explicar um código mal feito, use-o para limpar o código.

Algumas regrinhas para comentários

  • Apenas comente códigos que tenham complexidade de lógica de negócio.
  • Não deixe código comentado. Controle de versão já fazem o papel de guardar o histórico do código removido.
  • Não comente registros de alterações. Assim como resolver o problema de códigos antigos removidos, o controle de versão, também faz esse papel com um simples git log.
  • Por fim, mas não menos importante, evite marcadores de posição. Eles criam ruídos e estregam a harmonia do código.

Testes

Testes deveriam ser considerados mais importantes que a entrega final. Se o seu código não possui testes ou não possui uma quantia adequada de testes, então toda vez que tiver alguma entrega, não terá certeza se você não quebrou alguma coisa. Decidir a quantidade adequada de testes para uma aplicação, é decisão do seu time, mas ter no mínimo 70% de cobertura, deveria ser obrigatório. Atualmente, não existem desculpas para escrever teste. Existem diversos frameworks de testes em JavaScript. Na Vórtx utilizamos o mocha em conjunto com o chai. (Veja mais como são os testes aqui na Vórtx). Caso seu time queira utilizar um método de desenvolvimento orientado a testes (TDD), ele deveria seguir algumas regrinhas básicas para não errar:

  • Não se deve escrever o código de produção até criar um teste de unidade de falhas.
  • Não se deve escrever mais de um teste de unidade do que o necessário para falhar, e não compilar é falhar.
  • Não se deve escrever mais códigos de produção do que o necessário para aplicar o teste de falha atual.

Escrever testes mal e porcamente só para chegar até a cobertura mínima, é o mesmo que fazer nada. Na verdade é até pior que não ter teste. O problema é que os testes devem ser alterados conforme o código de produção evolui. Quanto pior o teste, mais difícil será de mudá-lo. Quanto mais confuso for o código de teste, maiores são as chances do time perder mais tempo tentando entender o teste que desenvolvendo o código de produção.

E qual o segredo para um teste limpo? Legibilidade! E o que torna um código legível? O mesmo que torna todos os códigos legíveis: clareza, simplicidade e consistência de expressão. Em um teste você quer dizer muito com o mínimo de expressões possíveis.

E por fim, a clareza do código limpo

Alguns pontos que podemos sempre lembrar e ter como regrinhas bolso para chegarmos a um código limpo. São elas:

  • Efetue todos os testes
  • Refatore o código
  • Não crie repetição de código
  • Seja expressivo
  • Crie classes e métodos pequenos

Acredito que este artigo possa lhe ajudar de alguma forma a alcançar um código melhor e mais limpo. Seguir as regras e dicas de projeto simples podem realmente incentivar e possibilitar a criação de códigos mais limpos, eficientes e duradouros.

Referências

C. Martin, Robert. Código limpo: Habilidades práticas do Agile Software. primeira ed., Alta Books, 2008.

Clean Code JavaScript. Disponível em: https://github.com/felipe-augusto/clean-code-javascript. Último acesso em: 10 set. 2021.