Nome de função que mente: validar que grava, buscar que cria
Renomeie funções para que o nome diga o efeito real: separe consulta de comando, escolha verbos com contrato claro e troque o nome sem quebrar quem já chama.
A função se chama validar_pedido. Uma desenvolvedora nova escreve um teste que a chama com um pedido de exemplo para conferir as mensagens de erro. O teste passa. Na manhã seguinte, o cliente fictício "Teste da Silva" recebeu e-mail de confirmação e o pedido 0 aparece como confirmado no banco de homologação. A função validava, sim. E também gravava o status e disparava o e-mail. O nome autorizou o uso errado.
A resposta curta: o nome da função é o contrato curto, e o verbo precisa ser o efeito real. Se a função consulta, o nome diz consulta e ela não altera nada. Se altera estado, grava ou envia algo, o nome diz isso. Quando uma função faz duas coisas de naturezas diferentes, vire duas funções, ou use um nome que admita as duas, como obter_ou_criar.
Por que nome errado vira bug
Ninguém lê o corpo de toda função que chama. Num projeto de tamanho médio, você confia no nome, na assinatura e talvez na docstring. É assim que o código escala: cada nome resume um pedaço que você não precisa abrir.
Quando o nome promete menos do que a função faz, cada chamada vira uma aposta. Quem chamou validar_pedido esperava uma resposta, não um efeito. O mesmo vale para get_usuario(email) que cria o usuário se não existir: a tela de "buscar cliente" do suporte passa a cadastrar gente cada vez que alguém digita um e-mail errado na busca.
Existe um princípio antigo para isso, a separação entre consulta e comando, associada a Bertrand Meyer: uma função ou responde uma pergunta, ou muda o estado do sistema, mas não as duas coisas escondidas sob o mesmo nome. Você não precisa segui-lo como dogma. Precisa que o nome deixe claro de qual tipo é a função.
Desfazendo a mentira do validar_pedido
A versão original, num serviço Python com SQLAlchemy:
def validar_pedido(pedido):
erros = []
if not pedido.itens:
erros.append("Pedido sem itens.")
if pedido.total_centavos <= 0:
erros.append("Total precisa ser maior que zero.")
if not erros:
pedido.status = "confirmado"
db.session.commit()
enviar_email_confirmacao(pedido)
return erros
Três responsabilidades num nome que anuncia uma. O conserto segue um roteiro que serve para qualquer função suspeita.
- Liste os efeitos reais. Leia o corpo e anote tudo que sai da função: gravação no banco, chamada HTTP, e-mail, arquivo, alteração no objeto recebido. Aqui: altera
pedido.status, faz commit e envia e-mail. - Separe a consulta do comando. A parte que responde ("quais são os erros?") vira uma função sem efeito colateral. A parte que muda o mundo vira outra.
- Escolha o verbo pelo efeito. A consulta devolve uma lista de erros, então um nome como
erros_do_pedidodiz exatamente o que volta. O comando confirma o pedido, entãoconfirmar_pedido. - Encontre todos os chamadores antes de mudar qualquer coisa.
- Mantenha o nome antigo por um tempo se for usado em muitos lugares ou por outros times, avisando que vai sair.
- Atualize os testes para cobrir as duas funções separadamente.
import warnings
class PedidoInvalido(Exception):
def __init__(self, erros):
super().__init__("; ".join(erros))
self.erros = erros
def erros_do_pedido(pedido) -> list[str]:
erros = []
if not pedido.itens:
erros.append("Pedido sem itens.")
if pedido.total_centavos <= 0:
erros.append("Total precisa ser maior que zero.")
return erros
def confirmar_pedido(pedido) -> None:
erros = erros_do_pedido(pedido)
if erros:
raise PedidoInvalido(erros)
pedido.status = "confirmado"
db.session.commit()
enviar_email_confirmacao(pedido)
def validar_pedido(pedido):
warnings.warn(
"validar_pedido grava e envia e-mail; use erros_do_pedido ou confirmar_pedido",
DeprecationWarning,
stacklevel=2,
)
erros = erros_do_pedido(pedido)
if not erros:
confirmar_pedido(pedido)
return erros
O validar_pedido antigo continua funcionando, agora implementado com as duas funções novas, e emite um DeprecationWarning apontando o substituto. O stacklevel=2 faz o aviso indicar a linha de quem chamou, não a da própria função. Para achar os chamadores:
$ rg -nF "validar_pedido(" --type py
pedidos/api.py:48: erros = validar_pedido(pedido)
pedidos/importacao.py:112: validar_pedido(p)
tests/test_pedidos.py:21: assert validar_pedido(pedido_vazio) == ["Pedido sem itens."]
Três chamadas. A da API quer confirmar; a da importação provavelmente também; a do teste só queria os erros e deveria usar erros_do_pedido, exatamente o caso que sujou a base. Depois de migrar os três, apague o nome antigo.
Um vocabulário de verbos com contrato
Combine na equipe o que cada verbo promete, e o nome passa a carregar informação. Uma proposta:
- buscar / obter: só lê; devolve o item ou
Nonequando não encontra. - carregar: só lê; lança exceção quando não encontra, porque o chamador espera que exista.
- calcular: não lê banco nem rede; mesma entrada, mesma saída.
- criar / salvar / excluir: alteram o banco, e o nome diz qual operação.
- enviar / publicar: causam efeito fora do sistema, como e-mail ou mensagem de fila, que não se desfaz com rollback.
- obter_ou_criar: admite as duas coisas no nome. O Django usa esse padrão em
get_or_create, que devolve o objeto e um booleano dizendo se foi criado.
Fuja de verbos que cabem em tudo: processar, gerenciar, tratar, fazer. Se só esses servem, a função provavelmente faz coisas demais.
Variáveis também mentem
O mesmo vale fora de funções. lista_clientes que é um dicionário. timeout sem unidade, que alguém lê como segundos e outro como milissegundos; timeout_ms não deixa dúvida. Em projetos com nomes em português, cuidado com data: em inglês significa "dados", em português, "data do calendário", e a mesma base acaba com os dois sentidos. E booleanos com negação, como nao_ativo = False, obrigam o leitor a fazer lógica de cabeça.
Booleanos merecem atenção à parte também como parâmetro, assunto do post sobre parâmetro booleano na chamada.
Erros comuns
- Consulta que altera estado: chamar para "só ver" grava, envia ou cria, e o efeito aparece em teste, relatório ou tela de busca.
- Verbo genérico:
processar_pedidonão diz se confirma, cancela ou cobra, e cada chamador supõe uma coisa. - Renomear sem procurar os chamadores: código em outro módulo ou serviço quebra em produção, não no seu teste.
- Corrigir o corpo e manter o nome: a função passa a fazer outra coisa e o nome antigo agora mente na direção oposta.
- Compensar o nome com comentário: o comentário fica na definição, e quem lê a chamada nunca o vê, como discute o post sobre comentário do porquê.
- Unidade fora do nome: um valor em segundos tratado como milissegundos produz timeouts mil vezes errados.
Checklist
- Funções de consulta não gravam, não enviam e não alteram o objeto recebido.
- Funções que causam efeito têm o efeito no verbo.
- Não há funções com verbos genéricos onde um verbo específico serviria.
- Nomes antigos em migração emitem aviso de depreciação apontando o substituto.
- Variáveis de tempo, tamanho e dinheiro levam a unidade no nome.
- A equipe tem um vocabulário combinado para buscar, carregar, criar e enviar.
Escolha três funções do seu projeto ao acaso e leia só os nomes: dá para prever o efeito de cada uma, inclusive se ela grava ou envia algo, sem abrir o corpo?
Referência: https://www.python.org/dev/peps/pep-0008/#naming-conventions