Programação Por Equipe DevBras

Capa do artigo: Nome de função que mente: validar que grava, buscar que cria

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.

  1. 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.
  2. 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.
  3. Escolha o verbo pelo efeito. A consulta devolve uma lista de erros, então um nome como erros_do_pedido diz exatamente o que volta. O comando confirma o pedido, então confirmar_pedido.
  4. Encontre todos os chamadores antes de mudar qualquer coisa.
  5. Mantenha o nome antigo por um tempo se for usado em muitos lugares ou por outros times, avisando que vai sair.
  6. 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 None quando 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_pedido nã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?

Avalie este artigo

Referência: https://www.python.org/dev/peps/pep-0008/#naming-conventions