Come posso effettuare una chiamata con un clearer booleano? Trappola Booleana


76

Come notato nei commenti di @ benjamin-gruenbaum, questa è chiamata la trappola booleana:

Di 'che ho una funzione come questa

UpdateRow(var item, bool externalCall);

e nel mio controller, quel valore per externalCallsarà sempre VERO. Qual è il modo migliore per chiamare questa funzione? Di solito scrivo

UpdateRow(item, true);

Ma chiedo a me stesso, dovrei dichiarare un valore booleano, solo per indicare che cosa rappresenta quel valore "vero"? Puoi saperlo guardando la dichiarazione della funzione, ma è ovviamente più veloce e più chiaro se hai appena visto qualcosa di simile

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

PD: Non sono sicuro che questa domanda si adatti davvero qui, in caso contrario, dove posso ottenere maggiori informazioni su questo?

PD2: Non ho taggato nessuna lingua perché pensavo fosse un problema molto generico. In ogni caso, lavoro con c # e la risposta accettata funziona per c #


10
Questo schema è anche chiamato la trappola booleana
Benjamin Gruenbaum,

11
Se stai usando una lingua con supporto per tipi di dati algebrici, consiglio vivamente un nuovo annuncio invece di un valore booleano. data CallType = ExternalCall | InternalCallin haskell per esempio.
Filip Haglund,

6
Immagino che Enums avrebbe riempito lo stesso identico scopo; ottenere nomi per i booleani e un po 'di sicurezza del tipo.
Filip Haglund,

2
Non sono d'accordo sul fatto che dichiarare un booleano per indicare il significato sia "ovviamente più chiaro". Con la prima opzione, è ovvio che passerai sempre vero. Con il secondo, devi controllare (dove viene definita la variabile? Cambia il suo valore?). Certo, questo non è un problema se le due linee sono insieme ... ma qualcuno potrebbe decidere che il posto perfetto per aggiungere il proprio codice è esattamente tra le due linee. Succede!
AJPerez,

Dichiarare un booleano non è più pulito, più chiaro è solo saltare un gradino che è, guardando la documentazione del metodo in questione. Se conosci la firma, è meno chiaro aggiungere una nuova costante.
insidesin

Risposte:


154

Non c'è sempre una soluzione perfetta, ma hai molte alternative tra cui scegliere:

  • Usa argomenti con nome , se disponibili nella tua lingua. Funziona molto bene e non presenta particolari inconvenienti. In alcune lingue, qualsiasi argomento può essere passato come argomento denominato, ad esempio updateRow(item, externalCall: true)( C # ) o update_row(item, external_call=True)(Python).

    Il tuo suggerimento di utilizzare una variabile separata è un modo per simulare argomenti con nome, ma non ha i vantaggi di sicurezza associati (non c'è garanzia che tu abbia usato il nome di variabile corretto per quell'argomento).

  • Usa funzioni distinte per la tua interfaccia pubblica, con nomi migliori. Questo è un altro modo di simulare i parametri nominati, inserendo i valori del paremetro nel nome.

    Questo è molto leggibile, ma porta a molta piastra di caldaia per te, che stai scrivendo queste funzioni. Inoltre, non può affrontare bene l'esplosione combinatoria quando ci sono più argomenti booleani. Uno svantaggio significativo è che i client non possono impostare questo valore in modo dinamico, ma devono usare if / else per chiamare la funzione corretta.

  • Usa un enum . Il problema con i booleani è che sono chiamati "vero" e "falso". Quindi, invece, introdurre un tipo con nomi migliori (ad esempio enum CallType { INTERNAL, EXTERNAL }). Come ulteriore vantaggio, questo aumenta la sicurezza dei tipi del tuo programma (se la tua lingua implementa enum come tipi distinti). Lo svantaggio di enum è che aggiungono un tipo alla tua API visibile pubblicamente. Per funzioni puramente interne, questo non ha importanza e gli enum non hanno svantaggi significativi.

    In lingue senza enumerazioni, a volte vengono utilizzate stringhe brevi . Funziona e potrebbe persino essere migliore dei booleani grezzi, ma è molto sensibile agli errori di battitura. La funzione dovrebbe quindi affermare immediatamente che l'argomento corrisponde a una serie di valori possibili.

Nessuna di queste soluzioni ha un impatto sulle prestazioni proibitivo. I parametri nominati e gli enum possono essere risolti completamente al momento della compilazione (per un linguaggio compilato). L'uso delle stringhe può comportare un confronto delle stringhe, ma il costo è trascurabile per i letterali di stringhe piccole e la maggior parte dei tipi di applicazioni.


7
Ho appena imparato che esistono "argomenti denominati". Penso che questo sia di gran lunga il miglior suggerimento. Se puoi eludere un po 'questa opzione (in modo che altre persone non abbiano bisogno di google al riguardo), accetterò questa risposta. Anche qualche suggerimento sulle prestazioni se puoi ... Grazie
Mario Garcia,

6
Una cosa che può aiutare ad alleviare i problemi con stringhe brevi è usare costanti con i valori di stringa. @MarioGarcia Non c'è molto da approfondire. A parte quanto menzionato qui, la maggior parte dei dettagli dipenderà dalla lingua particolare. C'era qualcosa di particolare che volevi vedere menzionato qui?
jpmc26,

8
Nelle lingue in cui stiamo parlando di un metodo e l'enum può essere impostato su una classe in genere uso un enum. È completamente auto-documentante (nella singola riga di codice che dichiara il tipo enum, quindi è anche leggero), impedisce completamente che il metodo venga chiamato con un booleano nudo e l'impatto sull'API pubblica è trascurabile (poiché gli identificatori sono inclusi nella classe).
davidbak,

4
Un altro difetto dell'utilizzo delle stringhe in questo ruolo è che non è possibile verificarne la validità in fase di compilazione, a differenza di enums.
Ruslan,

32
enum è il vincitore. Solo perché un parametro può avere solo uno dei due stati possibili, ciò non significa che dovrebbe essere automaticamente un valore booleano.
Pete,

39

La soluzione corretta è fare ciò che suggerisci, ma impacchettalo in una mini-facciata:

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

La leggibilità supera la micro-ottimizzazione. Puoi permetterti la chiamata di funzione extra, sicuramente meglio di quanto ti possa permettere lo sforzo degli sviluppatori di dover cercare la semantica della bandiera booleana anche una volta.


8
Questo incapsulamento vale davvero la pena quando chiamo quella funzione una sola volta nel mio controller?
Mario Garcia,

14
Sì. Sì. Il motivo, come ho scritto sopra, è che il costo (una chiamata di funzione) è inferiore al vantaggio (si risparmia una quantità non banale di tempo di sviluppo perché nessuno deve mai cercare quello che truefa.) Sarebbe utile anche se costa tanto tempo CPU quanto fa risparmiare tempo allo sviluppatore, poiché il tempo CPU è molto più economico.
Kilian Foth,

8
Inoltre, qualsiasi ottimizzatore competente dovrebbe essere in grado di includerlo per te, quindi alla fine non dovresti perdere nulla.
firedraco,

33
@KilianFoth Tranne che è un falso presupposto. Un manutentore ha bisogno di sapere cosa fa il secondo parametro e perché è vero, e quasi sempre questo si riferisce al dettaglio di ciò che stai cercando di fare. Nascondere la funzionalità nelle minuscole funzioni di decine di migliaia riduce la manutenibilità, non la aumenta. L'ho visto accadere da solo, triste da dire. L'eccessiva suddivisione in funzioni può effettivamente essere peggiore di un'enorme funzione divina.
Graham,

6
Penso che UpdateRow(item, true /*external call*/);sarebbe più pulito, nelle lingue che consentono quella sintassi del commento. Gonfiare il codice con una funzione aggiuntiva solo per evitare di scrivere un commento non sembra valerne la pena per un semplice caso. Forse se ci fossero molti altri argomenti in questa funzione e / o qualche codice ingombrante + ingannevole, inizierebbe a fare appello di più. Ma penso che se stai eseguendo il debug finirai per controllare la funzione wrapper per vedere cosa fa, e devi ricordare quali funzioni sono wrapper sottili attorno a un'API della libreria e quali in realtà hanno qualche logica.
Peter Cordes,

25

Supponiamo che io abbia una funzione come UpdateRow (var item, bool externalCall);

Perché hai una funzione come questa?

In quali circostanze lo chiameresti con l'argomento externalCall impostato su valori diversi?

Se uno proviene da un'applicazione client esterna e l'altro è all'interno dello stesso programma (ovvero moduli di codice diversi), direi che dovresti avere due metodi diversi , uno per ogni caso, possibilmente anche definiti su distinti interfacce.

Se, tuttavia, stavi effettuando la chiamata in base a un dato prelevato da una fonte non di programma (ad esempio, un file di configurazione o una lettura del database), il metodo di passaggio booleano avrebbe più senso.


6
Sono d'accordo. E solo per chiarire il punto sollevato per gli altri lettori, si può fare un'analisi di tutti i chiamanti, che passeranno vero, falso o una variabile. Se non venisse trovato nessuno di questi argomenti, vorrei discutere per due metodi separati (senza booleano) su uno con il booleano: è un argomento YAGNI che il booleano introduce complessità non utilizzata / non necessaria. Anche se ci sono alcuni chiamanti che stanno passando variabili, potrei comunque fornire le versioni (booleane) prive di parametri da usare per i chiamanti che passano una costante - è più semplice, il che è buono.
Erik Eidt,

18

Anche se concordo sul fatto che sia ideale utilizzare una funzionalità linguistica per rafforzare la leggibilità e la sicurezza del valore, puoi anche optare per un approccio pratico : i commenti al momento della chiamata. Piace:

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

o:

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

o (giustamente, come suggerito da Frax):

UpdateRow(item, /* externalCall */true);

1
Nessun motivo per votare. Questo è un suggerimento perfettamente valido.
Suncat2000,

Apprezzo <3 @ Suncat2000!
vescovo,

11
Una variante (probabilmente preferibile) sta semplicemente mettendo lì il nome dell'argomento semplice, come UpdateRow(item, /* externalCall */ true ). Il commento a frase intera è molto più difficile da analizzare, in realtà è per lo più rumore (in particolare la seconda variante, che è anche molto liberamente accoppiata all'argomento).
Frax,

1
Questa è di gran lunga la risposta più appropriata. Questo è esattamente ciò a cui sono destinati i commenti.
Sentinella

9
Non è un grande fan di commenti come questo. I commenti tendono a diventare stantii se non vengono toccati durante il refactoring o apportando altre modifiche. Per non parlare non appena hai più di un bool param, questo diventa rapidamente confuso.
John V.

11
  1. Puoi "nominare" i tuoi bool. Di seguito è riportato un esempio per un linguaggio OO (dove può essere espresso nella classe che fornisce UpdateRow()), tuttavia il concetto stesso può essere applicato in qualsiasi lingua:

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

    e nel sito di chiamata:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Credo che l'articolo n. 1 sia migliore, ma non impone all'utente di utilizzare nuove variabili quando utilizza la funzione. Se la sicurezza dei tipi è importante per te, puoi crearne uno enume farlo UpdateRow()accettare invece di bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. È possibile modificare il nome della funzione in qualche modo in modo che rifletta meglio il significato del boolparametro. Non molto sicuro ma forse:

    UpdateRowWithExternalCall(var item, bool externalCall)


8
Non mi piace il 3 come adesso se chiami la funzione con externalCall=false, il suo nome non ha assolutamente senso. Il resto della tua risposta è buona.
Lightness Races con Monica

Concordato. Non ne ero molto sicuro, ma potrebbe essere un'opzione praticabile per qualche altra funzione. I
simurg

@LightnessRacesinOrbit Effettivamente. È più sicuro andare per UpdateRowWithUsuallyExternalButPossiblyInternalCall.
Eric Duminil,

@EricDuminil: Spot on 😂
Lightness Races con Monica

10

Un'altra opzione che non ho ancora letto qui è: utilizzare un IDE moderno.

Ad esempio IntelliJ IDEA stampa il nome della variabile delle variabili nel metodo che stai chiamando se stai passando un valore letterale come trueo nullo username + “@company.com. Questo viene fatto con un carattere piccolo, quindi non occupa troppo spazio sullo schermo e sembra molto diverso dal codice reale.

Non sto ancora dicendo che sia una buona idea lanciare booleani ovunque. L'argomento secondo cui leggi il codice molto più spesso di quanto scrivi è spesso molto forte, ma per questo caso particolare dipende fortemente dalla tecnologia che tu (e i tuoi colleghi!) Utilizzate per supportare il vostro lavoro. Con un IDE è molto meno un problema che ad esempio con vim.

Esempio modificato di parte della mia configurazione di prova: inserisci qui la descrizione dell'immagine


5
Ciò sarebbe vero solo se tutti i membri del tuo team usassero un IDE per leggere il tuo codice. Nonostante la prevalenza di editor non IDE, hai anche strumenti di revisione del codice, strumenti di controllo del codice sorgente, domande Stack Overflow, ecc. Ed è di vitale importanza che il codice rimanga leggibile anche in quei contesti.
Daniel Pryden,

@DanielPryden certo, da qui il mio commento "e dei tuoi colleghi". È comune avere un IDE standard nelle aziende. Per quanto riguarda altri strumenti, direi che è del tutto possibile che questi tipi di strumenti supportino anche questo o lo faranno in futuro, o che tali argomenti semplicemente non si applicano (come per un team di programmazione di coppie di 2 persone)
Sebastiaan van den Broek,

2
@Sebastiaan: non credo di essere d'accordo. Anche come sviluppatore solista, leggo regolarmente il mio codice in Git diffs. Git non sarà mai in grado di fare lo stesso tipo di visualizzazione consapevole del contesto che un IDE può fare, né dovrebbe farlo. Sono tutto per l'uso di un buon IDE per aiutarti a scrivere codice, ma non dovresti mai usare un IDE come scusa per scrivere codice che non avresti scritto senza di esso.
Daniel Pryden,

1
@DanielPryden Non sto sostenendo di scrivere codice estremamente sciatto. Non è una situazione in bianco e nero. Ci possono essere casi in cui in passato per una situazione specifica saresti il ​​40% a favore della scrittura di una funzione con un valore booleano e il 60% no e alla fine non lo faresti. Forse con un buon supporto IDE ora il saldo è del 55% scrivendolo in quel modo e il 45% no. E sì, a volte potresti ancora aver bisogno di leggerlo al di fuori dell'IDE, quindi non è perfetto. Ma è ancora un valido compromesso con un'alternativa, come l'aggiunta di un altro metodo o enumerazione per chiarire altrimenti il ​​codice.
Sebastiaan van den Broek,

6

2 giorni e nessuno ha menzionato il polimorfismo?

target.UpdateRow(item);

Quando sono un cliente che desidera aggiornare una riga con un elemento non voglio pensare al nome del database, all'URL del micro-servizio, al protocollo utilizzato per comunicare o alla presenza o meno di una chiamata esterna dovrà essere usato per realizzarlo. Smetti di spingere questi dettagli su di me. Prendi il mio oggetto e vai ad aggiornare una riga già da qualche parte.

Fallo e diventa parte del problema di costruzione. Questo può essere risolto in molti modi. Eccone uno:

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

Se lo stai guardando e pensando "Ma la mia lingua ha chiamato argomenti, non ne ho bisogno". Bene, ottimo, vai ad usarli. Basta tenere fuori questa assurdità dal client chiamante che non dovrebbe nemmeno sapere con cosa sta parlando.

Va notato che questo è più di un problema semantico. È anche un problema di progettazione. Passa un booleano e sei costretto a usare la ramificazione per affrontarlo. Non molto orientato agli oggetti. Devono passare due o più booleani? Desideri che la tua lingua abbia ricevuto più spedizioni? Scopri cosa possono fare le raccolte quando le annidate. La corretta struttura dei dati può semplificarti la vita.


2

Se puoi modificare la updateRowfunzione, forse puoi rifattarla in due funzioni. Dato che la funzione accetta un parametro booleano, sospetto che assomigli a questo:

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

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

Questo può essere un po 'un odore di codice. La funzione potrebbe avere un comportamento notevolmente diverso a seconda dell'impostazione della externalCallvariabile, nel qual caso ha due diverse responsabilità. Rifattorizzarlo in due funzioni che hanno una sola responsabilità può migliorare la leggibilità:

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

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

Ora, ovunque tu chiami queste funzioni, puoi capire a colpo d'occhio se viene chiamato il servizio esterno.

Questo non è sempre il caso, ovviamente. È situazionale e una questione di gusti. Rifattorizzare una funzione che prende un parametro booleano in due funzioni di solito vale la pena considerare, ma non è sempre la scelta migliore.


0

Se UpdateRow si trova in una base di codice controllata, prenderei in considerazione il modello di strategia:

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

Where Request rappresenta una specie di DAO (un callback in casi banali).

Caso vero:

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

Caso falso:

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

Di solito, l'implementazione dell'endpoint sarebbe configurata su un livello superiore di astrazione e la userei qui. Come utile effetto collaterale gratuito, questo mi dà la possibilità di implementare un altro modo per accedere ai dati remoti, senza alcuna modifica al codice:

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

In generale, tale inversione di controllo garantisce un accoppiamento inferiore tra la memorizzazione dei dati e il modello.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.