Sollten Programmierer boolesche Variablen verwenden, um ihren Code zu „dokumentieren“?

79

Ich lese McConells Code Complete und er diskutiert die Verwendung von Booleschen Variablen, um Ihren Code zu dokumentieren. Zum Beispiel anstelle von:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) || 
   (elementIndex == lastElementIndex)){
       ...
}

Er schlägt vor:

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

Dies erscheint mir logisch, bewährte und sehr selbstdokumentierend. Ich zögere jedoch, diese Technik regelmäßig anzuwenden, da ich fast nie darauf gestoßen bin. und vielleicht wäre es nur deshalb verwirrend, weil es selten ist. Meine Erfahrung ist jedoch noch nicht sehr umfangreich, daher bin ich daran interessiert, die Meinung der Programmierer zu dieser Technik zu hören, und ich wäre gespannt, ob jemand diese Technik regelmäßig verwendet oder sie beim Lesen von Code oft gesehen hat. Ist dies eine lohnende Konvention / Stil / Technik? Werden andere Programmierer es verstehen und schätzen oder es für seltsam halten?

froadie
quelle
@ Paul R - whoops, danke. Ich begann mit der Selbstdokumentation und stellte fest, dass es dafür kein Tag gab. Deshalb versuchte ich, es in eine bereits vorhandene Selbstdokumentation umzuwandeln. :)
Froadie
Ist es nicht besser, nur zu kommentieren, was im Test passiert?
JS
3
Gegebenenfalls versuche ich dies auch, insbesondere wenn ich in verschiedenen Teilen der Methode nach den Bedingungen suchen muss. Es ist auch viel aussagekräftiger.
Geffchang
"Das scheint mir logisch, eine gute Praxis und sehr selbstdokumentierend." Aufgrund Ihrer Erfahrung ist es wahrscheinlich, dass andere Programmierer, die zum ersten Mal darauf stoßen, dies genauso leicht verstehen.
2
Warum ist diese Frage noch offen?
Orangepips

Antworten:

55

Das Aufteilen eines zu verschachtelten und komplizierten Ausdrucks in einfachere Unterausdrücke, die lokalen Variablen zugewiesen und dann wieder zusammengesetzt werden, ist eine weit verbreitete und beliebte Technik - ganz unabhängig davon, ob die Unterausdrücke und / oder der Gesamtausdruck boolesch sind oder von fast jeder andere Typ. Bei ausgewählten Namen kann eine geschmackvolle Zerlegung dieser Art die Lesbarkeit verbessern, und ein guter Compiler sollte keine Probleme haben, Code zu generieren, der dem ursprünglichen, komplizierten Ausdruck entspricht.

Einige Sprachen, die nicht das Konzept der "Zuweisung" an sich haben, wie Haskell, führen sogar spezielle Konstrukte ein, mit denen Sie die Technik "einem Unterausdruck einen Namen geben" (die whereKlausel in Haskell) verwenden können - scheinen einige zu sprechen Popularität für die betreffende Technik! -)

Alex Martelli
quelle
6
Wenn es einfacher und leichter zu lesen ist, sage ich, dass dies ein ziemlich klarer Fall von Win-Win ist :)
djcouchycouch
Genau. In vielen Fällen könnten Sie wahrscheinlich mit einem kurzen Kommentar davonkommen, aber ich sehe nicht ein, wie dies tatsächlich schaden könnte.
Max E.
16

Ich habe es verwendet, obwohl ich normalerweise boolesche Logik in eine wiederverwendbare Methode einbinde (wenn sie von mehreren Speicherorten aufgerufen wird).

Es verbessert die Lesbarkeit und wenn sich die Logik ändert, muss es nur an einer Stelle geändert werden.

Andere werden es verstehen und es nicht seltsam finden (außer denen, die immer nur tausend Zeilenfunktionen schreiben).

Oded
quelle
Danke für die Antwort! In Bezug auf die wiederverwendbaren Methoden hat dies einen anderen (nicht verwandten) gültigen Grund, der herausgerechnet werden muss ... Ich nehme an, meine Frage ist wirklich, ob Sie einmalige boolesche Ausdrücke herausrechnen sollten, wenn es keinen anderen Grund als die Lesbarkeit gibt. (Was natürlich ein Grund genug für sich ist :)) Danke, dass Sie darauf hingewiesen haben.
Froadie
+1, um die Codeänderungen zu reduzieren, die erforderlich sind, wenn sich die Logik ändert, und um sich über Programmierer mit tausend Zeilenfunktionen lustig zu machen.
Jeffrey L Whitledge
9

Ich versuche es wo immer möglich. Sicher, Sie verwenden eine "zusätzliche Codezeile", aber gleichzeitig beschreiben Sie, warum Sie zwei Werte vergleichen.

In Ihrem Beispiel schaue ich mir den Code an und frage mich: "Okay, warum ist die Person, die den Wert sieht, kleiner als 0?" Im zweiten Teil sagen Sie mir deutlich, dass einige Prozesse in diesem Fall abgeschlossen sind. Keine Vermutung im zweiten, was Ihre Absicht war.

Das große Problem für mich ist, wenn ich eine Methode wie die folgende sehe: DoSomeMethod(true); Warum wird sie automatisch auf true gesetzt? Es ist viel besser lesbar

bool deleteOnCompletion = true;

DoSomeMethod(deleteOnCompletion);
kemiller2002
quelle
7
Genau aus diesem Grund mag ich keine booleschen Parameter. Am Ende erhalten Sie Aufrufe wie "createOrder (wahr, falsch, wahr, wahr, falsch)" und was bedeutet das? Ich bevorzuge die Verwendung von Aufzählungen, also sagen Sie etwas wie "createOrder (Source.MAIL_ORDER, BackOrder.NO, CustomOrder.CUSTOM, PayType.CREDIT)".
Jay
Aber wenn Sie Kevins Beispiel folgen, entspricht es Ihrem. Welchen Unterschied macht es, ob die Variable 2 Werte oder mehr als 2 annehmen kann?
Mark Ruzon
2
Mit Jay's können Sie den Vorteil haben, in bestimmten Fällen definitiv klarer zu sein. Zum Beispiel bei Verwendung des PayType. Wenn es ein Boolescher Wert wäre, würde der Parameter wahrscheinlich isPayTypeCredit heißen. Sie wissen nicht, was die Alternative ist. Mit einer Aufzählung können Sie deutlich sehen, welche Optionen der PayType bietet: Gutschrift, Scheck, Bargeld und wählen Sie die richtige aus.
kemiller2002
++ auch eine Aufzählung erlaubt keine
Nullzuweisung, so dass die Wertkontrolle
Objective-C und Smalltalk lösen dieses Problem wirklich in Objective-C:[Object createOrderWithSource:YES backOrder:NO custom:YES type:kCreditCard];
Grant Paul
5

Das bereitgestellte Beispiel:

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

Kann auch umgeschrieben werden, um Methoden zu verwenden, die die Lesbarkeit verbessern und die boolesche Logik beibehalten (wie Konrad betonte):

if (IsFinished(elementIndex) || IsRepeatedEntry(elementIndex, lastElementIndex)){
   ...
}

...

private bool IsFinished(int elementIndex) {
    return ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
}

private bool IsRepeatedEntry(int elementIndex, int lastElementIndex) {
    return (elementIndex == lastElementIndex);
}

Es hat natürlich einen Preis, der zwei zusätzliche Methoden darstellt. Wenn Sie dies häufig tun, wird Ihr Code möglicherweise besser lesbar, Ihre Klassen jedoch weniger transparent. Andererseits können Sie die zusätzlichen Methoden auch in Hilfsklassen verschieben.

Prutswonder
quelle
Wenn Sie mit C # in eine Menge Code-Rauschen geraten sind, können Sie auch Teilklassen nutzen und das Rauschen in das Teil verschieben. Wenn die Leute daran interessiert sind, was IsFinished überprüft, ist es einfach, zu springen.
Chris Marisic
3

Ich kann nur sehen, dass dies schief geht, wenn das boolesche Fragment keinen sinnvollen Namen hat und trotzdem ein Name ausgewählt wird.

//No clue what the parts might mean.
if(price>0 && (customer.IsAlive || IsDay(Thursday)))

=>

first_condition = price>0
second_condition =customer.IsAlive || IsDay(Thursday)

//I'm still not enlightened.
if(first_condition && second_condition)

Ich weise darauf hin, weil es üblich ist, Regeln wie "Kommentieren Sie Ihren gesamten Code" zu erstellen, "benannte Boolesche Werte für alle if-Kriterien mit mehr als 3 Teilen zu verwenden", um nur Kommentare zu erhalten, die semantisch leer der folgenden Art sind

i++; //increment i by adding 1 to i's previous value
MatthewMartin
quelle
2
Sie haben die Bedingungen in Ihrem Beispiel falsch gruppiert, oder? '&&' bindet fester als '||' in den meisten Sprachen, die sie verwenden (Shell-Skripte sind eine Ausnahme).
Jonathan Leffler
Parens hinzugefügt. Dies wäre also ein weiterer Schlag gegen die Aufteilung in benannte Variablen. Es bietet die Möglichkeit, die Gruppierung auf nicht offensichtliche Weise zu ändern.
Matthew Martin
2

Auf diese Weise

finished = ((elementIndex < 0) || (MAX_ELEMENTS < elementIndex));
repeatedEntry = (elementIndex == lastElementIndex);
if(finished || repeatedEntry){
   ...
}

Sie entfernen die Logik aus Ihrem Gehirn und fügen sie in den Code ein. Jetzt weiß das Programm , was Sie gemeint haben.
Wann immer Sie etwas benennen , geben Sie es physisch dar. Es existiert.
Sie können es manipulieren und wiederverwenden.

Sie können sogar den gesamten Block als Prädikat definieren:

bool ElementBlahBlah? (elementIndex, lastElementIndex);

und mache mehr Sachen (später) in dieser Funktion.

Nick Dandoulakis
quelle
Und was noch wichtiger ist, der nächste Entwickler, der sich den Code ansieht, weiß auch, was Sie gemeint haben! Das ist eine großartige Übung, und ich mache sie die ganze Zeit.
Chris Thornton
1
Außerdem kann dieser nächste Entwickler oft Sie selbst sein, der den Code nach einigen Monaten (oder sogar einigen Wochen) erneut betrachtet und froh ist, dass er gut selbst dokumentiert wurde.
Louis
2

Wenn der Ausdruck komplex ist, verschiebe ich ihn entweder in eine andere Funktion, die ein booleg zurückgibt , isAnEveningInThePubAGoodIdea(dayOfWeek, sizeOfWorkLoad, amountOfSpareCash)oder überarbeite den Code, damit ein solch komplexer Ausdruck nicht erforderlich ist.

Benedict Cohen
quelle
2

Ich denke, es ist besser, Funktionen / Methoden anstelle von temporären Variablen zu erstellen. Auf diese Weise wird die Lesbarkeit erhöht, auch weil die Methoden kürzer werden. Martin Fowlers Buch Refactoring enthält gute Ratschläge zur Verbesserung der Codequalität. Refactorings, die sich auf Ihr spezielles Beispiel beziehen, heißen "Temp durch Abfrage ersetzen" und "Extraktionsmethode".

mkj
quelle
2
Wollen Sie damit sagen, dass Sie die Lesbarkeit verbessern, indem Sie den Klassenraum mit vielen einmaligen Funktionen überladen? Bitte erkläre.
Zano
Es ist immer ein Kompromiss. Die Lesbarkeit der ursprünglichen Funktion wird verbessert. Wenn die ursprüngliche Funktion kurz ist, lohnt es sich möglicherweise nicht.
mkj
Ich denke auch, dass "das Durcheinander des Klassenraums" von der verwendeten Sprache und der Partitionierung Ihres Codes abhängt.
mkj
2

Persönlich denke ich, dass dies eine großartige Praxis ist. Die Auswirkungen auf die Ausführung des Codes sind minimal, aber die Klarheit, die er bei ordnungsgemäßer Verwendung bieten kann, ist von unschätzbarem Wert, wenn es darum geht, den Code später zu warten.

GrowlingDog
quelle
1

Denken Sie daran, dass Sie auf diese Weise mehr als nötig berechnen. Da Sie Bedingungen aus dem Code herausnehmen, berechnen Sie immer beide (kein Kurzschluss).

Damit:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) || 
   (elementIndex == lastElementIndex)){
   ...
}

Nach der Transformation wird:

if((elementIndex < 0) || (MAX_ELEMENTS < elementIndex) |
   (elementIndex == lastElementIndex)){
   ...
}

In den meisten Fällen kein Problem, aber in einigen Fällen kann dies eine schlechtere Leistung oder andere Probleme bedeuten, z. B. wenn Sie im zweiten Ausdruck davon ausgehen, dass der erste fehlgeschlagen ist.

Konrad Garus
quelle
Richtig - ich habe dies gerade bemerkt, als ich zu einem Teil meines Codes zurückkehrte, um einige if-Anweisungen mit dieser Methode zu überarbeiten. Es gab eine if-Anweisung, die die Kurzschlussauswertung mit einem && ausnutzte (der zweite Teil löst eine NPE aus, wenn der erste Teil falsch ist), und mein Code schlug fehl, als ich dies überarbeitete (weil er immer beide auswertete und sie in Booleschen Werten speicherte Variablen). Guter Punkt, danke! Aber ich habe mich gefragt, als ich das versucht habe - gibt es eine Möglichkeit, die Logik in einer Variablen zu speichern und die Auswertung bis zur tatsächlichen if-Anweisung zu verzögern?
Froadie
2
Ich bezweifle tatsächlich, dass der Compiler das lösen wird. Wenn der zweite Aufruf nicht effektiv ist, ist dies normalerweise auf den Aufruf einer Funktion zurückzuführen, und AFAIK kein Compiler versucht festzustellen, ob eine aufgerufene Funktion ohne Nebenwirkungen ist.
JS
Sie können die IFs verschachteln und die späteren Berechnungen nur durchführen, wenn der erste Test nicht ausreicht, um zu entscheiden, ob Sie fortfahren möchten.
Jay
1

Wenn die Methode eine Benachrichtigung über den Erfolg erfordert: (Beispiele in c #) Ich verwende gerne die

bool success = false;

anfangen. Der Code ist ein Fehler, bis ich ihn ändere in:

success = true;

dann am Ende:

return success;
Chris Hayes
quelle
0

Ich denke, es hängt davon ab, welchen Stil Sie / Ihr Team bevorzugen. Refactoring "Variable einführen" könnte nützlich sein, aber manchmal nicht :)

Und ich sollte Kevin in seinem vorherigen Beitrag nicht zustimmen. Sein Beispiel, nehme ich an, kann verwendet werden, wenn die eingeführte Variable geändert werden kann, aber das Einführen nur für einen statischen Booleschen Wert ist nutzlos, da wir in einer Methodendeklaration einen Parameternamen haben. Warum also im Code duplizieren?

zum Beispiel:

void DoSomeMethod(boolean needDelete) { ... }

// useful
boolean deleteOnCompletion = true;
if ( someCondition ) {
    deleteOnCompletion = false;
}
DoSomeMethod(deleteOnCompletion);

// useless
boolean shouldNotDelete = false;
DoSomeMethod(shouldNotDelete);
dchekmarev
quelle
0

Nach meiner Erfahrung bin ich oft zu alten Skripten zurückgekehrt und habe mich gefragt, was zum Teufel ich damals gedacht habe. Zum Beispiel:

Math.p = function Math_p(a) {
    var r = 1, b = [], m = Math;
    a = m.js.copy(arguments);
    while (a.length) {
        b = b.concat(a.shift());
    }
    while (b.length) {
        r *= b.shift();
    }
    return r;
};

das ist nicht so intuitiv wie:

/**
 * An extension to the Math object that accepts Arrays or Numbers
 * as an argument and returns the product of all numbers.
 * @param(Array) a A Number or an Array of numbers.
 * @return(Number) Returns the product of all numbers.
 */
Math.product = function Math_product(a) {
    var product = 1, numbers = [];
    a = argumentsToArray(arguments);
    while (a.length) {
        numbers = numbers.concat(a.shift());
    }
    while (numbers.length) {
        product *= numbers.shift();
    }
    return product;
};
Rolandog
quelle
-1. Dies hat ein wenig mit der ursprünglichen Frage zu tun, um ehrlich zu sein. Die Frage bezieht sich auf eine andere, sehr spezifische Sache, nicht darauf, wie man guten Code im Allgemeinen schreibt.
P Shved
0

Ich erstelle selten separate Variablen. Was ich mache, wenn die Tests kompliziert werden, ist, die IFs zu verschachteln und Kommentare hinzuzufügen. Mögen

boolean processElement=false;
if (elementIndex < 0) // Do we have a valid element?
{
  processElement=true;
}
else if (elementIndex==lastElementIndex) // Is it the one we want?
{
  processElement=true;
}
if (processElement)
...

Der zugegebene Fehler bei dieser Technik ist, dass der nächste Programmierer, der mitkommt, die Logik ändern kann, sich aber nicht die Mühe macht, die Kommentare zu aktualisieren. Ich denke, das ist ein allgemeines Problem, aber ich habe oft einen Kommentar gesehen, in dem "Kunden-ID validieren" steht, und in der nächsten Zeile wird die Teilenummer oder eine solche untersucht, und ich muss mich fragen, wo der Kunde ist ID kommt herein.

Jay
quelle