Einzeilige Funktionen, die nur einmal aufgerufen werden

120

Stellen Sie sich eine parameterlose ( edit: not required) Funktion vor, die eine einzelne Codezeile ausführt und nur einmal im Programm aufgerufen wird (obwohl es nicht ausgeschlossen ist, dass sie in Zukunft erneut benötigt wird).

Es könnte eine Abfrage durchführen, einige Werte überprüfen, etwas mit Regex tun ... irgendetwas Obskures oder "Hackiges".

Das Grundprinzip dahinter wäre, schwer lesbare Auswertungen zu vermeiden:

if (getCondition()) {
    // do stuff
}

wo getCondition()ist die einzeilige Funktion.

Meine Frage ist einfach: Ist das eine gute Praxis? Es scheint mir in Ordnung zu sein, aber ich weiß nicht über die langfristige ...

vemv
quelle
9
Sofern Ihr Codefragment nicht aus einer OOP-Sprache mit implizitem Empfänger stammt (z. B. dieser), wäre dies sicherlich eine schlechte Praxis, da getCondition () höchstwahrscheinlich auf dem globalen Status beruht ...
Ingo
2
Vielleicht wollte er damit andeuten, dass getCondition () Argumente haben könnte?
user606723
24
@Ingo - einige Dinge haben wirklich einen globalen Status. Aktuelle Uhrzeit, Hostnamen, Portnummern usw. sind gültige "Globals". Der Entwurfsfehler macht ein Global aus etwas, das von Natur aus nicht global ist.
James Anderson
1
Warum gehst du nicht einfach inline getCondition? Wenn es so klein ist und nur selten verwendet wird, wie Sie sagen, bringt es nichts, wenn Sie ihm einen Namen geben.
Davidk01
12
davidk01: Lesbarkeit des Codes.
wjl

Antworten:

240

Kommt auf die eine Zeile an. Wenn die Zeile für sich selbst lesbar und übersichtlich ist, wird die Funktion möglicherweise nicht benötigt. Einfaches Beispiel:

void printNewLine() {
  System.out.println();
}

OTOH, wenn die Funktion einer Codezeile, die z. B. einen komplexen, schwer lesbaren Ausdruck enthält, einen guten Namen gibt, ist dies (für mich) vollkommen gerechtfertigt. Erfundenes Beispiel (hier zur besseren Lesbarkeit in mehrere Zeilen unterteilt):

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}
Péter Török
quelle
99
+1. Das Zauberwort lautet hier "Selbstdokumentierender Code".
Konamiman,
8
Ein gutes Beispiel für das, was Onkel Bob "Einkapseln von Bedingungen" nennen würde.
Anthony Pegram
12
@Aditya: Nichts sagt, dass taxPayerin diesem Szenario global ist. Vielleicht ist TaxReturnund taxPayerist diese Klasse ein Attribut.
Adam Robinson
2
Zusätzlich können Sie die Funktion so dokumentieren, dass sie zB von javadoc aufgegriffen und öffentlich sichtbar ist.
2
@dallin, Bob Martins Clean Code zeigt viele Beispiele für semi-reale Codes. Wenn Sie zu viele Funktionen in einer einzelnen Klasse haben, ist Ihre Klasse möglicherweise zu groß?
Péter Török
67

Ja, dies kann verwendet werden, um Best Practices zu erfüllen. Zum Beispiel ist es besser, eine klar benannte Funktion eine Arbeit machen zu lassen, auch wenn sie nur eine Zeile lang ist, als diese Codezeile in einer größeren Funktion zu haben und einen einzeiligen Kommentar zu benötigen, der erklärt, was sie tut. Außerdem sollten benachbarte Codezeilen Aufgaben auf derselben Abstraktionsebene ausführen. Ein Gegenbeispiel wäre so etwas wie

startIgnition();
petrolFlag |= 0x006A;
engageChoke();

In diesem Fall ist es definitiv besser, die Mittellinie in eine sinnvoll benannte Funktion zu verschieben.

Kilian Foth
quelle
15
Das 0x006A ist herrlich: Eine bekannte Konstante von kohlensäurehaltigem Kraftstoff mit den Zusätzen a, b und c.
Coder
2
+1 Ich bin alles für selbstdokumentierenden Code :) Übrigens, ich glaube, ich bin damit einverstanden, ein konstantes Abstraktionsniveau durch Blöcke zu halten, aber ich konnte nicht erklären, warum. Würde es Ihnen etwas ausmachen, das zu erweitern?
vemv
3
Wenn Sie das nur petrolFlag |= 0x006A;ohne irgendeine Art von Entscheidungsfindung sagen, ist es besser, einfach petrolFlag |= A_B_C; ohne eine zusätzliche Funktion zu sagen . Vermutlich engageChoke()sollte nur aufgerufen werden, wenn petrolFlagein bestimmtes Kriterium erfüllt ist und das klar sagen sollte: "Ich brauche hier eine Funktion." Nur eine Kleinigkeit, diese Antwort ist im Grunde genau richtig :)
Tim Post
9
+1, um darauf hinzuweisen, dass sich der Code innerhalb einer Methode auf derselben Abstraktionsebene befinden sollte. @vemv, es ist eine gute Übung, da der Code dadurch leichter zu verstehen ist, da Sie nicht zwischen verschiedenen Abstraktionsebenen wechseln müssen, während Sie den Code lesen. Das Binden von Abstraktionsstufenschaltern an Methodenaufrufe / -rückgaben (dh strukturelle "Sprünge") ist eine schöne Möglichkeit, den Code flüssiger und sauberer zu gestalten.
Péter Török
21
Nein, es ist ein komplett erfundener Wert. Aber die Tatsache, dass Sie sich darüber wundern, unterstreicht nur den Punkt: Wenn der Code gesagt hätte mixLeadedGasoline(), müssten Sie nicht!
Kilian Foth
37

Ich denke, dass eine solche Funktion in vielen Fällen einen guten Stil hat, aber Sie können eine lokale boolesche Variable als Alternative in Betracht ziehen, wenn Sie diese Bedingung nicht an einer anderen Stelle verwenden müssen, z.

bool someConditionSatisfied = [complex expression];

Dies gibt dem Codeleser einen Hinweis und erspart die Einführung einer neuen Funktion.

cybevnm
quelle
1
Der Bool ist besonders nützlich, wenn der Name der Bedingungsfunktion schwierig oder möglicherweise irreführend ist (z IsEngineReadyUnlessItIsOffOrBusyOrOutOfService. B. ).
Dienstag,
2
Ich empfand diesen Rat als eine schlechte Idee: Der Narr und der Zustand lenken die Aufmerksamkeit vom Kerngeschäft der Funktion ab. Außerdem erschwert ein Stil, der lokale Variablen bevorzugt, das Refactoring.
Wolf
1
@Wolf OTOH, ich ziehe dies einem Funktionsaufruf vor, um die "Abstraktionstiefe" des Codes zu reduzieren. IMO ist das Springen in eine Funktion ein größerer Kontextwechsel als ein explizites und direktes Koppeln von Codezeilen, insbesondere wenn es nur einen Booleschen Wert zurückgibt.
Kache
@Kache Ich denke, es hängt davon ab, ob Sie objektorientiert codieren oder nicht. Im OOP-Fall sorgt die Verwendung der Elementfunktion dafür, dass das Design viel flexibler bleibt. Es kommt wirklich auf den Kontext an ...
Wolf
23

Zusätzlich zu Peters Antwort , wenn diese Bedingung zu einem späteren Zeitpunkt möglicherweise aktualisiert werden muss, können Sie festlegen, dass nur ein einziger Bearbeitungspunkt vorhanden sein soll.

Nach Peters Vorbild, wenn das so ist

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

wird dies

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isMutant() 
        && (taxPayer.getNumberOfThumbs() > 2 
        || (taxPayer.getAge() > 123 && taxPayer.getEmployer().isXMan()));
}

Sie nehmen eine einzelne Bearbeitung vor und sie wird allgemein aktualisiert. In Bezug auf die Wartbarkeit ist dies ein Plus.

In Bezug auf die Leistung werden die meisten optimierenden Compiler den Funktionsaufruf entfernen und den kleinen Codeblock trotzdem einbinden. Wenn Sie so etwas optimieren, kann die Blockgröße tatsächlich verringert werden (indem Sie die für den Funktionsaufruf, die Rückgabe usw. erforderlichen Anweisungen löschen), sodass dies normalerweise auch unter Bedingungen möglich ist, die sonst ein Inlining verhindern könnten.

Stephen
quelle
2
Ich bin mir bereits der Vorteile bewusst, die es mit sich bringt, einen einzigen Bearbeitungspunkt zu behalten - deshalb lautet die Frage "... einmal aufgerufen". Trotzdem ist es großartig, über diese Compiler-Optimierungen Bescheid zu wissen. Ich dachte immer, sie befolgen diese Art von Anweisungen wörtlich.
vemv
Das Aufteilen einer Methode zum Testen einer einfachen Bedingung kann bedeuten, dass eine Bedingung durch Ändern der Methode geändert werden kann. Eine solche Implikation ist nützlich, wenn sie wahr ist, kann aber gefährlich sein, wenn sie nicht wahr ist. Angenommen, der Code muss die Objekte in Lightweight-Objekte, Heavy-Green-Objekte und Heavy-Non-Green-Objekte unterteilen. Objekte haben eine schnelle "seemGreen" -Eigenschaft, die für schwere Objekte zuverlässig ist, bei leichten Objekten jedoch möglicherweise falsche Positive zurückgibt. Sie haben auch eine "measureSpectralResponse" -Funktion, die langsam ist, aber zuverlässig für alle Objekte funktioniert.
Supercat
Die Tatsache, dass seemsGreenLightweight-Objekte unzuverlässig sind, ist irrelevant, wenn sich Code nie darum kümmert, ob Lightweight-Objekte grün sind. Wenn sich die Definition von "Lightweight" jedoch ändert, sodass einige nicht-grüne Objekte, für die true zurückgegeben seemsGreenwird, nicht als "Lightweight" gemeldet werden, kann eine solche Änderung der Definition von "Lightweight" den Code zum Testen von Objekten beschädigen Grün sein". In einigen Fällen wird die Beziehung zwischen den Tests klarer, wenn im Code auf Grün und Gewicht geprüft wird, als wenn es sich um separate Methoden handelt.
Supercat
5

Zusätzlich zur Lesbarkeit (oder in Ergänzung dazu) können Funktionen auf der richtigen Abstraktionsebene geschrieben werden.

tylermac
quelle
1
Ich verstehe
leider
1
@vemv: Ich denke, er meint mit "angemessener Abstraktionsebene", dass Sie nicht mit Code enden, in dem verschiedene Abstraktionsebenen gemischt sind (dh was Kilian Foth bereits gesagt hat). Sie möchten nicht, dass Ihr Code scheinbar unbedeutende Details verarbeitet (z. B. die Bildung aller Bindungen im Kraftstoff), wenn der umgebende Code eine umfassendere Sicht auf die aktuelle Situation (z. B. das Betreiben eines Motors) in Betracht zieht. Ersteres macht letzteres unübersichtlich und macht das Lesen und Pflegen Ihres Codes weniger einfach, da Sie jederzeit alle Abstraktionsebenen gleichzeitig berücksichtigen müssen.
Egon
4

Es hängt davon ab, ob. Manchmal ist es besser, einen Ausdruck in eine Funktion / Methode zu kapseln, auch wenn es sich nur um eine Zeile handelt. Wenn das Lesen kompliziert ist oder Sie es an mehreren Stellen benötigen, halte ich es für eine gute Übung. Auf lange Sicht ist es einfacher zu warten, da Sie einen einzigen Änderungspunkt und eine bessere Lesbarkeit eingeführt haben .

Manchmal ist es jedoch nur etwas, was Sie nicht brauchen. Wenn der Ausdruck ohnehin leicht zu lesen ist und / oder nur an einer Stelle erscheint, dann wickeln Sie ihn nicht um.

Falke
quelle
3

Ich denke, wenn Sie nur ein paar davon haben, ist es in Ordnung, aber das Problem tritt auf, wenn viele davon in Ihrem Code enthalten sind. Und wenn der Compiler ausgeführt wird oder der Interpitor (abhängig von der verwendeten Sprache), wird diese Funktion im Speicher abgelegt. Nehmen wir also an, Sie haben 3 davon. Ich glaube nicht, dass der Computer dies bemerkt. Wenn Sie jedoch anfangen, 100 dieser kleinen Dinge zu haben, muss das System Funktionen im Speicher registrieren, die nur einmal aufgerufen und dann nicht zerstört werden.

WojonsTech
quelle
Laut Stephens Antwort könnte dies nicht immer der Fall sein (obwohl es ohnehin nicht gut sein kann, sich blind auf die Magie der Compiler zu verlassen)
vemv
1
Ja, es sollte in Abhängigkeit von vielen Fakten beseitigt werden. Wenn es sich um eine interpatierte Sprache handelt, kann das System jedes Mal auf die Funktionen für einzelne Zeilen zurückgreifen, es sei denn, Sie installieren etwas für den Cache, das den Trick möglicherweise immer noch nicht ausführt. Wenn es um Compiler geht, ist es nur am Tag der Schwachen und der Platzierung der Planeten wichtig, ob der Compiler dein Freund ist und dein kleines Durcheinander ausräumt oder ob du denkst, dass du es wirklich brauchst. Ich erinnere mich, dass, wenn Sie genau wissen, wie oft eine Schleife ausgeführt wird, es manchmal besser ist, sie nur so oft zu kopieren und einzufügen, wie es in der Schleife der Fall ist.
WojonsTech
+1 für die Ausrichtung der Planeten :) aber dein letzter Satz klingt für mich total verrückt, tust du das wirklich?
Vemv
Es hängt wirklich die meiste Zeit davon ab, nein, ich tue es nicht, es sei denn, ich erhalte den richtigen Zahlungsbetrag, um zu überprüfen, ob es Geschwindigkeitszuwächse und andere Dinge wie diese gibt. Bei älteren Compilern war es jedoch besser, sie zu kopieren und einzufügen, als das Compilere zu verlassen, um 9/10 herauszufinden.
WojonsTech
3

Genau das habe ich kürzlich in einer Anwendung getan, die ich überarbeitet habe, um die tatsächliche Bedeutung des Codes ohne Kommentare zu verdeutlichen:

protected void PaymentButton_Click(object sender, EventArgs e)
    Func<bool> HaveError = () => lblCreditCardError.Text == string.Empty && lblDisclaimer.Text == string.Empty;

    CheckInputs();

    if(HaveError())
        return;

    ...
}
joshperry
quelle
Nur neugierig, welche Sprache ist das?
12.
5
@vemv: Sieht aus wie C # für mich.
Scott Mitchell
Ich bevorzuge auch zusätzliche Bezeichner gegenüber Kommentaren. Aber unterscheidet sich dies wirklich von der Einführung einer lokalen Variablen , um die ifKurzform zu halten ? Dieser lokale Ansatz (Lambda) erschwert das PaymentButton_ClickLesen der Funktion (insgesamt). Das lblCreditCardErrorin Ihrem Beispiel scheint ein Mitglied zu sein, also HaveErrorist es auch ein (privates) Prädikat, das für das Objekt gültig ist. Ich würde dies eher ablehnen, aber ich bin kein C # -Programmierer, also widersetze ich mich.
Wolf
@ Wolf Hey Mann, ja. Ich habe das vor einiger Zeit geschrieben :) Ich mache die Dinge jetzt definitiv ganz anders. Wenn ich mir den Inhalt des Etiketts ansehe, um festzustellen, ob ein Fehler aufgetreten ist, bin ich erschrocken ... Warum habe ich nicht einfach einen Bool von zurückgegeben CheckInputs()???
Joshperry
0

Wenn Sie diese eine Zeile in eine gut benannte Methode verschieben, wird der Code leichter lesbar. Viele andere haben dies bereits erwähnt ("selbstdokumentierender Code"). Der andere Vorteil beim Übertragen in eine Methode besteht darin, dass es einfacher ist, einen Komponententest durchzuführen. Wenn es in einer eigenen Methode isoliert und in einer Einheit getestet ist, können Sie sicher sein, dass ein gefundener Fehler nicht in dieser Methode enthalten ist.

Andrew
quelle
0

Es gibt bereits viele gute Antworten, aber es gibt einen erwähnenswerten Sonderfall .

Wenn Ihre einzeilige Anweisung einen Kommentar benötigt und Sie in der Lage sind, den Zweck klar zu identifizieren (was bedeutet: Name), sollten Sie eine Funktion extrahieren und den Kommentar in das API-Dokument einbinden. Auf diese Weise machen Sie den Funktionsaufruf einfacher, schneller und verständlicher.

Interessanterweise kann das Gleiche getan werden, wenn derzeit nichts zu tun ist, sondern ein Kommentar, der an notwendige Erweiterungen erinnert (in naher Zukunft 1) .

def sophisticatedHello():
    # todo set up
    say("hello")
    # todo tear down

könnte ebenso gut geändert werden

def sophisticatedHello():
    setUp()
    say("hello")
    tearDown()

1) Sie sollten sich wirklich sicher sein (siehe YAGNI- Prinzip)

Wolf
quelle
0

Wenn die Sprache dies unterstützt, verwende ich normalerweise gekennzeichnete anonyme Funktionen, um dies zu erreichen.

someCondition = lambda p: True if [complicated expression involving p] else False
#I explicitly write the function with a ternary to make it clear this is a a predicate
if (someCondition(p)):
    #do stuff...

IMHO ist dies ein guter Kompromiss, da Sie den Vorteil der Lesbarkeit haben, dass der komplizierte Ausdruck die ifBedingung nicht überfrachtet , während der globale Namespace / Package-Namespace nicht mit kleinen Wegwerfetiketten überfrachtet wird. Es hat den zusätzlichen Vorteil, dass die Funktion "Definition" genau dort eingesetzt wird, wo sie verwendet wird, wodurch es einfach ist, die Definition zu ändern und zu lesen.

Es müssen nicht nur Prädikatfunktionen sein. Ich mag es auch, wiederholte Boiler-Platten in kleine Funktionen wie diese einzuschließen. Das folgende Beispiel ist zu stark vereinfacht, wenn Sie mit PIL in Python arbeiten

#goal - I have a list of PIL Image objects and I want them all as grayscale (uint8) numpy arrays
im_2_arr = lambda im: array(im.convert('L')) 
arr_list = [im_2_arr(image) for image in image_list]
crasic
quelle
Warum "Lambda p: Richtig, wenn [komplizierter Ausdruck mit p] else False" anstelle von "Lambda p: [komplizierter Ausdruck mit p]"? 8-)
Hans-Peter Störr
@hstoerr its im Kommentar direkt unter dieser Zeile. Ich möchte ausdrücklich darauf hinweisen, dass wir someCondition ein Prädikat sind. Obwohl es absolut unnötig ist, schreibe ich eine Menge wissenschaftlicher Skripte, die von Leuten gelesen werden, die nicht so viel programmieren. Ich persönlich halte es für angemessener, die zusätzliche Knappheit zu haben, als meine Kollegen zu verwirren, weil sie das [] == Falseoder etwas anderes nicht wissen Eine ähnliche pythonische Äquivalenz, die nicht immer intuitiv ist. Dies ist im Grunde eine Möglichkeit, zu kennzeichnen, dass someCondition tatsächlich ein Prädikat ist.
Crasic
Nur meinen offensichtlichen Fehler zu löschen , [] != Falsesondern []ist False als wenn sie an einen Bool werfen
crasic
@crasic: Wenn ich nicht erwarte, dass meine Leser wissen, dass [] Falsch ergibt, bevorzuge ich die len([complicated expression producing a list]) == 0Verwendung, bei True if [blah] else Falseder der Leser weiterhin wissen muss, dass [] Falsch ergibt.
Lie Ryan