Programação Por Equipe DevBras

Capa do artigo: Parâmetro booleano solto: por que emitirFatura(f, true, false) é uma armadilha

Parâmetro booleano solto: por que emitirFatura(f, true, false) é uma armadilha

Troque booleanos posicionais por objeto de opções, funções separadas ou tipos com nome, em TypeScript e Python, para que a chamada se leia sem abrir a definição.

A chamada era emitirFatura(fatura, false, true). Quem escreveu queria "enviar e-mail, cobrar só no vencimento". A assinatura era (fatura, enviarEmail, cobrarAgora), então a chamada fez o contrário: não mandou e-mail e cobrou o cartão do cliente na hora. O compilador não reclamou, porque os dois parâmetros são do mesmo tipo. O review não pegou, porque ninguém sabe de cabeça a ordem de dois booleanos.

A resposta curta: booleano posicional esconde o significado na definição, longe da chamada. Troque por um objeto de opções com campos nomeados, por um tipo com valores que dizem o que significam ou, quando o booleano muda completamente o comportamento, por duas funções. O teste é simples: leia a chamada em voz alta. Se precisar abrir a definição para entender, o desenho está torto.

Por que o booleano é o pior parâmetro posicional

Um parâmetro posicional só é legível se o valor carrega o próprio significado. cotarFrete("01310-100", 1500) se entende razoavelmente: um CEP e um peso. true e false não carregam nada. Eles respondem "sim" ou "não" a uma pergunta que não aparece na chamada.

Com dois ou mais booleanos, o problema se multiplica. Existem quatro combinações para dois e oito para três, todas com a mesma cara. Trocar a ordem não gera erro de tipo, não gera erro em tempo de execução e produz um comportamento plausível, só que errado.

Martin Fowler chama isso de flag argument e argumenta que, quando o booleano seleciona entre dois comportamentos, a função deveria ser duas. A ideia por trás é que quem chama quase sempre sabe, no momento de escrever, qual dos dois comportamentos quer. Então pode dizer isso pelo nome.

Dica de editor ajuda menos do que parece. IDEs mostram o nome do parâmetro ao lado do valor, mas o diff do pull request, o log de erro e a busca no repositório mostram só false, true.

Como reescrever a chamada

A função original, em TypeScript, com o terceiro booleano que apareceu depois:

function emitirFatura(
  fatura: Fatura,
  enviarEmail: boolean,
  cobrarAgora: boolean,
  rascunho: boolean
): void {
  if (rascunho) {
    salvar(fatura, 'rascunho');
    return;
  }
  salvar(fatura, 'emitida');
  if (cobrarAgora) cobrar(fatura);
  if (enviarEmail) notificar(fatura);
}

emitirFatura(fatura, false, true, false);
  1. Identifique o booleano que troca o comportamento inteiro. rascunho faz a função sair cedo e ignorar todo o resto. Isso é outra operação.
  2. Extraia essa operação numa função própria, com nome que diga o efeito.
  3. Veja se algum booleano é na verdade uma escolha entre opções. "Cobrar agora ou no vencimento" é um modo de cobrança; amanhã pode surgir "cobrar em três vezes". Um tipo união com nomes acomoda isso.
  4. Agrupe o que sobra num objeto de opções, com campos nomeados.
  5. Torne obrigatórios os campos perigosos. Se esquecer a forma de cobrança pode cobrar alguém errado, não ofereça valor padrão.
  6. Migre os chamadores e remova a assinatura antiga.
type Cobranca = 'imediata' | 'no-vencimento';

interface OpcoesDeEmissao {
  cobranca: Cobranca;
  enviarEmail: boolean;
}

function emitirFatura(fatura: Fatura, opcoes: OpcoesDeEmissao): void {
  salvar(fatura, 'emitida');
  if (opcoes.cobranca === 'imediata') cobrar(fatura);
  if (opcoes.enviarEmail) notificar(fatura);
}

function salvarRascunhoDeFatura(fatura: Fatura): void {
  salvar(fatura, 'rascunho');
}

emitirFatura(fatura, { cobranca: 'no-vencimento', enviarEmail: true });

A chamada agora se lê sozinha: emitir fatura com cobrança no vencimento e envio de e-mail. A ordem dos campos no objeto não importa, e trocar 'no-vencimento' por 'imediata' exige escrever a palavra, o que torna o erro visível no diff. Se alguém digitar 'imediato', o TypeScript recusa, porque o tipo só aceita os dois valores declarados.

Repare que enviarEmail continuou booleano. Booleano dentro de um objeto com nome não é problema: enviarEmail: true é perfeitamente legível. O problema era o true sem rótulo.

O mesmo em Python e C#

Em Python, parâmetros depois de um * na assinatura só podem ser passados por nome. Isso transforma a boa prática em regra:

def emitir_fatura(fatura, *, cobranca: str, enviar_email: bool) -> None:
    if cobranca not in ("imediata", "no-vencimento"):
        raise ValueError(f"cobrança desconhecida: {cobranca}")
    salvar(fatura, "emitida")
    if cobranca == "imediata":
        cobrar(fatura)
    if enviar_email:
        notificar(fatura)

emitir_fatura(fatura, cobranca="no-vencimento", enviar_email=True)
emitir_fatura(fatura, "no-vencimento", True)  # TypeError: argumentos posicionais demais

Se o conjunto de valores crescer, um enum.Enum no lugar da string deixa a lista de opções explícita e verificável.

Em C#, argumentos nomeados existem (EmitirFatura(fatura, enviarEmail: true, cobrarAgora: false)), mas são opcionais na chamada: nada impede a próxima pessoa de escrever só true, false. Para garantir, use um enum para o modo de cobrança ou um tipo de opções, como no TypeScript.

Quando um booleano solto é aceitável

Funções com um único parâmetro booleano, cujo nome deixa óbvio o que ele controla, costumam ser legíveis: botao.setDesabilitado(true) ou painel.setVisivel(false). O verbo da função já é a pergunta, e o booleano é a resposta. A armadilha começa quando o significado do valor depende de lembrar a posição, ou quando há mais de um.

Erros comuns

  • Dois ou mais booleanos posicionais: a ordem invertida passa no compilador e no review, e o efeito errado aparece só em produção.
  • Booleano que muda o comportamento inteiro: a função vira duas funções coladas por um if, e cada mudança arrisca a outra metade.
  • Booleano para algo que vai ganhar terceira opção: quando surge o "cobrar em três vezes", vem um segundo booleano e combinações impossíveis, como imediata e parcelada ao mesmo tempo.
  • Objeto de opções com padrão perigoso: esquecer o campo de cobrança cai num padrão que cobra na hora, sem ninguém ter decidido isso.
  • Confiar nas dicas da IDE: elas não aparecem no diff, no log nem na busca, que é onde o erro precisa ser visto.
  • Corrigir com comentário na chamada: emitirFatura(f, false /* email */, true /* cobrar */) funciona até alguém mudar a assinatura e o comentário continuar dizendo a ordem antiga.

Checklist

  • Nenhuma função pública recebe mais de um booleano posicional.
  • Booleanos que trocam o comportamento inteiro viraram funções separadas.
  • Escolhas que podem ganhar terceira opção usam tipo união ou enum.
  • Campos cujo esquecimento causa dano são obrigatórios, sem valor padrão.
  • Em Python, opções de comportamento são keyword-only.
  • No review, cada chamada nova se entende sem abrir a definição.

Procure no seu projeto a chamada com mais true e false seguidos: dá para dizer o que ela faz sem abrir a definição da função? Se não der, comece por ela, e aproveite para conferir se o nome da função conta a verdade, como no post sobre nome que não mente.

Avalie este artigo

Referência: https://martinfowler.com/bliki/FlagArgument.html