Como posso fazer uma ligação com um limpador booleano? Armadilha booleana

76

Como observado nos comentários de @ benjamin-gruenbaum, isso é chamado de armadilha booleana:

Digamos que eu tenho uma função como esta

UpdateRow(var item, bool externalCall);

e no meu controlador, esse valor para externalCallsempre será VERDADEIRO. Qual é a melhor maneira de chamar essa função? Eu costumo escrever

UpdateRow(item, true);

Mas pergunto a mim mesmo, devo declarar um booleano, apenas para indicar o que esse valor "verdadeiro" significa? Você pode saber disso olhando a declaração da função, mas é obviamente mais rápido e mais claro se você apenas visse algo como

bool externalCall = true;
UpdateRow(item, externalCall);

PD: Não tenho certeza se essa pergunta realmente se encaixa aqui, se não, onde eu poderia obter mais informações sobre isso?

PD2: não codifiquei nenhum idioma porque pensei que era um problema muito genérico. Enfim, eu trabalho com c # e a resposta aceita funciona para c #

Mario Garcia
fonte
10
Esse padrão também é chamado a armadilha boolean
Benjamin Gruenbaum
11
Se você estiver usando um idioma com suporte para tipos de dados algébricos, recomendo muito um novo anúncio em vez de um booleano. data CallType = ExternalCall | InternalCallem haskell, por exemplo.
Filip Haglund
6
Eu acho que Enums preencheria exatamente o mesmo propósito; recebendo nomes para os booleanos e um pouco de segurança de tipo.
Filip Haglund
2
Não concordo que declarar um booleano para indicar o significado é "obviamente mais claro". Com a primeira opção, é óbvio que você sempre será verdadeiro. Com o segundo, você deve verificar (onde a variável é definida? O seu valor muda?). Claro, isso não é um problema se as duas linhas estiverem juntas ... mas alguém pode decidir que o lugar perfeito para adicionar seu código é exatamente entre as duas linhas. Acontece!
AJPerez 17/05/19
Declarar um booleano não é mais limpo, é mais claro que ele está pulando uma etapa que é, olhando a documentação do método em questão. Se você conhece a assinatura, é menos claro adicionar uma nova constante.
dentro de

Respostas:

154

Nem sempre existe uma solução perfeita, mas você tem muitas alternativas para escolher:

  • Use argumentos nomeados , se disponíveis no seu idioma. Isso funciona muito bem e não possui desvantagens particulares. Em algumas linguagens, qualquer argumento pode ser passado como um argumento nomeado, por exemplo updateRow(item, externalCall: true)( C # ) ou update_row(item, external_call=True)(Python).

    Sua sugestão de usar uma variável separada é uma maneira de simular argumentos nomeados, mas não possui os benefícios de segurança associados (não há garantia de que você tenha usado o nome correto da variável para esse argumento).

  • Use funções distintas para sua interface pública, com nomes melhores. Essa é outra maneira de simular parâmetros nomeados, colocando os valores do parâmetro no nome.

    Isso é muito legível, mas gera muitos clichês para você, que está escrevendo essas funções. Também não pode lidar bem com a explosão combinatória quando há vários argumentos booleanos. Uma desvantagem significativa é que os clientes não podem definir esse valor dinamicamente, mas devem usar if / else para chamar a função correta.

  • Use um enum . O problema com os booleanos é que eles são chamados de "verdadeiro" e "falso". Portanto, introduza um tipo com nomes melhores (por exemplo enum CallType { INTERNAL, EXTERNAL }). Como um benefício adicional, isso aumenta a segurança de tipo do seu programa (se o seu idioma implementar enumerações como tipos distintos). A desvantagem das enumerações é que elas adicionam um tipo à sua API publicamente visível. Para funções puramente internas, isso não importa e as enumerações não têm desvantagens significativas.

    Em idiomas sem enumerações, às vezes são usadas seqüências curtas . Isso funciona e pode até ser melhor que os booleanos crus, mas é muito suscetível a erros de digitação. A função deve afirmar imediatamente que o argumento corresponde a um conjunto de valores possíveis.

Nenhuma dessas soluções tem um impacto proibitivo no desempenho. Parâmetros nomeados e enums podem ser resolvidos completamente em tempo de compilação (para um idioma compilado). O uso de strings pode envolver uma comparação de strings, mas o custo disso é insignificante para literais de strings pequenos e para a maioria dos tipos de aplicativos.

amon
fonte
7
Acabei de aprender que existem "argumentos nomeados". Eu acho que essa é de longe a melhor sugestão. Se você puder elaborar um pouco dessa opção (para que outras pessoas não precisem muito do google), aceitarei esta resposta. Além disso, quaisquer sugestões sobre o desempenho se você puder ... Graças
Mario Garcia
6
Uma coisa que pode ajudar a aliviar os problemas com cadeias curtas é usar constantes com os valores das cadeias. @MarioGarcia Não há muito o que elaborar. Além do mencionado aqui, a maioria dos detalhes dependerá do idioma específico. Havia algo em particular que você queria ver mencionado aqui?
Jpmc26
8
Nas linguagens em que estamos falando de um método e o enum pode ser definido como uma classe, geralmente uso um enum. É completamente auto-documentado (na única linha de código que declara o tipo de enum, também é leve), impede completamente que o método seja chamado com um booleano nu, e o impacto na API pública é insignificante (uma vez que os identificadores têm escopo definido para a classe).
Davidbak
4
Outra falha no uso de strings nessa função é que você não pode verificar a validade deles no tempo de compilação, ao contrário das enumerações.
Ruslan
32
enum é o vencedor. Só porque um parâmetro pode ter apenas um dos dois estados possíveis, isso não significa que ele deva ser automaticamente um booleano.
Pete
39

A solução correta é fazer o que você sugere, mas empacotá-lo em uma mini-fachada:

void updateRowExternally() {
  bool externalCall = true;
  UpdateRow(item, externalCall);
}

A legibilidade supera a micro-otimização. Você pode pagar a chamada de função extra, certamente melhor do que o esforço do desenvolvedor de ter que procurar a semântica do sinalizador booleano pelo menos uma vez.

Kilian Foth
fonte
8
Esse encapsulamento realmente vale a pena quando eu chamo essa função apenas uma vez no meu controlador?
Mario Garcia
14
Sim. Sim. O motivo, como escrevi acima, é que o custo (uma chamada de função) é menor que o benefício (você economiza uma quantidade não trivial de tempo de desenvolvedor, porque ninguém nunca precisa procurar o que truefaz.) Valeria a pena, mesmo que custam tanto tempo de CPU quanto economiza tempo de desenvolvedor, pois o tempo de CPU é bem mais barato.
Kilian Foth
8
Além disso, qualquer otimizador competente deve ser capaz de incorporar isso para você, para que você não perca nada no final.
Firedraco 16/05/19
33
@KilianFoth Exceto que é uma suposição falsa. Um mantenedor não precisa saber o que o segundo parâmetro faz, e por que é verdade, e quase sempre isso se relaciona com o detalhe de que você está tentando fazer. Ocultar a funcionalidade em funções minúsculas que reduzem a morte por mil cortes diminui a capacidade de manutenção, não a aumenta. Eu já vi isso acontecer, é triste dizer. Particionar excessivamente em funções pode ser pior do que uma enorme Função de Deus.
Graham
6
Eu acho UpdateRow(item, true /*external call*/);que seria mais limpo, em idiomas que permitem essa sintaxe de comentário. Inchaço do código com uma função extra apenas para evitar escrever um comentário não parece valer a pena para um caso simples. Talvez se houvesse muitos outros argumentos para essa função e / ou algum código circundante desordenado e complicado, ele começaria a atrair mais. Mas acho que, se você estiver depurando, acabará verificando a função wrapper para ver o que ela faz e precisará lembrar quais funções são wrappers finas em torno de uma API de biblioteca e quais realmente têm alguma lógica.
Peter Cordes
25

Digamos que eu tenho uma função como UpdateRow (item var, bool externalCall);

Por que você tem uma função como essa?

Sob quais circunstâncias você o chamaria com o argumento externalCall definido para valores diferentes?

Se um é de, digamos, um aplicativo cliente externo e o outro está dentro do mesmo programa (ou seja, módulos de código diferentes), então eu argumentaria que você deveria ter dois métodos diferentes , um para cada caso, possivelmente até definidos em distintos Interfaces.

Se, no entanto, você estivesse fazendo a chamada com base em dados retirados de uma fonte que não é do programa (por exemplo, um arquivo de configuração ou leitura do banco de dados), o método de passagem booleana faria mais sentido.

Phill W.
fonte
6
Concordo. E, apenas para entender o argumento proposto para outros leitores, é possível fazer uma análise de todos os chamadores, que passarão verdadeiro, falso ou variável. Se nenhum destes for encontrado, eu argumentaria por dois métodos separados (sem booleano) sobre um com o booleano - é um argumento YAGNI que o booleano introduz complexidade não utilizada / desnecessária. Mesmo que haja alguns chamadores passando variáveis, eu ainda posso fornecer as versões (booleanas) sem parâmetros para os chamadores passarem uma constante - é mais simples, o que é bom.
Erik Eidt
18

Embora eu concorde que seja ideal usar um recurso de idioma para reforçar a legibilidade e a segurança de valor, você também pode optar por uma abordagem prática : comentários na hora da chamada. Gostar:

UpdateRow(item, true /* row is an external call */);

ou:

UpdateRow(item, true); // true means call is external

ou (com razão, conforme sugerido por Frax):

UpdateRow(item, /* externalCall */true);
bispo
fonte
1
Não há razão para votar. Esta é uma sugestão perfeitamente válida.
Suncat2000
Aprecie o <3 @ Suncat2000!
bispo
11
Uma variante (provavelmente preferível) está apenas colocando o nome do argumento simples lá, como UpdateRow(item, /* externalCall */ true ). O comentário de sentenças completas é muito mais difícil de analisar, na verdade é principalmente ruído (especialmente a segunda variante, que também é muito pouco acoplada ao argumento).
Frax
1
Essa é de longe a resposta mais apropriada. É exatamente para isso que os comentários se destinam.
Sentinel
9
Não é um grande fã de comentários como este. Os comentários tendem a ficar obsoletos se não forem tocados ao refatorar ou fazer outras alterações. Sem mencionar, assim que você tiver mais de um parâmetro bool, isso fica confuso rapidamente.
John V.
11
  1. Você pode 'nomear' seus bools. Abaixo está um exemplo para uma linguagem OO (onde pode ser expressa na classe UpdateRow()), no entanto, o próprio conceito pode ser aplicado em qualquer idioma:

    class Table
    {
    public:
        static const bool WithExternalCall = true;
        static const bool WithoutExternalCall = false;
    

    e no site da chamada:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Acredito que o item 1 seja melhor, mas não força o usuário a usar novas variáveis ​​ao usar a função. Se a segurança do tipo é importante para você, você pode criar um enumtipo e UpdateRow()aceitar isso em vez de bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Você pode modificar o nome da função de alguma forma, para refletir melhor o significado do boolparâmetro. Não tenho muita certeza, mas talvez:

    UpdateRowWithExternalCall(var item, bool externalCall)

simurg
fonte
8
Não goste do número 3 como agora, se você chamar a função com externalCall=false, seu nome não faz absolutamente nenhum sentido. O resto da sua resposta é boa.
Lightness Races in Orbit
Acordado. Eu não tinha muita certeza, mas pode ser uma opção viável para alguma outra função. I
simurg 17/05/19
@LightnessRacesinOrbit Indeed. É mais seguro procurar UpdateRowWithUsuallyExternalButPossiblyInternalCall.
Eric Duminil
@EricDuminil: Spot on 😂
Lightness Races in Orbit
10

Outra opção que ainda não li aqui é: Use um IDE moderno.

Por exemplo, o IntelliJ IDEA imprime o nome da variável no método que você está chamando se estiver passando um literal como trueor nullou username + “@company.com. Isso é feito em uma fonte pequena, para não ocupar muito espaço na tela e parecer muito diferente do código real.

Ainda não estou dizendo que é uma boa ideia jogar booleanos em todos os lugares. O argumento que diz que você lê código muito mais frequentemente do que escreve é ​​muito forte, mas, nesse caso em particular, depende muito da tecnologia que você (e seus colegas!) Usa para dar suporte ao seu trabalho. Com um IDE, é muito menos problemático do que com o vim, por exemplo.

Exemplo modificado de parte da minha configuração de teste: insira a descrição da imagem aqui

Sebastiaan van den Broek
fonte
5
Isso só seria verdade se todos na sua equipe usassem apenas um IDE para ler seu código. Não obstante a prevalência de editores não IDE, você também possui ferramentas de revisão de código, ferramentas de controle de origem, perguntas sobre estouro de pilha, etc., e é de vital importância que o código permaneça legível, mesmo nesses contextos.
Daniel Pryden
@DanielPryden claro, daí meu comentário 'e de seus colegas'. É comum ter um IDE padrão nas empresas. Quanto a outras ferramentas, eu diria que é perfeitamente possível que esses tipos de ferramentas também apóiem ​​isso ou que venham a surgir no futuro, ou que esses argumentos simplesmente não se apliquem (como para uma equipe de programação de pares de duas pessoas)
Sebastiaan van den Broek
2
@Sebastiaan: Acho que não concordo. Mesmo como desenvolvedor solo, eu leio regularmente meu próprio código no Git diffs. O Git nunca será capaz de fazer o mesmo tipo de visualização sensível ao contexto que um IDE pode fazer, nem deveria. Sou a favor do uso de um bom IDE para ajudá-lo a escrever código, mas você nunca deve usar um IDE como desculpa para escrever código que você não teria escrito sem ele.
Daniel Pryden
1
@DanielPryden Não estou defendendo a escrita de códigos extremamente desleixados. Não é uma situação em preto e branco. Pode haver casos em que, no passado, para uma situação específica, você seria 40% a favor de escrever uma função com um valor booleano e 60% não, e você acabaria não fazendo isso. Talvez com um bom suporte a IDE agora, o saldo seja de 55% escrevendo assim e 45% não. E sim, você ainda pode precisar lê-lo fora do IDE algumas vezes, para que não seja perfeito. Mas ainda é uma troca válida em relação a uma alternativa, como adicionar outro método ou enumeração para esclarecer o código de outra forma.
Sebastiaan van den Broek
6

2 dias e ninguém mencionou polimorfismo?

target.UpdateRow(item);

Quando sou um cliente que deseja atualizar uma linha com um item, não quero pensar no nome do banco de dados, na URL do microsserviço, no protocolo usado para se comunicar ou se uma chamada externa é ou não precisará ser usado para que isso aconteça. Pare de me impor esses detalhes. Basta pegar meu item e atualizar uma linha em algum lugar já.

Faça isso e isso se torna parte do problema de construção. Isso pode ser resolvido de várias maneiras. Aqui está um:

Target xyzTargetFactory(TargetBuilder targetBuilder) {
    return targetBuilder
        .connectionString("some connection string")
        .table("table_name")
        .external()
        .build()
    ; 
}

Se você está olhando para isso e pensando "Mas minha linguagem nomeou argumentos, não preciso disso". Tudo bem, ótimo, vá usá-los. Apenas mantenha essa bobagem fora do cliente que está chamando, que nem deveria saber com o que está falando.

Note-se que isso é mais do que um problema semântico. Também é um problema de design. Passe um valor booleano e você será forçado a usar a ramificação para lidar com isso. Não é muito orientado a objetos. Tem dois ou mais booleanos para passar? Deseja que seu idioma tenha vários envios? Veja o que as coleções podem fazer quando você as aninha. A estrutura de dados correta pode facilitar sua vida.

candied_orange
fonte
2

Se você pode modificar a updateRowfunção, talvez você possa refatorá-la em duas funções. Dado que a função aceita um parâmetro booleano, suspeito que seja assim:

function updateRow(var item, bool externalCall) {
  Database.update(item);

  if (externalCall) {
    Service.call();
  }
}

Isso pode ser um pouco de cheiro de código. A função pode ter um comportamento dramaticamente diferente, dependendo de como a externalCallvariável está configurada; nesse caso, ela tem duas responsabilidades diferentes. A refatoração em duas funções que possuem apenas uma responsabilidade pode melhorar a legibilidade:

function updateRowAndService(var item) {
  updateRow(item);
  Service.call();
}

function updateRow(var item) {
  Database.update(item);
}

Agora, em qualquer lugar que você chamar essas funções, poderá saber rapidamente se o serviço externo está sendo chamado.

Isso nem sempre é o caso, é claro. É situacional e uma questão de gosto. Vale a pena considerar a refatoração de uma função que leva um parâmetro booleano para duas funções, mas nem sempre é a melhor opção.

Kevin
fonte
0

Se o UpdateRow estiver em uma base de código controlada, eu consideraria o padrão de estratégia:

public delegate void Request(string query);    
public void UpdateRow(Item item, Request request);

Onde Request representa algum tipo de DAO (um retorno de chamada em caso trivial).

Caso verdadeiro:

UpdateRow(item, query =>  queryDatabase(query) ); // perform remote call

Caso falso:

UpdateRow(item, query => readLocalCache(query) ); // skip remote call

Normalmente, a implementação do ponto de extremidade seria configurada em um nível mais alto de abstração e eu a usaria aqui. Como um efeito colateral gratuito e útil, isso me dá a opção de implementar outra maneira de acessar dados remotos, sem nenhuma alteração no código:

UpdateRow(item, query => {
  var data = readLocalCache(query);
  if (data == null) {
    data = queryDatabase(query);
  }
  return data;
} );

Em geral, essa inversão de controle garante menor acoplamento entre o armazenamento e o modelo de dados.

Basilevs
fonte