Como posso fazer uma chamada com um clearer booleano? Armadilha Booleana

74

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

Digamos que eu tenha uma função como esta

UpdateRow(var item, bool externalCall);

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

UpdateRow(item, true);

Mas eu me pergunto, devo declarar um booleano, apenas para indicar o que esse valor "verdadeiro" representa? Você pode saber isso olhando a declaração da função, mas é obviamente mais rápido e claro se você acabou de ver 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 marquei nenhum idioma porque achei que era um problema muito genérico. Enfim, eu trabalho com c # e a resposta aceita funciona para c #

    
por Mario Garcia 16.05.2018 / 15:09
fonte

9 respostas

151

Nem sempre há 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 tem desvantagens específicas. Em alguns idiomas, qualquer argumento pode ser passado como um argumento nomeado, por ex. updateRow(item, externalCall: true) ( C # ) ou update_row(item, external_call=True) (Python).

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

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

    Isso é muito legível, mas leva muito clichê 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 "true" e "false". Então, em vez disso, introduza um tipo com nomes melhores (por exemplo, enum CallType { INTERNAL, EXTERNAL } ). Como um benefício adicional, isso aumenta o tipo de segurança do seu programa (se o seu idioma implementa enums como tipos distintos). A desvantagem de enums é que eles adicionam um tipo à sua API publicamente visível. Para funções puramente internas, isso não importa e os enums não têm desvantagens significativas.

    Em idiomas sem enums, às vezes são usadas strings curtas . Isso funciona e pode até ser melhor do que booleanos crus, mas é muito suscetível a erros de digitação. A função deve então afirmar imediatamente que o argumento corresponde a um conjunto de valores possíveis.

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

    
por 16.05.2018 / 15:28
fonte
37

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 você pode pagar o esforço do desenvolvedor de ter que procurar a semântica da bandeira Booleana uma única vez.

    
por 16.05.2018 / 15:12
fonte
24

Say I have a function like UpdateRow(var item, bool externalCall);

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

Em que circunstâncias você chamaria com o argumento externalCall definido para valores diferentes?

Se uma for de, digamos, uma aplicação cliente externa e a outra estiver dentro do mesmo programa (ou seja, módulos de código diferentes), então eu diria que você deve ter dois métodos diferentes , um para cada caso, possivelmente até definido em interfaces distintas.

Se, no entanto, você estivesse fazendo a chamada com base em algum datum obtido de uma fonte que não seja de programa (digamos, um arquivo de configuração ou banco de dados lido), o método de passagem Boolean faria mais sentido.

    
por 16.05.2018 / 15:51
fonte
18

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

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

ou:

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

ou (corretamente, como sugerido por Frax):

UpdateRow(item, /* externalCall */true);
    
por 16.05.2018 / 22:02
fonte
10
  1. Você pode 'nomear' seus bools. Abaixo está um exemplo para um idioma OO (onde pode ser expresso na classe que fornece 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 de chamadas:

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

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Você pode modificar o nome da função de alguma forma que ela reflita melhor o significado do parâmetro bool . Não tenho muita certeza, mas talvez:

    UpdateRowWithExternalCall(var item, bool externalCall)

por 16.05.2018 / 15:32
fonte
9

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

Por exemplo, o IntelliJ IDEA imprime o nome da variável das variáveis no método para o qual você está chamando se estiver passando um literal como true ou null ou username + “@company.com . Isso é feito em uma fonte pequena, por isso, não ocupa muito espaço na tela e parece muito diferente do código real.

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

Exemplo modificado de parte da minha configuração de teste:

    
por 17.05.2018 / 05:33
fonte
3

2 dias e nenhum deles mencionou o polimorfismo?

target.UpdateRow(item);

Quando sou um cliente querendo atualizar uma linha com um item, não quero pensar no nome do banco de dados, na URL do micro-serviço, no protocolo usado para se comunicar ou em se Uma chamada externa precisará ser usada para que isso aconteça. Pare de empurrar esses detalhes para mim. Apenas pegue meu item e atualize 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 eu nomeei argumentos, não preciso disso". Tudo bem, ótimo, vá usá-los. Apenas mantenha esse absurdo fora do cliente de chamada que nem deveria saber com o que está falando.

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

    
por 18.05.2018 / 20:54
fonte
1

Se você puder modificar a função updateRow , talvez seja possível refatorá-la em duas funções. Dado que a função assume um parâmetro booleano, eu suspeito que se parece com isso:

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 radicalmente diferente dependendo de como a variável externalCall está definida e, nesse caso, ela tem duas responsabilidades diferentes. Refatorá-lo em duas funções que têm 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 para o qual você chama essas funções, é possível saber, de imediato, se o serviço externo está sendo chamado.

Este não é sempre o caso, claro. É situacional e uma questão de gosto. A refatoração de uma função que usa um parâmetro booleano em duas funções geralmente vale a pena ser considerada, mas nem sempre é a melhor escolha.

    
por 16.05.2018 / 20:18
fonte
0

Se o UpdateRow estiver em uma base de código controlada, 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 um 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 usaria apenas aqui. Como um efeito colateral gratuito útil, isso me dá uma opção para implementar outra maneira de acessar dados remotos, sem qualquer 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 de dados e o modelo.

    
por 18.05.2018 / 12:26
fonte

Tags