loading...

A sutil arte de escrever funções limpas.

lucascprazeres profile image Lucas dos Prazeres ・10 min read

Introdução.

Nos mais diversos paradigmas e linguagens utilizados hoje em dia, as funções são elementos importantíssimos e podem definir, a depender do modo como são feitas, a saúde ou precariedade de um sistema. Escrevê-las bem é um desafio de organização, compreensão de responsabilidades e até comunicação, para pessoas desenvolvedoras!

Por isso, trago a vocês o segundo artigo da série clean code (se você ainda não leu o primeiro, acesse esse link), onde vamos entender, afinal, o que são funções "limpas".

Bons programas são funções bem pequenas.

Isso não é necessariamente óbvio para pessoas com pouca experiência em programação, mas para você que já fez todo o tipo de engenhoca, sim. Funções grandes ferem boa parte das boas práticas em programação e são ótimas em colecionar bugs e estruturas estranhas e indecifráveis (e, claro, são SUPER cansativas de serem lidas). Por essa razão, é recomendado que funções tenham algo em torno de 1 a 20 linhas, o que vai soar como um exagero, até que você experimente nos seus códigos.

Vamos ver exemplo prático disso. Veja essa função "amoeba" que valida CPFs.

function validate(cpf) {
  if (!cpf) return false;

  const sanitizedCPF = cpf.replace(/\D+/g, '');

  const seq = cpf[0].repeat(sanitizedCPF.length);

  if (sanitizedCPF === seq) return false;

  if (cpf.match(/[a-zA-Z!@#$%^&*(),?":{}|<>]/g)) return false;

  let cpfWithoutPrefix = sanitizedCPF.slice(0, 9);

  let firstLastingDigitOnCPF;

  let factor = cpfWithoutPrefix.length + 1;
  let sum = 0;
  for (let i in cpfWithoutPrefix) {
    sum += cpfWithoutPrefix[i] * factor;
    factor--;
  }

  firstLastingDigitOnCPF = 11 - (sum % 11);

  let secondLastingDigitOnCPF;

  cpfWithoutPrefix += firstLastingDigitOnCPF;

  factor = cpfWithoutPrefix.length + 1;
  sum = 0;
  for (let i in cpfWithoutPrefix) {
    sum += cpfWithoutPrefix[i] * factor;
    factor--;
  }
  secondLastingDigitOnCPF = 11 - (sum % 11);

  const validCPF = cpfWithoutPrefix + secondLastingDigitOnCPF;

  return sanitizedCPF == validCPF;
};

Apesar de esse exemplo seguir uma sequência lógica de etapas, ainda é difícil definir quais são elas e quais os seus conceitos e detalhes.

Agora, veja a versão refatorada dessa mesma função, na forma de um método estático da recém criada classe CPF:

validate(cpf) {
    const sanitizedCPF = CPF.sanitize(cpf)
    if (!cpf) return false;
    if (CPF.isSenquence(sanitizedCPF)) return false;
    if (CPF.hasInvalidChars(sanitizedCPF)) return false;

    const validCPF = CPF.generateValidCpfFrom(sanitizedCPF);

    return sanitizedCPF === validCPF;
};

Vamos ver alguns conceitos que vão deixar suas funções mais parecidas com o segundo exemplo.

Nomes descritivos

Eu sei que já falamos sobre isso, antes, mas a importância da nomenclatura em funções nunca será reforçada o suficiente. O motivo disso? Bons programas devem ser lidos como uma narrativa, que conta a história daquele sistema e o seu funcionamento, de maneira lógica e fluida. Afinal,

"Programação é e sempre foi, a arte do design de linguagem"

Além dos nomes, outro importante importante recurso de comunicação com o leitor é o uso da sintaxe da linguagem de programação que você está utilizando. Um exemplo disso é o if (se, em inglês) que fica muito mais comunicativo quando o conectamos com uma função que descreve uma condição, como em:

if (CPF.hasInvalidChars(cpf)) {
  return false
}

Além do mais, nomes de funções deveriam conter informações o suficiente sobre as mesmas, para que não seja preciso consultar a sua declaração. Ou seja, o que ela faz, qual seu retorno, e quais os seus parâmetros.

O que exatamente o nome dessa função indica?

const email = getEmailAddresssFromUserObject(user);
  • Que o retorno dessa função é uma string contendo o email de um usuário

  • Que a sua função é apenas informar o email

  • Que ela recebe um objeto de usuário como parâmetro

Economizamos uma ida à declaração dessa função, não?

Crie pares de verbo/substantivo nas suas funções.

Quando as suas funções recebem um ou mais parâmetros, é interessante fazer com que os nomes da função e de suas entradas contem uma história. Para ilustrar essa ideia, temos o seguinte caso.

write(name)

Esses dois elementos combinam tão bem que é possível saber de cara que, seja lá o que name for, ele está sendo escrito.

Para melhorar ainda mais, poderíamos incluir

writeField(name)

Agora, sabemos que name é um campo a ser escrito!

Uma função deveria informar algo ou fazer algo. Nunca as duas coisas.

É muito comum criarmos funções com o que se chama de argumentos de saída, que alteram o valor da variável passada por parâmetro e não retornam nada. Não há problema algum em fazer isso, desde que você deixe claro que é isso o que a função faz

appendFooter(s)

você consegue me dizer, com certeza, se essa função inclui um footer na variável s, ou se ela inclui o valor de s como um footer?

Esse problema pode ser corrigido através de dois caminhos. O primeiro deles seria renomear a função para appendFooterOn(s) ou appendFooterElement(s). A segunda seria utilizar o conceito de .this, da Orientação a Objetos. Assim, a própria classe serve como identificação do retorno da função, como em:

htmlPage.appendFooter(s);

Sendo assim, é muito importante definir se a sua função vai retornar (informar) um valor, ou alterá-lo. Caso contrário, seus sistemas serão cada vez mais ambíguos e difíceis de se interpretar.

Faça uma coisa, faça-a bem e faça-a apenas.

Cada função em seu sistema deveria ter de se preocupar em fazer uma única coisa. Assim, ela pode ser a especialista nesse assunto e ser facilmente reconhecida por isso. Isso pode ser visto nos exemplos da validação de cpf. A primeira função tinha muitas tarefas e etapas, enquanto a segunda, se preocupava apenas em validar o cpf informado.

Existem algumas técnicas para sabermos quando uma função está fazendo uma única coisa ou não. Vamos conhecê-las.

Verificar o nível de abstração.

De maneira simples, o nível de abstração de um código se dá por quantos detalhes ele conhece sobre o que está fazendo e, quanto mais subirmos nesse nível, mais simples os conceitos se parecem.

Para entender isso melhor, vamos utilizar um trecho da função que valida CPF's

const sanitizedCPF = cpf.replace(/\D+/g, '');

const seq = cpf[0].repeat(sanitizedCPF.length);

if (sanitizedCPF === seq) return false;

É aqui que validamos se o cpf informado é uma sequência de números repetidos, como em 999.999.999-99, caso em que seria invalidado imediatamente.

Para fazer essa verificação, extraimos os pontos e traços do cpf, criando a string sanitizedCPF. Depois diso, criamos uma sequência com o primeiro caractere do cpf, utilizando a função repeat, do javascript, que vai gerar o valor considerado inválido.

Comparamos o valor recebido e o valor inválido e temos um resultado.

Isso é um baixo nível de abstração.

Agora, no segundo caso. Só precisamos chamar os métodos internos sanitize e isSequence e, em um passe de mágica, executamos a mesma lógica, de forma abstraída.

const sanitizedCPF = CPF.sanitize(cpf)
if (CPF.isSenquence(sanitizedCPF)) return false;

Por isso, você deve se certificar de que os elementos internos da sua função mantenham um padrão onde todos possuam o mesmo nível de detalhamento em relação ao que estão fazendo.

A regra do parágrafo.

Outra forma muito interessante de descobrir quando uma função tem apenas uma responsabilidade, é tentar expressar o que ela faz por meio de um parágrafo PARA. A exemplo disso:

CPF.isSenquence(cpf)

// PARA verificar se um cpf é uma sequência, comparamos o valor numérico do cpf informado, com uma string gerada pela repetição do primeiro caractere do mesmo e retornamos o resultado.

Caso essa sentença precise de um além de ou algo semelhante, sabemos que a função tem mais de uma responsabilidade.

Granularidade.

Fora as duas útimas regras vistas, temos, também, uma terceira opção para identificar quanta responsabilidade uma função carrega.

Se você conseguir extrair dela uma segunda função, que não seja simplesmente uma outra forma de expressar o que o código dela já diz, então ainda há tarefas demais.

Um caso em que seria possível fazer essa extração é

function generateRandomList(length) {
  const list = [];

  let cont = 0;

  while (cont < length) {
    let randomCharCode = Math.floor(Math.random() * (122 - 97) + 97);

    let randomChar = String.fromCharCode(randomCharCode);

    list.append(randomChar);
  }

  return list;
}

em que o bloco dentro da estrutura while poderia ser facilmente substituido por

function generateRandomCharList(length) {
  const list = [];

  let cont = 0;

  while (cont < length) {
    let randomChar = generateRandomChar();

    list.append(randomChar);
  }

  return list;
}

function generateRandomChar() {
  const randomCharCode = Math.floor(Math.random() * (122 - 97) + 97);

  const randomChar = String.fromCharCode(randomCharCode);

  return randomChar;
}

Ou, até mesmo

// ...

function generateRandomChar() {
  const randomCharCode = generateRandomDigitStartingAndEndingIn(97, 122);

  const randomChar = String.fromCharCode(randomCharCode);

  return randomChar;
}

function generateRandomDigitStartingAndEndingIn(min, max) {
  return Math.floor(Math.random() * (max - min) + min);
}

Com esses exemplos, é possível perceber que, quanto menor e mais específica uma função for, mais descritivo será o seu nome e mais fácil será identificar qualquer bug que possa acontecer.

A regra da descida

Como foi dito anteriormente, um código deve ser tão claro e fuido a ponto de ser lido como uma narrativa que passa por cada etapa de um processo. Por isso, é importante que você defina uma ordem vertical para as suas funções.

O JavaScript possui um recurso fantástico chamado hoisting, que, de forma resumida, te permite chamar uma função declarada com a palavra function antes mesmo de ela ser criada.

Isso nos dá a vantagem de escrever códigos que possam ser lidos de cima para baixo, de acordo com a ordem em que as funções são chamadas. Qual a vantagem disso?

Vamos pegar o exemplo criado no tópico anterior e tentar lê-lo como uma série de parágrafos.

PARA criar uma lista de caracteres aleatórios, devemos informar o tamanho dessa lista e, a cada iteração, criar um caractere aleatório e adicioná-lo à lista

PARA criar um caractere aleatório, devemos criar um dígito aleatório correspondente a um caractere e convertê-lo com a classe String

PARA criar um dígito aleatório, devemos utilizar uma expressão matemática que gera valores reais aleatórios em um intervalo e, em seguida, arredondá-los com o método Math.floor

Ao manter essa sequência lógica em seus sistemas, será uma tarefa fácil compreender seus componentes um a um, ou até mesmo com um todo.

O perigo do efeitos colaterais.

Vou começar esse tópico com um trecho de clean code, que resume o que vou dizer, em poucas palavras

"Efeitos colaterais são grandes mentirosos. Sua função promete fazer uma coisa, mas também faz outras, secretamente, o que pode gerar mal comportamento em seu sistema"

Essa é, sem sombra de dúvidas, uma das principais origens de bugs em sistemas. Você não deveria, em hipótese alguma, fazer algo que a sua função não admite que faz.

Em uma função que valida senhas, encontrar a seguinte estrutura

function checkPassword(username, password) {
  const correspondingUser = userRepository.findByUsername(username);

  if (password === correspondingUser.password) {
    session.initialize();
    return true
  }
}

É achar a prova de que as funções estão mentindo. Isso pode custar caro, pois, nesse exemplo, cada vez que a verificação de senha for chamada, uma nova sessão de usuário será iniciada e perderemos os dados da antiga.

Claro, poderiamos renomear a função para checkPasswordAndInitializeSection mas ela claramente teria mais de uma responsabilidade.

Outra forma muito comum de efeito colateral (ou side effect, como é mais conhecido) é alterar valores de atributos internos em uma classe, ao chamar um método da mesma. Assim, sempre que tivermos problemas com um valor específico, será bem mais difícil delimitar o seu escopo, já que outras funções modificam o seu valor, secretamente.

Don't Repeat Yourself (DRY)

Eu mantive o nome desse tópico em inglês porque ele é bem famoso na programação e é assim que você vai ouvir falar dele.

O DRY, ou Não Se Repita é um princípio que nos orienta a eliminar código duplicado, em nossas aplicações. Isso não quer dizer automatizar TUDO o que há nelas, mas evitar pontos em que uma única mudança teria de ser refletida em várias partes de um mesmo código.

Uma clara violação desse princípio pode ser encontrada no exemplo do CPF validator, onde a lógica para gerar um dígito ao final do CPF foi duplicada

// Primeira criação de dígito
let firstLastingDigitOnCPF;

  let factor = cpfWithoutPrefix.length + 1;
  let sum = 0;
  for (let i in cpfWithoutPrefix) {
    sum += cpfWithoutPrefix[i] * factor;
    factor--;
  }

  firstLastingDigitOnCPF = 11 - (sum % 11);

  // segunda criação de dígito
  let secondLastingDigitOnCPF;

  cpfWithoutPrefix += firstLastingDigitOnCPF;

  factor = cpfWithoutPrefix.length + 1;
  sum = 0;
  for (let i in cpfWithoutPrefix) {
    sum += cpfWithoutPrefix[i] * factor;
    factor--;
  }
  secondLastingDigitOnCPF = 11 - (sum % 11);

Isso pôde ser contornado envolvendo a lógica repetida em um método chamado createDigit, que foi chamado duas vezes

static createDigit(incompleteCpf) {
    let factor = incompleteCpf.length + 1;
    let sum = 0;
    for (let i in incompleteCpf) {
        sum += incompleteCpf[i] * factor;
        factor--;
    }
    const newDigit = 11 - (sum % 11);
    return newDigit > 9 ? 0 : String(newDigit);
  }

Você não precisa se preocupar com o que essa função faz, no momento, apenas entender que, agora, qualquer mudança feita na lógica de criação de dígito de cpf será implementada apenas nessa função e não duplicada ao longo do código.

Dito isso, busque identificar pontos onde você percebe que está "Se repetindo" e poderia facilmente fazer uma otimização segura.

Como escrever boas funções?

Agora que você tem acesso a conhecimentos privilegiados sobre boas práticas, é preciso entender uma coisa muito importante.

Escrever código é como qualquer outro tipo de escrita. O que você cria logo depois de ter uma ideia é um rascunho, não necessariamente o resultado final. Esse rascunho será desorganizado, com nomenclaturas inadequadas ou com qualquer um dos problemas citados nesse artigo.

Depois de sentar e colocar seus pensamentos no papel (ou melhor, no editor) você faz uma revisão do que foi escrito e identifica pontos fortes e fracos no resultado, faz mudanças aqui e ali, cria novas funções e/ou classes e, quando menos perceber, já terá em mãos uma função bem escrita.

Para reforçar o que foi dito, deixo aqui um último trecho de Clean Code

"No final do processo, eu acabo criando funções que obedecem às regras nesse capítulo. Eu não as escrevo assim desde o começo. Não acho que alguém conseguiria"

Fonte

Clean Code - Robert C. Martin: buy it on Amazon

Posted on by:

lucascprazeres profile

Lucas dos Prazeres

@lucascprazeres

Um programador apaixonado por engenharias, não engenhocas 💡

Discussion

pic
Editor guide