it-swarm.com.de

Ist es eine gute Praxis, eine Variable zu definieren, um ein Methodenargument zu benennen?

Aus Gründen der Lesbarkeit definiere ich häufig temporäre Variablen, während ich Funktionen wie den folgenden Code aufrufe

var preventUndo = true;
doSomething(preventUndo);

Die kürzere Version davon wäre,

doSomething(true);

Aber wenn ich zum Code zurückkomme, frage ich mich oft, worauf sich true bezieht. Gibt es eine Konvention für diese Art von Rätsel?

76
methodofaction

Variablen erklären

Ihr Fall ist ein Beispiel für das Einführung der erklärenden Variablen/Extraktvariablen Refactoring. Kurz gesagt, eine erklärende Variable ist eine, die nicht unbedingt erforderlich ist, aber es Ihnen ermöglicht, etwas einen klaren Namen zu geben, um die Lesbarkeit zu verbessern.

Guter Qualitätscode kommuniziert Absicht mit dem Leser; Als professioneller Entwickler sind Lesbarkeit und Wartbarkeit Ihre obersten Ziele.

Als Faustregel würde ich folgende empfehlen: Wenn der Zweck Ihres Parameters nicht sofort offensichtlich ist, können Sie eine Variable verwenden, um ihm einen guten Namen zu geben. Ich denke, dies ist eine gute Praxis im Allgemeinen (sofern nicht missbraucht). Hier ist ein kurzes, erfundenes Beispiel - bedenken Sie:

editButton.Enabled = (_grid.SelectedRow != null && ((Person)_grid.SelectedRow).Status == PersonStatus.Active);

im Vergleich zu den etwas längeren, aber wohl klarer:

bool personIsSelected = (_grid.SelectedRow != null);
bool selectedPersonIsEditable = (personIsSelected && ((Person)_grid.SelectedRow).Status == PersonStatus.Active)
editButton.Enabled = (personIsSelected && selectedPersonIsEditable);

Boolesche Parameter

Ihr Beispiel zeigt tatsächlich, warum Boolesche Werte in APIs sind oft eine schlechte Idee - auf der aufrufenden Seite erklären sie nichts, was passiert. Erwägen:

ParseFolder(true, false);

Sie müssten nachschlagen, was diese Parameter bedeuten. Wenn sie Aufzählungen wären, wäre es viel klarer:

ParseFolder(ParseBehaviour.Recursive, CompatibilityOption.Strict);

Bearbeiten:

Überschriften hinzugefügt und die Reihenfolge der beiden Hauptabsätze vertauscht, da sich zu viele Leute auf den Teil mit den booleschen Parametern konzentrierten (um fair zu sein, war der erste Absatz ursprünglich). Außerdem wurde dem ersten Teil ein Beispiel hinzugefügt.

119
Daniel B

Schreiben Sie keinen Code, den Sie nicht benötigen.

Wenn Sie doSomething(true) schwer zu verstehen finden, sollten Sie entweder einen Kommentar hinzufügen:

// Do something and prevent the undo.
doSomething(true);

oder, wenn die Sprache dies unterstützt, fügen Sie den Parameternamen hinzu:

doSomething(preventUndo: true);

Andernfalls verlassen Sie sich auf Ihre IDE, um die Signatur der aufgerufenen Methode zu erhalten:

enter image description here

Fälle, in denen es nützlich ist

Das Einfügen einer zusätzlichen Variablen kann nützlich sein:

  1. Für Debugging-Zwecke:

    var productId = this.Data.GetLastProductId();
    this.Data.AddToCart(productId);
    

    Der gleiche Code kann in eine einzelne Zeile geschrieben werden. Wenn Sie jedoch vor dem Hinzufügen eines Produkts zum Warenkorb einen Haltepunkt setzen möchten, um festzustellen, ob die Produkt-ID korrekt ist, sollten Sie zwei statt einer Zeile schreiben.

  2. Wenn Sie eine Methode mit vielen Parametern haben und jeder Parameter aus einer Auswertung stammt. In einer Zeile kann dies völlig unlesbar werden.

    // Even with indentation, this is unreadable.
    var doSomething(
        isSomethingElse ? 0 : this.getAValue(),
        this.getAnotherOne() ?? this.default,
        (a + b + c + d + e) * f,
        this.hello ? this.world : (this.hello2 ? this.world2 : -1));
    
  3. Wenn die Auswertung eines Parameters zu kompliziert ist. Ein Beispiel für einen Code, den ich gesehen habe:

    // Wouldn't it be easier to have several if/else's (maybe even in a separate method)?
    do(something ? (hello ? world : -1) : (programmers ? stackexchange : (com ? -1 : 0)));
    

Warum nicht in anderen Fällen?

Warum sollten Sie in einfachen Fällen keine zusätzlichen Variablen erstellen?

  1. Nicht wegen der Auswirkungen auf die Leistung. Dies wäre eine sehr falsche Annahme eines Anfängers, der Mikrooptimierung seine App ist. Es gibt keine Auswirkungen auf die Leistung in den meisten Sprachen, da der Compiler die Variable inline. In den Sprachen, in denen der Compiler dies nicht tut, können Sie einige Mikrosekunden gewinnen, indem Sie ihn von Hand einfügen, was sich nicht lohnt. Tu das nicht.

  2. Wegen des Risikos, den Namen, den Sie der Variablen geben, vom Namen des Parameters zu trennen.

    Beispiel:

    Angenommen, der ursprüngliche Code lautet:

    void doSomething(bool preventUndo)
    {
        // Does something very interesting.
        undoHistory.removeLast();
    }
    
    // Later in code:
    var preventUndo = true;
    doSomething(preventUndo);
    

    Später bemerkt ein Entwickler, der an doSomething arbeitet, dass der Rückgängig-Verlauf kürzlich zwei neue Methoden eingeführt hat:

    undoHistory.clearAll() { ... }
    undoHistory.disable() { ... }
    

    Nun scheint preventUndo nicht sehr klar zu sein. Bedeutet das, dass nur die letzte Aktion verhindert wird? Oder bedeutet dies möglicherweise, dass der Benutzer die Rückgängig-Funktion nicht mehr verwenden kann? Oder dass der Rückgängig-Verlauf gelöscht wird? Die klareren Namen wären:

    doSomething(bool hideLastUndo) { ... }
    doSomething(bool removeAllUndo) { ... }
    doSomething(bool disableUndoFeature) { ... }
    

    Jetzt haben Sie also:

    void doSomething(bool hideLastUndo)
    {
        // Does something very interesting.
        undoHistory.removeLast();
    }
    
    // Later in code, very error prone, while the signature of the method is clear:
    var preventUndo = true;
    doSomething(preventUndo);
    

Was ist mit Aufzählungen?

Einige andere Leute schlugen vor, Aufzählungen zu verwenden. Während es das unmittelbare Problem löst, schafft es ein größeres. Lass uns mal sehen:

enum undoPrevention
{
    keepInHistory,
    prevent,
}

void doSomething(undoPrevention preventUndo)
{
    doTheJob();
    if (preventUndo == undoPrevention.prevent)
    {
        this.undoHistory.discardLastEntry();
    }
}

doSomething(undoPrevention.prevent);

Probleme damit:

  1. Es ist zu viel Code. if (preventUndo == undoPrevention.prevent)? Ernsthaft?! Ich möchte nicht jedes Mal solche ifs schreiben.

  2. Das Hinzufügen eines Elements zur Aufzählung ist später sehr verlockend, wenn ich dieselbe Aufzählung woanders verwende. Was ist, wenn ich es so ändere:

    enum undoPrevention
    {
        keepInHistory,
        prevent,
        keepButDisable, // Keeps the entry in the history, but makes it disabled.
    }
    

    Was wird jetzt passieren? Funktioniert die Methode doSomething wie erwartet? Um dies zu verhindern, müsste diese Methode von Anfang an folgendermaßen geschrieben werden:

    void doSomething(undoPrevention preventUndo)
    {
        if (![undoPrevention.keepInHistory, undoPrevention.prevent].contains(preventUndo))
        {
            throw new ArgumentException('preventUndo');
        }
    
        doTheJob();
        if (preventUndo == undoPrevention.prevent)
        {
            this.undoHistory.discardLastEntry();
        }
    }
    

    Die Variante, die Boolesche Werte verwendet, sieht so schön aus!

    void doSomething(bool preventUndo)
    {
        doTheJob();
        if (preventUndo)
        {
            this.undoHistory.discardLastEntry();
        }
    }
    
42

Sie sollten auch keine Methoden mit booleschen Flags schreiben.

Um Robert C. Martin zu zitieren, sagt er in seinem Buch Clean Code (ISBN-13 978-0-13-235088-4): "Es ist eine wirklich schreckliche Praxis, einen Booleschen Wert in eine Funktion zu übertragen."

Um ihn zu paraphrasieren: Die Tatsache, dass Sie einen True/False-Schalter haben, bedeutet, dass Ihre Methode höchstwahrscheinlich zwei verschiedene Dinge tut (dh "Mach etwas mit Rückgängig" und "Mach etwas ohne Rückgängig") und sollte es daher sein aufgeteilt in zwei verschiedene Methoden (die intern dasselbe aufrufen können).

DoSomething()
{...

DoSomethingUndoable()
{....
20
Nick B.

Wenn Sie sich zwei Klassen ansehen, um zu sehen, wie gekoppelt sie sind, ist eine der Kategorien Datenkopplung , die sich auf Code bezieht In einer Klasse werden Methoden einer anderen Klasse aufgerufen und nur Daten übergeben, z. B. für welchen Monat Sie einen Bericht wünschen. In einer anderen Kategorie werden Steuerkopplungen aufgerufen und etwas übergeben, das das Verhalten der Methode steuert. Das Beispiel, das ich in der Klasse verwende, ist ein verbose Flag oder ein reportType Flag, aber preventUndo ist auch ein großartiges Beispiel. Wie Sie gerade gezeigt haben, macht es die Steuerkopplung schwierig, den aufrufenden Code zu lesen und zu verstehen, was passiert. Es macht den aufrufenden Code auch anfällig für Änderungen in doSomething(), die dasselbe Flag verwenden, um sowohl das Rückgängigmachen als auch das Archivieren zu steuern, oder fügen Sie doSomething() einen weiteren Parameter hinzu, um die Archivierung zu steuern, wodurch Ihr Code beschädigt wird. und so weiter.

Das Problem ist, dass der Code zu eng gekoppelt ist. Das Übergeben eines Bools zur Kontrolle des Verhaltens ist meiner Meinung nach ein Zeichen für eine schlechte API. Wenn Sie die API besitzen, ändern Sie sie. Zwei Methoden, doSomething() und doSomethingWithUndo(), wären besser. Wenn Sie es nicht besitzen, schreiben Sie zwei Wrapper-Methoden selbst in den Code, den Sie besitzen, und lassen Sie eine davon doSomething(true) und die andere doSomething(false) aufrufen.

5
Kate Gregory

Es ist nicht die Rolle des Aufrufers, die Rolle von Argumenten zu definieren. Ich greife immer zur Inline-Version und überprüfe im Zweifelsfall die Signatur der aufgerufenen Funktion.

Die Variable sollte nach ihrer Rolle im aktuellen Bereich benannt werden. Nicht der Umfang, in den sie gesendet werden.

4
Simon Bergot

Was ich in JavaScript tun würde, ist, dass die Funktion ein Objekt als einzigen Parameter annimmt:

function doSomething(settings) {
    var preventUndo = settings.hasOwnProperty('preventUndo') ? settings.preventUndo : false;
    // Deal with preventUndo in the normal way.
}

Dann nenne es mit:

doSomething({preventUndo: true});
4
ridecar2

Ich nutze gerne Sprachfunktionen, um mich selbst klarer zu machen. Zum Beispiel können Sie in C # Parameter nach Namen angeben:

 CallSomething(name: "So and so", age: 12, description: "A random person.");

In JavaScript bevorzuge ich normalerweise ein Optionsobjekt als Argument aus diesem Grund:

function doSomething(args) { /*...*/ }

doSomething({ name: 'So and so', age: 12, description: 'A random person.' });

Das ist aber nur meine Präferenz. Ich nehme an, es wird stark von der aufgerufenen Methode abhängen und davon, wie IDE dem Entwickler hilft, die Signatur zu verstehen.

4
blesh

Ich finde das am einfachsten und am einfachsten zu lesen:

enum Undo { ALLOW, PREVENT }

doSomething(Undo u) {
    if (Undo.ALLOW == u) {
        // save stuff to undo later.
    }
    // do your thing
}

doSomething(Undo.ALLOW);

MainMa gefällt das nicht. Möglicherweise müssen wir zustimmen, nicht zuzustimmen, oder wir können eine Lösung verwenden, die Joshua Bloch vorschlägt:

enum Undo {
    ALLOW {
        @Override
        public void createUndoBuffer() {
            // put undo code here
        }
    },
    PREVENT {
        @Override
        public void createUndoBuffer() {
            // do nothing
        }
    };

    public abstract void createUndoBuffer();
}

doSomething(Undo u) {
    u.createUndoBuffer();
    // do your thing
}

Wenn Sie jetzt jemals eine Undo.LIMITED_UNDO hinzufügen, wird Ihr Code nur kompiliert, wenn Sie die Methode createUndoBuffer () implementieren. Und in doSomething () gibt es kein if (Undo.ALLOW == u). Ich habe es in beide Richtungen gemacht und die zweite Methode ist ziemlich schwer und ein wenig schwer zu verstehen, wenn die Rückgängig-Aufzählung auf Seiten und Seiten mit Code erweitert wird, aber es bringt Sie zum Nachdenken. Normalerweise bleibe ich bei der einfacheren Methode, einen einfachen Booleschen Wert durch eine 2-Wert-Aufzählung zu ersetzen, bis ich Grund zur Änderung habe. Wenn ich einen dritten Wert hinzufüge, verwende ich meine IDE zu "Find-usages" und repariere dann alles.

3
GlenPeterson

Da Sie hauptsächlich nach Javascript fragen, können Sie mit Coffeescript ein benanntes Argument wie die Syntax haben, ganz einfach:

# declare method, ={} declares a default value to prevent null reference errors
doSomething = ({preventUndo} = {}) ->
  if preventUndo
    undo = no
  else
    undo = yes

#call method
doSomething preventUndo: yes
#or 
doSomething(preventUndo: yes)

kompiliert zu

var doSomething;

doSomething = function(_arg) {
  var preventUndo, undo;
  preventUndo = (_arg != null ? _arg : {}).preventUndo;
  if (preventUndo) {
    return undo = false;
  } else {
    return undo = true;
  }
};

doSomething({
  preventUndo: true
});

doSomething({
  preventUndo: true
});

Viel Spaß unter http://js2coffee.org/ , um die Möglichkeiten auszuprobieren

2
Guillaume86

Ich denke, Ihre Lösung macht Ihren Code ein bisschen lesbarer, aber ich würde es vermeiden, eine zusätzliche Variable zu definieren, nur um Ihren Funktionsaufruf klarer zu machen. Wenn Sie eine zusätzliche Variable verwenden möchten, würde ich sie als konstant markieren, wenn Ihre Programmiersprache dies unterstützt.

Ich kann mir zwei Alternativen vorstellen, die keine zusätzliche Variable beinhalten:

1. Verwenden Sie einen zusätzlichen Kommentar

doSomething(/* preventUndo */ true);

2. Verwenden Sie eine zweiwertige Aufzählung anstelle eines Booleschen Wertes

enum Options
{
    PreventUndo,
    ...
}

...

doSomething(PreventUndo);

Sie können Alternative 2 verwenden, wenn Ihre Sprache Aufzählungen unterstützt.

EDIT

Die Verwendung benannter Argumente ist natürlich auch eine Option, wenn Ihre Sprache sie unterstützt.

In Bezug auf die zusätzliche Codierung, die für Enums benötigt wird, scheint es mir wirklich vernachlässigbar. Anstatt

if (preventUndo)
{
    ...
}

du hast

if (undoOption == PreventUndo)
{
    ...
}

oder

switch (undoOption)
{
  case PreventUndo:
  ...
}

Und selbst wenn es etwas mehr tippt, denken Sie daran, dass Code einmal geschrieben und oft gelesen wird. Es kann sich also lohnen, jetzt ein bisschen mehr zu tippen, um sechs Monate später einen besser lesbaren Code zu finden.

2
Giorgio

Ich stimme dem zu, was @GlenPeterson gesagt hat:

doSomething(Undo.ALLOW); // call using a self evident enum

aber auch insteresting wäre das Folgende, da es mir scheint, dass es nur zwei Möglichkeiten gibt (wahr oder falsch)

//Two methods    

doSomething()  // this method does something but doesn't prevent undo

doSomethingPreventUndo() // this method does something and prevents undo
2

Der Fokus Ihrer Frage und der anderen Antworten liegt auf der Verbesserung der Lesbarkeit, wenn die Funktion aufgerufen wird. Diesem Fokus stimme ich zu. Alle spezifischen Richtlinien, Ereignis "keine booleschen Argumente", sollten immer als Mittel zu diesem Zweck fungieren und sich nicht selbst werden und beenden.

Ich denke, es ist nützlich zu beachten, dass dieses Problem meistens abgewendet wird, wenn die Programmiersprache benannte Argumente/Schlüsselwortargumente unterstützt, wie in C # 4 und Python, oder wenn die Methodenargumente im Methodennamen verschachtelt sind, wie in Smalltalk oder Objective-C.

Beispiele:

// C# 4
foo.doSomething(preventUndo: true);
# Python
foo.doSomething(preventUndo=True)
// Objective-C
[foo doSomethingWith:bar shouldPreventUndo:YES];
2
orip

Nur für eine weniger konventionelle Lösung und da das OP Javascript erwähnt, besteht eine Möglichkeit darin, ein assoziatives Array zum Zuordnen von Werten zu verwenden. Der Vorteil gegenüber einem einzelnen Booleschen Wert besteht darin, dass Sie so viele Argumente haben können, wie Sie möchten, und sich keine Sorgen über deren Reihenfolge machen müssen.

var doSomethingOptions = new Array();

doSomethingOptions["PREVENTUNDO"] = true;
doSomethingOptions["TESTMODE"] = false;

doSomething( doSomethingOptions );

// ...

function doSomething( doSomethingOptions ){
    // Check for null here
    if( doSomethingOptions["PREVENTUNDO"] ) // ...
}

Mir ist klar, dass dies ein Reflex von jemandem mit einem stark typisierten Hintergrund ist und vielleicht nicht so praktisch ist, aber ich halte dies für Originalität.

Eine andere Möglichkeit besteht darin, ein Objekt zu haben, und dies ist auch in Javascript möglich. Ich bin mir nicht 100% sicher, ob dies syntaktisch korrekt ist. Schauen Sie sich Muster wie Factory an, um weitere Inspirationen zu erhalten.

function SomethingDoer( preventUndo ) {
    this.preventUndo = preventUndo;
    this.doSomething = function() {
            if( this.preventUndo ){
                    // ...
            }
    };
}

mySomethingDoer = new SomethingDoer(true).doSomething();
1
James P.

Man sollte vermeiden, boolesche Variablen als Argumente an Funktionen zu übergeben. Weil Funktionen jeweils eine Aufgabe ausführen sollten. Durch die Übergabe einer booleschen Variablen hat die Funktion zwei Verhaltensweisen. Dies führt auch zu Lesbarkeitsproblemen für einen späteren Zeitpunkt oder für andere Programmierer, die dazu neigen, Ihren Code zu sehen. Dies führt auch zu Problemen beim Testen der Funktion. Wahrscheinlich müssen Sie in diesem Fall zwei Testfälle erstellen. Stellen Sie sich vor, Sie haben so viele verschiedene Testfälle definiert, wenn Sie eine Funktion haben, die eine switch-Anweisung hat und deren Verhalten basierend auf dem switch-Typ ändert.

Immer wenn jemand auf ein boolesches Argument für die Funktion stößt, muss er den Code so anpassen, dass er überhaupt kein boolesches Argument schreibt.

0
Ganji