Post de autoria de Filipe Ximenes originalmente publicado em inglês no blog da Vinta. Tradução livre para o português por Eduardo Cuducos.
Para pythonistas isso também poderia ser “por que linear é melhor do que aninhado”
Todo código limpo e fácil de manter normalmente segue um princípio simples: toda etapa deve ter um fluxo de sucesso claro. Esse conceito é fundamental mas muitas vezes desprezado no desenvolvimento.
Um fluxo de sucesso claro envolve duas ideais fundamentais: Primeiro, cada função deve servir única e exclusivamente a um propósito. Pense como se você fosse perguntar como chegar a algum lugar — se você não consegue explicar para onde você está indo em uma frase simples, provavelmente você está tentando visitar mais de um lugar de uma vez só. Quando você tem dificuldade para dar um nome conciso a uma função, provavelmente isso é um sinal de que essa função está fazendo coisa demais.
Segundo, o fluxo de sucesso — a sequência de passos quando tudo ocorre como esperado — deve ser óbvio de forma imediata quando além lê o código. Isso quer dizer que a lógica deve ser mantida de forma limpa e organizada, deixando o tratamento de erros e casos extremos para escanteio. O fluxo primário da função deve ser lido como uma história na qual cada linha naturalmente leva à próxima.
Vamos falar sobre como manter o código linear
Um comando aninhado é um bloco de código que está debaixo de uma cláusula que move, visualmente, o início do código para longe da margem esquerda do editor de texto (considerando que você está utilizando as boas práticas de indentação).
if
/else
etry
/catch
são exemplos disso. Um código linear é o oposto de código aninhado, é um código que está mais próximo da margem esquerda do editor.
O que é um fluxo de sucesso varia em diversas parte do código. Pode ser o comportamento padrão de uma função, o retorno mais provável, ou simplesmente o caminho que dá conta do propósito principal daquele código. Considere um função dividir(x, y)
que recebe os valores de quem a utiliza. Por mais que executar x / y
seja o propósito dessa função, ela tem que verificar se y
não é 0
antes de efetuar o cálculo. Validação dos argumentos recebidos é essencial para que a função funcione corretamente, mas, ainda assim, esse não é o propósito principal de dividir
. O destaque aqui é essa relação importantíssima: um fluxo verdadeiramente linear só é possível se a função tem um único propósito.
Para exemplificar esse conceito, vamos examinar uma função que cuida da transferência de dinheiro entre correntistas, retornando True
para transferências efetuadas com sucesso, e False
para os outros casos.
def transferir_dinheiro(remetente, destinatário, quantia):
if quantia > 0:
if remetente.saldo >= quantia:
rementente.saldo = rementente.saldo - quantia
destinatário.saldo = destinatário.saldo + quantia
notificar_sucesso(remetente, quantia)
return True
else:
notificar_saldo_insuficiente(remetente)
return False
else:
return False
Esse código é difícil de compreender em uma olhada rápida. Duas coisas fazem ser difícil de acompanhar o que acontece:
- O
if
/else
aninhado ofusca o propósito principal da função — não fica claro qual o fluxo que representa a abstração principal desse código - O valores a serem retornados
True
/False
ficam espalhados pelo códigos, fazendo com que seja difícil de identificar as condições que fazem a função falhar ou ter sucesso sem que a pessoa tenha que ler todo o código
Vamos refatorar para tornar esse código mais claro
def transferir_dinheiro(remetente, destinatário, quantia):
if quantia <= 0:
return False
if remetente.saldo < quantia:
notificar_saldo_insuficiente(remetente)
return False
rementente.saldo = rementente.saldo - quantia
destinatário.saldo = destinatário.saldo + quantia
notificar_sucesso(remetente, quantia)
return True
Repare que apesar de ser mais explícito, o código refatorado tem a mesma complexidade ciclomática que antes. Vale notar ainda que a complexidade ciclomática é um conceito matemático preciso que pode indicar que o código precisa ser refatorado. Já a linearidade tem a ver com a semântica do código e é um quesito muito mais subjetivo.
A principal mudança da primeira versão para a versão refatorada é que ignorando todo o código aninhado, a leitura evidencia o fluxo principal do programa:
def transferir_dinheiro(remetente, destinatário, quantia):
rementente.saldo = rementente.saldo - quantia
destinatário.saldo = destinatário.saldo + quantia
notificar_sucesso(remetente, quantia)
return True
Isso é o que representa o fluxo linear de sucesso — a funcionalidade principal da nossa função de transferência. Quando se lê um código com o qual não se tem familiaridade, quem desenvolve normalmente foca, primeiro, nos blocos de código que não estão aninhados, que devem representar o fluxo principal. Os blocos aninhados normalmente lidam com casos especiais e condições de exceção.
Utilizar cláusulas de guarda ao invés de blocos de if
/else
é uma das formas mais efetivas de destacar o fluxo de sucesso. Quando você sentir que não consegue chegar nesse nível de linearidade, isso pode ser um sinal de que o código está lidando com muito mais responsabilidades do que ele deveria, e talvez deva ser separado em mais de uma função.