Wie kann ich mit einem Booleschen Clearer telefonieren? Boolesche Falle

76

Wie in den Kommentaren von @ benjamin-gruenbaum angemerkt, wird dies die Boolesche Falle genannt:

Angenommen, ich habe eine Funktion wie diese

UpdateRow(var item, bool externalCall);

und in meinem Controller wird dieser Wert für externalCallimmer WAHR sein. Wie kann man diese Funktion am besten aufrufen? Normalerweise schreibe ich

UpdateRow(item, true);

Aber ich frage mich, ob ich einen Booleschen deklarieren soll, nur um anzugeben, wofür dieser "wahre" Wert steht. Sie können das an der Deklaration der Funktion erkennen, aber es ist offensichtlich schneller und klarer, wenn Sie so etwas gesehen haben

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

PD: Ich bin mir nicht sicher, ob diese Frage wirklich hier passt. Wenn nicht, wo kann ich weitere Informationen dazu erhalten?

PD2: Ich habe keine Sprache markiert, weil ich dachte, es sei ein sehr allgemeines Problem. Wie auch immer, ich arbeite mit c # und die akzeptierte Antwort funktioniert für c #

Mario Garcia
quelle
10
Dieses Muster wird auch die Boolesche Falle genannt
Benjamin Gruenbaum
11
Wenn Sie eine Sprache verwenden, die algebraische Datentypen unterstützt, empfehle ich dringend einen neuen ADT anstelle eines Booleschen. data CallType = ExternalCall | InternalCallin haskell zum beispiel.
Filip Haglund,
6
Ich denke, Enums würde genau den gleichen Zweck erfüllen; Beschaffung von Namen für die Booleschen Werte und ein bisschen Typensicherheit.
Filip Haglund
2
Ich bin nicht der Meinung, dass es "offensichtlich klarer" ist, einen Booleschen Wert zu deklarieren, um die Bedeutung anzuzeigen. Bei der ersten Option ist es offensichtlich, dass Sie immer true übergeben. Mit der zweiten müssen Sie überprüfen (wo ist die Variable definiert? Ändert sich ihr Wert?). Klar, das ist kein Problem, wenn die beiden Zeilen zusammen sind ... aber jemand könnte entscheiden, dass der perfekte Ort zum Hinzufügen seines Codes genau zwischen den beiden Zeilen liegt. Es passiert!
AJPerez,
Das Deklarieren eines Booleschen Werts ist nicht sauberer, klarer, es wird nur über einen Schritt gesprungen, der sich in der Dokumentation der betreffenden Methode befindet. Wenn Sie die Signatur kennen, ist es weniger klar, eine neue Konstante hinzuzufügen.
Insidesin

Antworten:

154

Es gibt nicht immer eine perfekte Lösung, aber Sie haben viele Alternativen zur Auswahl:

  • Verwenden Sie benannte Argumente , sofern diese in Ihrer Sprache verfügbar sind. Dies funktioniert sehr gut und hat keine besonderen Nachteile. In einigen Sprachen kann jedes Argument als benanntes Argument übergeben werden, z. B. updateRow(item, externalCall: true)( C # ) oder update_row(item, external_call=True)(Python).

    Ihr Vorschlag, eine separate Variable zu verwenden, ist eine Möglichkeit, benannte Argumente zu simulieren, hat jedoch nicht die damit verbundenen Sicherheitsvorteile (es gibt keine Garantie dafür, dass Sie den richtigen Variablennamen für dieses Argument verwendet haben).

  • Verwenden Sie unterschiedliche Funktionen für Ihre öffentliche Schnittstelle mit besseren Namen. Dies ist eine weitere Möglichkeit, benannte Parameter zu simulieren, indem die Parameterwerte in den Namen eingefügt werden.

    Dies ist sehr gut lesbar, führt aber zu einer Menge Boilerplate für Sie, die diese Funktionen schreiben. Es kann auch nicht gut mit kombinatorischer Explosion umgehen, wenn es mehrere boolesche Argumente gibt. Ein wesentlicher Nachteil ist, dass Clients diesen Wert nicht dynamisch festlegen können, sondern if / else verwenden müssen, um die richtige Funktion aufzurufen.

  • Verwenden Sie eine Aufzählung . Das Problem mit Booleschen Werten ist, dass sie als "wahr" und "falsch" bezeichnet werden. Führen Sie stattdessen einen Typ mit besseren Namen ein (z enum CallType { INTERNAL, EXTERNAL }. B. ). Als zusätzlicher Vorteil, erhöht dies die Art Sicherheit Ihres Programms (wenn Ihre Sprache Aufzählungen als verschiedene Typen implementiert). Der Nachteil von Enums ist, dass sie Ihrer öffentlich sichtbaren API einen Typ hinzufügen. Für rein interne Funktionen spielt dies keine Rolle und Aufzählungen haben keine wesentlichen Nachteile.

    In Sprachen ohne Aufzählungen werden stattdessen manchmal kurze Zeichenfolgen verwendet. Dies funktioniert und ist möglicherweise sogar besser als rohe Boolesche Werte, ist jedoch sehr anfällig für Tippfehler. Die Funktion sollte dann sofort bestätigen, dass das Argument mit einer Reihe möglicher Werte übereinstimmt.

Keine dieser Lösungen wirkt sich negativ auf die Leistung aus. Benannte Parameter und Aufzählungen können zur Kompilierungszeit (für eine kompilierte Sprache) vollständig aufgelöst werden. Die Verwendung von Zeichenfolgen kann einen Zeichenfolgenvergleich erfordern, die Kosten hierfür sind jedoch für kleine Zeichenfolgenliterale und die meisten Arten von Anwendungen vernachlässigbar.

amon
quelle
7
Ich habe gerade erfahren, dass es "benannte Argumente" gibt. Ich denke, das ist bei weitem der beste Vorschlag. Wenn Sie ein wenig über diese Option nachdenken können (damit andere Personen nicht zu googeln brauchen), werde ich diese Antwort akzeptieren. Auch irgendwelche Hinweise auf die Leistung, wenn Sie können ... Danke
Mario Garcia
6
Eine Sache, die helfen kann, die Probleme mit kurzen Zeichenfolgen zu lindern, ist die Verwendung von Konstanten mit den Zeichenfolgenwerten. @ MarioGarcia Es gibt nicht viel zu erläutern. Abgesehen von dem, was hier erwähnt wird, hängen die meisten Details von der jeweiligen Sprache ab. Gab es etwas Besonderes, das Sie hier sehen wollten?
jpmc26
8
In Sprachen, in denen es sich um eine Methode handelt und die Aufzählung einer Klasse zugeordnet werden kann, verwende ich im Allgemeinen eine Aufzählung. Es ist vollständig selbstdokumentierend (in der einzelnen Codezeile, die den Aufzählungstyp deklariert, ist es also auch leichtgewichtig), verhindert vollständig, dass die Methode mit einem nackten Booleschen Wert aufgerufen wird, und die Auswirkung auf die öffentliche API ist vernachlässigbar (da die Bezeichner sind auf die Klasse beschränkt).
Davidbak
4
Ein weiteres Manko bei der Verwendung von Zeichenfolgen in dieser Rolle besteht darin, dass Sie im Gegensatz zu Aufzählungen die Gültigkeit bei der Kompilierung nicht überprüfen können.
Ruslan
32
Enum ist der Gewinner. Nur weil ein Parameter nur einen von zwei möglichen Zuständen haben kann, heißt das nicht, dass er automatisch ein Boolescher Wert sein sollte.
Pete
39

Die richtige Lösung ist, das zu tun, was Sie vorschlagen, aber es in eine Mini-Fassade zu packen:

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

Lesbarkeit trumpft mit Mikrooptimierung. Sie können sich den zusätzlichen Funktionsaufruf leisten, sicherlich besser als Sie es sich leisten können, die Semantik des Booleschen Flags einmal nachschlagen zu müssen.

Kilian Foth
quelle
8
Lohnt sich diese Kapselung wirklich, wenn ich diese Funktion in meinem Controller nur einmal aufrufe?
Mario Garcia
14
Ja. Ja ist es. Der Grund, wie ich oben schrieb, ist, dass die Kosten (ein Funktionsaufruf) geringer sind als der Nutzen (Sie sparen eine nicht unerhebliche Menge an Entwicklerzeit, weil niemand nachschauen muss, was der truetut.) Es wäre auch dann lohnenswert kosten so viel CPU-Zeit wie es Entwicklerzeit spart, da CPU-Zeit viel billiger ist.
Kilian Foth
8
Jeder kompetente Optimierer sollte dies auch für Sie tun können, damit Sie am Ende nichts verlieren.
firedraco
33
@KilianFoth Außer das ist eine falsche Annahme. Ein Betreuer muss wissen, was der zweite Parameter tut und warum er wahr ist, und dies bezieht sich fast immer auf das Detail dessen, was Sie versuchen zu tun. Das Ausblenden von Funktionen in Winzig-Funktionen verringert die Wartbarkeit, erhöht sie jedoch nicht. Ich habe es selbst gesehen, traurig zu sagen. Übermäßige Aufteilung in Funktionen kann tatsächlich schlimmer sein als eine riesige Gottesfunktion.
Graham
6
Ich denke, es UpdateRow(item, true /*external call*/);wäre sauberer in Sprachen, die diese Kommentarsyntax zulassen. Das Aufblähen Ihres Codes mit einer zusätzlichen Funktion, nur um das Schreiben eines Kommentars zu vermeiden, scheint es für einen einfachen Fall nicht wert. Vielleicht würde diese Funktion mehr Anklang finden, wenn es viele andere Argumente für diese Funktion gäbe und / oder etwas überfüllten und kniffligen Umgebungscode. Aber ich denke, wenn Sie debuggen, werden Sie am Ende die Wrapper-Funktion überprüfen, um zu sehen, was sie tut, und sich merken müssen, welche Funktionen Thin Wrapper um eine Bibliotheks-API sind und welche tatsächlich eine Logik haben.
Peter Cordes
25

Angenommen, ich habe eine Funktion wie UpdateRow (var item, bool externalCall);

Warum hast du so eine Funktion?

Unter welchen Umständen würden Sie das Argument externalCall auf unterschiedliche Werte setzen?

Wenn eine von beispielsweise einer externen Client-Anwendung stammt und die andere sich im selben Programm befindet (dh in verschiedenen Codemodulen), würde ich argumentieren, dass Sie zwei verschiedene Methoden haben sollten, eine für jeden Fall, möglicherweise sogar für verschiedene Schnittstellen.

Wenn Sie den Aufruf jedoch auf der Grundlage eines Datums aus einer Nicht-Programmquelle (z. B. einer Konfigurationsdatei oder einem Datenbanklesevorgang) ausführen würden, wäre die Methode der Booleschen Übergabe sinnvoller.

Phill W.
quelle
6
Genau. Und nur um die Aussage für andere Leser zu verdeutlichen, kann eine Analyse aller Aufrufer durchgeführt werden, die entweder true, false oder eine Variable übergeben. Wenn keine der letzteren gefunden wird, würde ich für zwei getrennte Methoden (ohne Booleschen Wert) über eine mit dem Booleschen Wert argumentieren - es ist ein YAGNI-Argument, dass der Boolesche Wert nicht verwendete / unnötige Komplexität einführt. Selbst wenn es einige Aufrufer gibt, die Variablen übergeben, kann ich dennoch die (booleschen) parameterfreien Versionen bereitstellen, die für Aufrufer verwendet werden, die eine Konstante übergeben - es ist einfacher, was gut ist.
Erik Eidt
18

Ich bin damit einverstanden, dass es ideal ist , eine Sprachfunktion zu verwenden, um sowohl Lesbarkeit als auch Wertsicherheit zu gewährleisten. Sie können sich jedoch auch für einen praktischen Ansatz entscheiden: Kommentare zur Anrufzeit. Mögen:

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

oder:

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

oder (zu Recht, wie von Frax vorgeschlagen):

UpdateRow(item, /* externalCall */true);
Bischof
quelle
1
Kein Grund zur Ablehnung. Dies ist ein durchaus gültiger Vorschlag.
Suncat2000,
Schätzen Sie die <3 @ Suncat2000!
Bischof
11
Eine (wahrscheinlich vorzuziehende) Variante ist, einfach den einfachen Argumentnamen dort zu platzieren, wie UpdateRow(item, /* externalCall */ true ). Ein vollständiger Satzkommentar ist viel schwerer zu analysieren, es ist eigentlich meistens Rauschen (insbesondere die zweite Variante, die auch sehr locker mit dem Argument verbunden ist).
Frax
1
Dies ist bei weitem die am besten geeignete Antwort. Genau dafür sind Kommentare gedacht.
Sentinel
9
Kein großer Fan von Kommentaren wie diesen. Kommentare neigen dazu, alt zu werden, wenn sie beim Umgestalten oder Vornehmen anderer Änderungen unberührt bleiben. Ganz zu schweigen davon, dass dies schnell verwirrend wird, sobald Sie mehr als einen Bool-Parameter haben.
John V.
11
  1. Sie können Ihre Narren "benennen". Nachfolgend finden Sie ein Beispiel für eine OO-Sprache (wo sie in der Klassenbereitstellung ausgedrückt werden kann UpdateRow()). Das Konzept selbst kann jedoch in einer beliebigen Sprache angewendet werden:

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

    und auf der Anrufseite:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Ich glaube, Punkt 1 ist besser, erzwingt aber nicht, dass der Benutzer neue Variablen verwendet, wenn er die Funktion verwendet. Wenn die Typensicherheit für Sie wichtig ist, können Sie einen enumTyp erstellen und UpdateRow()diesen anstelle eines bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Sie können den Namen der Funktion so ändern, dass er die Bedeutung des boolParameters besser widerspiegelt . Nicht sehr sicher, aber vielleicht:

    UpdateRowWithExternalCall(var item, bool externalCall)

simurg
quelle
8
Mag # 3 nicht, da externalCall=falseder Name der Funktion, mit der Sie sie aufrufen , absolut keinen Sinn ergibt . Der Rest Ihrer Antwort ist gut.
Leichtigkeitsrennen im Orbit
Einverstanden. Ich war mir nicht sicher, aber es könnte eine sinnvolle Option für eine andere Funktion sein. I
Simurg
@LightnessRacesinOrbit In der Tat. Es ist sicherer zu gehen UpdateRowWithUsuallyExternalButPossiblyInternalCall.
Eric Duminil
@EricDuminil: Spot on 😂
Leichtigkeitsrennen im Orbit
10

Eine andere Option, die ich hier noch nicht gelesen habe, ist: Verwenden Sie eine moderne IDE.

Beispielsweise gibt IntelliJ IDEA den Variablennamen der Variablen in der aufgerufenen Methode aus, wenn Sie ein Literal wie trueoder nulloder übergeben username + “@company.com. Dies geschieht in einer kleinen Schrift, damit sie nicht zu viel Platz auf Ihrem Bildschirm einnimmt und sich stark vom tatsächlichen Code unterscheidet.

Ich sage immer noch nicht, dass es eine gute Idee ist, überall Booleaner einzusetzen. Das Argument, dass Sie Code weitaus häufiger lesen als schreiben, ist oft sehr stark. In diesem speziellen Fall hängt es jedoch stark von der Technologie ab, mit der Sie (und Ihre Kollegen!) Ihre Arbeit unterstützen. Mit einer IDE ist das weitaus weniger ein Problem als zum Beispiel mit vim.

Geändertes Beispiel eines Teils meines Testaufbaus: Bildbeschreibung hier eingeben

Sebastiaan van den Broek
quelle
5
Dies wäre nur dann der Fall, wenn jeder in Ihrem Team jemals eine IDE zum Lesen Ihres Codes verwendet hätte. Ungeachtet der Verbreitung von Nicht-IDE-Editoren verfügen Sie auch über Tools zur Codeüberprüfung, Tools zur Quellcodeverwaltung, Stapelüberlauf-Fragen usw., und es ist von entscheidender Bedeutung, dass Code auch in diesen Kontexten lesbar bleibt.
Daniel Pryden
@ DanielPryden sicher, daher mein "und Ihre Kollegen" Kommentar. In Unternehmen ist es üblich, eine Standard-IDE zu haben. Was andere Tools angeht, würde ich sagen, dass es durchaus möglich ist, dass diese Tools dies oder das auch in Zukunft unterstützen oder dass diese Argumente einfach nicht zutreffen (wie bei einem 2-Personen-Paar-Programmierteam)
Sebastiaan van den Broek,
2
@Sebastiaan: Ich glaube nicht, dass ich damit einverstanden bin. Auch als Solo-Entwickler lese ich regelmäßig meinen eigenen Code in Git-Diffs. Git wird niemals in der Lage sein, eine kontextsensitive Visualisierung zu erstellen, wie dies eine IDE kann oder sollte. Ich bin alle für die Verwendung einer guten IDE, um Ihnen beim Schreiben von Code zu helfen, aber Sie sollten niemals eine IDE als Entschuldigung verwenden, um Code zu schreiben, den Sie ohne sie nicht geschrieben hätten.
Daniel Pryden
1
@ DanielPryden Ich befürworte nicht, extrem schlampigen Code zu schreiben. Es ist keine Schwarz-Weiß-Situation. Es kann Fälle geben, in denen Sie in der Vergangenheit für eine bestimmte Situation zu 40% dafür waren, eine Funktion mit einem Booleschen Wert zu schreiben, und zu 60% dafür, dass Sie dies nicht getan haben. Vielleicht liegt der Saldo bei guter IDE-Unterstützung jetzt bei 55%, die es so schreiben, und 45%, die es nicht schreiben. Und ja, Sie müssen es möglicherweise immer noch außerhalb der IDE lesen, damit es nicht perfekt ist. Aber es ist immer noch ein gültiger Kompromiss mit einer Alternative, wie das Hinzufügen einer anderen Methode oder einer Aufzählung, um den Code anderweitig zu klären.
Sebastiaan van den Broek
6

2 Tage und niemand erwähnte Polymorphismus?

target.UpdateRow(item);

Wenn ich ein Kunde bin, der eine Zeile mit einem Element aktualisieren möchte, möchte ich nicht über den Namen der Datenbank, die URL des Mikrodienstes, das zur Kommunikation verwendete Protokoll oder darüber nachdenken, ob es sich um einen externen Anruf handelt oder nicht müssen verwendet werden, um es zu verwirklichen. Hör auf, mir diese Details aufzuzwingen. Nimm einfach meinen Gegenstand und aktualisiere irgendwo schon eine Zeile.

Wenn Sie das tun, wird es Teil des Konstruktionsproblems. Das kann auf viele Arten gelöst werden. Hier ist eins:

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

Wenn Sie sich das ansehen und denken "Aber meine Sprache hat Argumente benannt, ich brauche das nicht". Gut, großartig, benutze sie. Halten Sie diesen Unsinn einfach von dem anrufenden Client fern, der nicht einmal wissen sollte, womit er spricht.

Es ist anzumerken, dass dies mehr als ein semantisches Problem ist. Es ist auch ein Designproblem. Übergeben Sie einen Booleschen Wert, und Sie sind gezwungen, mit der Verzweigung umzugehen. Nicht sehr objektorientiert. Müssen zwei oder mehr Booleaner übergeben werden? Möchten Sie, dass Ihre Sprache mehrfach versendet wird? Prüfen Sie, was Sammlungen tun können, wenn Sie sie verschachteln. Die richtige Datenstruktur kann Ihr Leben so viel einfacher machen.

kandierte_orange
quelle
2

Wenn Sie die updateRowFunktion ändern können, können Sie sie möglicherweise in zwei Funktionen umgestalten. Angesichts der Tatsache, dass die Funktion einen booleschen Parameter hat, vermute ich, dass dies so aussieht:

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

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

Das kann ein bisschen nach Code riechen. Die Funktion kann je nach externalCallEinstellung der Variablen ein dramatisch unterschiedliches Verhalten aufweisen. In diesem Fall hat sie zwei unterschiedliche Verantwortlichkeiten. Das Umgestalten in zwei Funktionen, die nur eine Verantwortung haben, kann die Lesbarkeit verbessern:

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

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

Überall dort, wo Sie diese Funktionen aufrufen, können Sie jetzt auf einen Blick feststellen, ob der externe Dienst aufgerufen wird.

Dies ist natürlich nicht immer der Fall. Es ist situativ und Geschmackssache. Das Refactoring einer Funktion, die einen booleschen Parameter in zwei Funktionen umwandelt, ist normalerweise erwägenswert, aber nicht immer die beste Wahl.

Kevin
quelle
0

Wenn sich UpdateRow in einer kontrollierten Codebasis befindet, würde ich ein Strategiemuster in Betracht ziehen:

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

Wobei Request eine Art DAO darstellt (ein Rückruf in trivialem Fall).

Wahrer Fall:

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

Falscher Fall:

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

Normalerweise wird die Endpunktimplementierung auf einer höheren Abstraktionsebene konfiguriert, und ich würde sie hier nur verwenden. Als nützlichen kostenlosen Nebeneffekt habe ich die Möglichkeit, einen anderen Weg für den Zugriff auf entfernte Daten zu implementieren, ohne den Code zu ändern:

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

Im Allgemeinen sorgt eine solche Umkehrung der Steuerung für eine geringere Kopplung zwischen Datenspeicher und Modell.

Basilevs
quelle