Pular para o conteúdo
Atualizado em 14 min de leitura

Funções pequenas e o princípio da responsabilidade única

Por Lucas Andrade ·

Funções pequenas que fazem uma coisa só são mais legíveis e testáveis. Entenda o princípio da responsabilidade única, quando extrair e quando parar.

Neste artigo

Toda pessoa que programa há algum tempo já encontrou aquela função monstruosa: centenas de linhas, uma dúzia de responsabilidades, variáveis reaproveitadas ao longo do caminho, blocos condicionais aninhados a perder de vista, e um nome tão genérico que poderia significar qualquer coisa. Mexer nela é um exercício de coragem, porque nunca se sabe qual das muitas coisas que ela faz vai quebrar quando você alterar uma linha. Essa função é o retrato do que acontece quando o código cresce sem disciplina de responsabilidade.

No outro extremo está a função pequena, que faz uma coisa só, tem um nome que descreve exatamente essa coisa, e cabe inteira na sua cabeça em uma única leitura. Você a entende, confia nela, e a usa como um bloco de construção sem precisar reabrir a implementação. Um sistema feito dessas peças é como um texto bem parágrafo, cada unidade com uma ideia, encadeadas de modo que o todo se lê com fluidez. A diferença entre os dois mundos não é uma questão de gosto; é uma diferença concreta de custo de manutenção, de facilidade de teste e de probabilidade de erro.

Este artigo trata do princípio da responsabilidade única aplicado às funções: a ideia de que cada função deve fazer uma coisa e fazê-la bem. Vamos entender o que "uma coisa" realmente significa, por que um único nível de abstração por função importa tanto, quais são os sinais de que uma função cresceu demais, como extrair funções menores com segurança, o papel dos efeitos colaterais e da pureza, e, crucialmente, como não cair no exagero oposto de fragmentar o código a ponto de torná-lo ilegível.

O que significa fazer uma coisa só#

O princípio da responsabilidade única, na sua forma clássica, diz que um módulo deve ter um, e apenas um, motivo para mudar. Aplicado a funções, ele se traduz numa diretriz mais direta: uma função deve fazer uma coisa, fazer só ela e fazê-la bem. A dificuldade está em definir o que conta como "uma coisa", porque quase toda função, olhada de perto, é composta de passos menores.

A chave para resolver esse aparente paradoxo é pensar em níveis de abstração. Uma função faz "uma coisa" quando todos os seus passos estão um nível de abstração abaixo do nome dela. Se a função se chama de forma a prometer que processa um pagamento, então cada linha dentro dela deve ser um passo desse processamento, expresso no nível certo: validar os dados, cobrar o valor, registrar a transação, notificar o cliente. Cada um desses passos é, por sua vez, "uma coisa" de um nível mais baixo, que pode virar sua própria função. A função de cima conta a história; as de baixo executam os detalhes.

Um teste prático ajuda a verificar isso. Tente descrever o que a função faz em uma frase, sem usar as palavras "e" nem "ou" para emendar responsabilidades distintas. Se você consegue dizer "ela calcula o desconto do pedido", ótimo, é uma coisa. Se você é forçado a dizer "ela valida o pedido e calcula o desconto e envia o e-mail e atualiza o estoque", a função está fazendo quatro coisas, e o "e" é a evidência. A necessidade de conjunções para descrever uma função é um sinal quase infalível de que ela deveria ser dividida.

Fazer uma coisa só não significa que a função seja trivial ou inútil por ser pequena. Significa que ela tem um propósito coeso, um foco único, de modo que quem a lê entende completamente o que ela faz e por quê, e quem a altera sabe que só está mexendo naquele propósito, sem risco de afetar responsabilidades vizinhas que, num monólito de função, estariam entrelaçadas.

Um nível de abstração por função#

Uma das causas mais comuns de funções difíceis de ler é a mistura de níveis de abstração dentro de um mesmo corpo. Quando uma função alterna entre conceitos de alto nível, como "processar o pedido", e detalhes de baixíssimo nível, como manipular índices de um vetor ou formatar uma cadeia de caracteres byte a byte, o leitor é obrigado a subir e descer constantemente numa escada mental. Essa oscilação é exaustiva e é onde muitos erros de compreensão nascem.

A regra do nível único de abstração combate isso. Ela diz que, dentro de uma função, todas as instruções devem operar aproximadamente na mesma altura conceitual. Os detalhes de nível mais baixo são empurrados para funções auxiliares, cujos nomes representam a intenção daquele detalhe. Assim, a função de alto nível se lê como um resumo, uma sequência de intenções nomeadas, e o leitor que quer o resumo o obtém sem se afogar em minúcias; o leitor que precisa de uma minúcia específica desce até a função correspondente e a encontra isolada, com seu próprio foco.

O efeito colateral positivo dessa disciplina é uma propriedade que às vezes se chama de leitura de cima para baixo. O código passa a se organizar como uma narrativa em camadas: no topo, as funções mais abstratas contam a história em termos gerais; abaixo delas, funções cada vez mais concretas preenchem os detalhes. Navegar por esse código é como fazer zoom: você começa vendo o mapa inteiro e mergulha só nas regiões que interessam. Um sistema assim organizado é dramaticamente mais fácil de explorar do que um em que cada função é um poço de detalhes sem estrutura de níveis.

Manter um nível de abstração por função também melhora a nomeação, porque uma função focada num único nível tem um propósito claro, e propósitos claros geram nomes claros. Aquela dificuldade de nomear que aparece diante de funções confusas costuma desaparecer quando a função é fatiada em níveis coerentes, cada fatia com uma responsabilidade que se descreve em poucas palavras.

Sinais de que a função é grande demais#

Nem sempre é óbvio, no calor da escrita, que uma função ultrapassou seu tamanho saudável. Mas há sinais concretos, observáveis, que funcionam como alarmes. Aprender a reconhecê-los é metade do caminho para manter as funções sob controle.

  • Muitos parâmetros. Uma função que recebe muitos argumentos geralmente está tentando fazer coisas demais ou operando sobre conceitos que deveriam estar agrupados. Poucos parâmetros são fáceis de entender e de chamar corretamente; uma lista longa de parâmetros é difícil de memorizar, fácil de trocar de ordem por engano, e costuma denunciar uma responsabilidade inflada.
  • Muitos blocos condicionais e aninhamento profundo. Quando uma função acumula muitas ramificações, especialmente aninhadas em vários níveis, ela está codificando muitas decisões diferentes num só lugar. Cada nível de aninhamento multiplica os caminhos possíveis que o leitor precisa considerar, e a explosão combinatória rapidamente ultrapassa o que uma pessoa consegue manter na cabeça.
  • A palavra "e" no nome, ou a impossibilidade de nomear. Se o nome mais honesto para a função contém um "e" que junta responsabilidades, ou se você simplesmente não consegue encontrar um nome específico e cai num genérico como processar ou gerenciar, a função está fazendo coisas demais. O nome é um termômetro da coesão.
  • Comentários que dividem a função em seções. Quando você sente a necessidade de escrever um comentário anunciando "agora a parte que valida" e mais adiante "agora a parte que salva", cada uma dessas seções é uma função implícita pedindo para nascer. O comentário de seção é quase sempre o nome de uma função que ainda não foi extraída.
  • Variáveis locais em excesso e reaproveitadas. Um número grande de variáveis temporárias, especialmente aquelas reutilizadas para propósitos diferentes ao longo da função, indica que há várias linhas de raciocínio disputando o mesmo espaço. Cada linha de raciocínio poderia viver em sua própria função, com suas próprias variáveis locais, sem interferência.
  • Dificuldade de testar. Se escrever um teste para a função exige montar um cenário elaborado, com muitas dependências e muitos estados, é sinal de que ela concentra responsabilidades demais. Funções focadas são fáceis de testar justamente porque têm poucas razões para se comportar de formas diferentes.

Nenhum desses sinais isolado é uma sentença definitiva; há exceções legítimas. Mas quando vários deles aparecem juntos na mesma função, a mensagem é inequívoca: está na hora de dividir.

Extração de funções como ferramenta central#

A técnica que transforma funções grandes em pequenas é a extração de função, e ela é, talvez, a refatoração mais fundamental de todas. Extrair uma função consiste em pegar um trecho coeso de código, movê-lo para uma nova função com um nome descritivo, e substituir o trecho original por uma chamada a essa nova função. Simples de descrever, poderosa em efeito.

O grande valor da extração está no nome. Quando você isola um trecho e é obrigado a nomeá-lo, você troca um bloco de instruções, que o leitor teria que decifrar, por um nome que declara a intenção daquele bloco. Um pedaço de código que calculava algo obscuro vira uma chamada a uma função com nome claro, e a função de cima fica mais legível na mesma proporção. A extração é, em essência, uma máquina de converter "como" em "o quê": onde antes havia mecânica, passa a haver intenção nomeada.

O momento certo para extrair costuma ser sinalizado justamente pelos alarmes da seção anterior. Sentiu vontade de escrever um comentário de seção? Extraia a seção e use o comentário como base do nome. Percebeu um aninhamento profundo? Extraia o miolo de uma ramificação para uma função à parte, e a função original respira. Notou um cálculo repetido em vários lugares? Extraia-o uma vez e reutilize, eliminando duplicação e, de quebra, criando um único ponto de verdade para aquela lógica.

A segurança da extração depende de fazê-la em passos pequenos e verificáveis, idealmente com testes que confirmam que o comportamento não mudou. Refatoração, por definição, é alterar a estrutura sem alterar o comportamento observável, e uma boa rede de testes é o que permite extrair com confiança, sabendo que qualquer engano será apanhado de imediato. Ferramentas modernas de desenvolvimento automatizam boa parte da extração, movendo o trecho e ajustando as referências de forma segura, o que reduz ainda mais o risco e o custo dessa operação.

Efeitos colaterais, pureza e previsibilidade#

Uma dimensão frequentemente esquecida da responsabilidade única é o tratamento dos efeitos colaterais. Um efeito colateral é qualquer coisa que uma função faz além de calcular e devolver um resultado a partir de suas entradas: modificar uma variável externa, gravar em um banco, escrever em um arquivo, enviar uma mensagem pela rede, alterar o estado de um objeto compartilhado. Efeitos colaterais são necessários, afinal um programa que não afeta o mundo é inútil, mas eles precisam ser explícitos e concentrados, não espalhados e escondidos.

Uma função pura é aquela que, para as mesmas entradas, sempre produz as mesmas saídas e não causa nenhum efeito colateral observável. Funções puras são o sonho da legibilidade e da testabilidade: você as entende olhando apenas para suas entradas e saídas, sem precisar conhecer o estado do mundo ao redor, e as testa sem montar cenário nenhum, bastando fornecer entradas e verificar saídas. Sempre que uma parte da lógica pode ser expressa de forma pura, isolá-la assim é um ganho enorme de clareza.

O problema surge quando uma função mistura cálculo e efeito de forma implícita. Uma função cujo nome sugere apenas consulta, mas que silenciosamente altera o estado do sistema, viola a expectativa do leitor e vira fonte perene de bugs, porque ninguém espera que perguntar algo mude alguma coisa. A boa prática é separar comando de consulta: funções que respondem perguntas não devem alterar o estado, e funções que alteram o estado devem deixar isso evidente no nome e na estrutura. Essa separação é uma forma particular de responsabilidade única, aplicada à distinção entre saber e agir.

Concentrar os efeitos colaterais nas bordas do sistema, mantendo o núcleo o mais puro possível, é um princípio de projeto que se combina naturalmente com funções pequenas. O miolo da lógica de negócio, feito de funções puras e focadas, fica fácil de raciocinar e de testar; as funções que tocam o mundo externo ficam poucas, explícitas e localizadas, onde é possível dar-lhes atenção especial. Um sistema organizado assim é muito mais previsível, porque a maior parte dele é composta de peças cujo comportamento não depende de contexto oculto.

O equilíbrio: não fragmentar demais#

Como todo bom princípio, o das funções pequenas pode ser levado longe demais, e o exagero tem seus próprios custos. Fragmentar o código em uma nuvem de funções minúsculas, cada uma com uma ou duas linhas, chamando umas às outras em cadeias longas, pode tornar o programa tão difícil de seguir quanto o monólito que se queria evitar. Em vez de rolar por uma função gigante, o leitor passa a saltar de definição em definição, perdendo o fio da meada a cada salto. Isso às vezes é chamado, com humor amargo, de código-espaguete de funções.

O sinal de fragmentação excessiva é quando uma função existe apenas para dar nome a uma linha que já era clara por si, ou quando entender um fluxo simples exige abrir uma dúzia de funções espalhadas, sem que nenhuma delas represente um conceito genuíno. A extração deve criar abstrações significativas, unidades que o leitor reconhece como uma ideia coesa, e não apenas picar o código em pedaços arbitrários pelo prazer de tê-los pequenos. Uma função de uma linha vale a pena quando seu nome esconde um "como" e revela um "o quê" importante; não vale a pena quando o nome apenas repete o que a linha já dizia.

O equilíbrio certo é guiado pela coesão, não pela contagem de linhas. O objetivo nunca foi funções curtas por serem curtas; foi funções focadas, com uma responsabilidade clara, num único nível de abstração. Uma função um pouco mais longa que mantém um raciocínio coeso do início ao fim pode ser mais legível do que três funções minúsculas que estilhaçam esse raciocínio sem necessidade. O tamanho é um indicador útil, mas é a responsabilidade única, e não o número de linhas, que dita a fronteira correta.

Na prática, isso significa usar o julgamento e ouvir o código. Extraia quando a extração cria uma abstração que ajuda a entender; pare quando extrair mais começaria a esconder o fluxo em vez de esclarecê-lo. A meta é que cada função seja um degrau confortável na escada de abstração, nem tão alto que exija esforço para subir, nem tão baixo que multiplique degraus a ponto de a escada virar um labirinto. Esse senso de proporção se afina com a prática e com a atenção constante ao que torna o código mais fácil, ou mais difícil, de ler.

Síntese: pequenas peças, sistema compreensível#

O princípio da responsabilidade única aplicado a funções produz um efeito profundo sobre a qualidade de um sistema. Funções que fazem uma coisa só, num único nível de abstração, com efeitos colaterais explícitos e nomes que revelam intenção, formam blocos de construção confiáveis. Cada peça é fácil de entender isoladamente, fácil de testar sem cenários elaborados, e segura de alterar porque sua responsabilidade é única e delimitada. O sistema montado com essas peças herda essas propriedades: torna-se um todo compreensível, feito de partes compreensíveis.

Chegar lá é uma questão de hábito mais do que de heroísmo. É reconhecer os sinais de uma função inchada, os muitos parâmetros, os aninhamentos, o "e" no nome, os comentários de seção, e responder a eles com a ferramenta certa, a extração de funções, feita em passos seguros amparados por testes. É separar o que calcula do que age, concentrando efeitos nas bordas e mantendo o núcleo previsível. E é, sobretudo, buscar a coesão como critério, evitando tanto o monólito que faz tudo quanto a fragmentação que esconde o fluxo em migalhas.

No fim, funções pequenas não são um objetivo estético; são um meio para os fins que realmente importam: legibilidade, testabilidade e capacidade de mudar o sistema com segurança. Quando você divide o código pela responsabilidade, e não pelo acaso de onde a linha calhou de cair, cada função passa a contar uma parte clara de uma história maior. E um sistema que se lê como uma história bem contada, em camadas de intenção que se pode explorar no nível que se quiser, é exatamente o tipo de sistema que envelhece bem, que acolhe quem chega e que perdoa quem precisa mudá-lo sob pressão. Esse é o retorno duradouro de levar a responsabilidade única a sério, uma função de cada vez.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly