Sind lange Funktionen akzeptabel, wenn sie eine interne Struktur haben?


23

Wenn ich mich mit komplizierten Algorithmen in Sprachen beschäftige, die verschachtelte Funktionen (wie Python und D) unterstützen, schreibe ich oft riesige Funktionen (weil der Algorithmus kompliziert ist), aber mildere dies, indem ich verschachtelte Funktionen verwende, um den komplizierten Code zu strukturieren. Werden riesige Funktionen (über 100 Zeilen) immer noch als böse angesehen, selbst wenn sie intern durch die Verwendung verschachtelter Funktionen gut strukturiert sind?

Bearbeiten: Für diejenigen unter Ihnen, die nicht mit Python oder D vertraut sind, ermöglichen verschachtelte Funktionen in diesen Sprachen auch den Zugriff auf den äußeren Funktionsumfang. In D ermöglicht dieser Zugriff die Mutation von Variablen im äußeren Bereich. In Python ist nur Lesen möglich. In D können Sie den Zugriff auf den äußeren Bereich in einer verschachtelten Funktion explizit deaktivieren, indem Sie diese deklarieren static.


5
Ich schreibe regelmäßig über 400 Zeilenfunktionen / -methoden. Muss ich irgendwo die 500 case switch-Anweisung setzen :)
Callum Rogers

7
@ Callum Rogers: In Python würden Sie anstelle der Anweisung 500 case switch ein Nachschlagewörterbuch / Mapping verwenden. Ich glaube, sie sind überlegen, wenn es um 500 Fälle geht. Sie benötigen noch 500 Zeilen Wörterbuchdefinitionen (alternativ können Sie in Python Reflection verwenden, um sie dynamisch aufzulisten), aber die Wörterbuchdefinition besteht aus Daten und nicht aus Code. und es gibt viel weniger Bedenken bei der Definition großer Datenmengen als bei der Definition großer Funktionen. Darüber hinaus sind Daten robuster als Code.
Lie Ryan

Ich erschrecke jedes Mal, wenn ich Funktionen mit viel LOC sehe, und frage mich, was der Zweck der Modularität ist. Es ist einfacher, der Logik zu folgen und mit kleineren Funktionen zu debuggen.
Aditya P

Die alte und vertraute Pascal-Sprache ermöglicht auch den Zugriff auf den äußeren Gültigkeitsbereich, ebenso wie die geschachtelte Funktionserweiterung in GNU C. weitergegeben und nicht zurückgegeben, was eine vollständige Unterstützung des lexikalischen Verschlusses erfordern würde).
Kaz

Ein gutes Beispiel für Funktionen, die in der Regel lang sind, sind die Kombinatoren, die Sie beim reaktiven Programmieren schreiben. Sie treffen leicht dreißig Zeilen, würden sich aber verdoppeln, wenn sie getrennt werden, weil Sie Verschlüsse verlieren.
Craig Gidney

Antworten:


21

Denken Sie immer an die Regel, eine Funktion macht eines und macht es gut! Vermeiden Sie in diesem Fall verschachtelte Funktionen.

Es behindert die Lesbarkeit und das Testen.


9
Ich habe diese Regel immer gehasst, weil eine Funktion, die "eins" macht, wenn sie auf einer hohen Abstraktionsebene betrachtet wird, "viele Dinge" macht, wenn sie auf einer niedrigeren Ebene betrachtet wird. Bei falscher Anwendung kann die "one thing" -Regel zu übermäßig feinkörnigen APIs führen.
Dsimcha

1
@dsimcha: Welche Regel kann nicht falsch angewendet werden und zu Problemen führen? Wenn jemand "Regeln" / Plattitüden anwendet, die über den Punkt hinausgehen, an dem sie nützlich sind, bringen Sie einfach eine andere heraus: Alle Verallgemeinerungen sind falsch.

2
@ Roger: Eine berechtigte Beschwerde, mit der Ausnahme, dass die Regel meiner Meinung nach in der Praxis häufig falsch angewendet wird, um übermäßig feinkörnige, überentwickelte APIs zu rechtfertigen. Dies hat konkrete Kosten zur Folge, da es schwieriger und ausführlicher wird, die API für einfache, allgemeine Anwendungsfälle zu verwenden.
Dsimcha

2
"Ja, die Regel wird falsch angewendet, aber die Regel wird zu oft falsch angewendet!" : P

1
Meine Funktion erledigt eine Sache und sie erledigt diese Sache sehr gut: nämlich 27 Bildschirme zu belegen. :)
Kaz

12

Einige haben argumentiert, dass kurze Funktionen fehleranfälliger sein können als lange Funktionen .

Card and Glass (1990) weisen darauf hin, dass die Designkomplexität tatsächlich zwei Aspekte umfasst: die Komplexität innerhalb jeder Komponente und die Komplexität der Beziehungen zwischen Komponenten.

Persönlich habe ich festgestellt, dass gut kommentierter direkter Code leichter zu befolgen ist (insbesondere, wenn Sie nicht derjenige waren, der ihn ursprünglich geschrieben hat), als wenn er in mehrere Funktionen unterteilt ist, die an keiner anderen Stelle verwendet werden. Aber es kommt wirklich auf die Situation an.

Ich denke, das Hauptproblem ist, dass Sie, wenn Sie einen Codeblock aufteilen, eine Art von Komplexität gegen eine andere austauschen. Es gibt wahrscheinlich irgendwo in der Mitte einen Sweet Spot.


Hast du "Clean Code" gelesen? Es wird ausführlich darauf eingegangen. amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/…
Martin Wickman

9

Idealerweise sollte die gesamte Funktion ohne Scrollen sichtbar sein. Manchmal ist das nicht möglich. Wenn Sie es jedoch in Teile zerlegen können, wird das Lesen des Codes erheblich erleichtert.

Ich weiß, dass ich mich nur an 7 +/- 2 Dinge von der vorherigen Seite erinnern kann, sobald ich Page Up / Down drücke oder zu einem anderen Abschnitt des Codes gehe. Und leider werden einige dieser Speicherorte beim Lesen des neuen Codes verwendet.

Ich denke immer gerne an mein Kurzzeitgedächtnis wie an die Register eines Computers (CISC, nicht RISC). Wenn Sie die gesamte Funktion auf derselben Seite haben, können Sie in den Cache wechseln, um die erforderlichen Informationen aus einem anderen Programmabschnitt abzurufen. Wenn die gesamte Funktion nicht auf eine Seite passt, entspricht dies dem Übertragen von Speicher nach jedem Vorgang auf die Festplatte.


3
Sie müssen es nur auf einem Matrixdrucker ausdrucken - sehen Sie, 10 Meter Code auf einmal :)

Oder holen Sie sich einen Monitor mit höherer Auflösung
frogstarr78

Und verwenden Sie es in vertikaler Ausrichtung.
Calmarius

4

Warum verschachtelte Funktionen anstelle normaler externer Funktionen verwenden?

Selbst wenn die externen Funktionen immer nur in Ihrer einzigen, ehemals großen Funktion verwendet werden, ist das Ganze dennoch besser lesbar:

DoThing(int x){
    x += ;1
    int y = FirstThing(x);
    x = SecondThing(x, y);
    while(spoo > fleem){
        x = ThirdThing(x+y);
    }
}

FirstThing(int x){...}
SecondThing(int x, int y){...}
ThirdThing(int z){...}

2
Zwei mögliche Gründe: Unordnung im Namespace und das, was ich als "lexikalische Nähe" bezeichne. Dies können je nach Sprache gute oder schlechte Gründe sein.
Frank Shearar

Geschachtelte Funktionen können jedoch weniger effizient sein, da sie Abwärtsfunktionen und möglicherweise vollständige lexikalische Closures unterstützen müssen. Wenn die Sprache schlecht optimiert ist, muss möglicherweise zusätzliche Arbeit geleistet werden, um eine dynamische Umgebung zu erstellen, die an die untergeordneten Funktionen übergeben wird.
Kaz

3

Ich habe das Buch momentan nicht vor mir (um es zu zitieren), aber laut Code Complete betrug der "Sweetspot" für die Funktionslänge laut seiner Recherche etwa 25-50 Codezeilen.

Es gibt Zeiten, in denen es in Ordnung ist, lange Funktionen zu haben:

  • Wenn die zyklomatische Komplexität der Funktion gering ist. Ihre Mitentwickler könnten ein wenig frustriert sein, wenn sie sich eine Funktion ansehen müssen, die eine riesige if-Anweisung und die else-Anweisung dafür enthält, wenn sie nicht gleichzeitig auf dem Bildschirm angezeigt werden.

Die Zeiten, in denen es nicht in Ordnung ist, lange Funktionen zu haben:

  • Sie haben eine Funktion mit tief verschachtelten Bedingungen. Tun Sie Ihren Mitlesern einen Gefallen, verbessern Sie die Lesbarkeit, indem Sie die Funktion auflösen. Eine Funktion zeigt dem Leser an, dass "dies ein Codeblock ist, der eine Sache tut". Fragen Sie sich auch, ob die Länge der Funktion darauf hinweist, dass sie zu viel bewirkt und in eine andere Klasse zerlegt werden muss.

Unter dem Strich sollte die Wartbarkeit eine der höchsten Prioritäten auf Ihrer Liste sein. Wenn ein anderer Entwickler Ihren Code nicht in weniger als 5 Sekunden einsehen kann, um einen Überblick über die Funktionsweise des Codes zu erhalten, bietet Ihr Code nicht genügend "Metadaten", um die Funktionsweise des Codes zu beschreiben. Andere Entwickler sollten in der Lage sein, zu erkennen, was Ihre Klasse tut, indem sie sich den Objektbrowser in der von Ihnen ausgewählten IDE ansehen, anstatt mehr als 100 Codezeilen zu lesen.

Kleinere Funktionen haben folgende Vorteile:

  • Portabilität: Es ist viel einfacher, Funktionen zu verschieben (entweder innerhalb der Klasse beim Refactoring auf eine andere)
  • Debugging: Wenn Sie sich das Stacktrace ansehen, können Sie einen Fehler viel schneller lokalisieren, wenn Sie eine Funktion mit 25 statt 100 Codezeilen betrachten.
  • Lesbarkeit - Der Name der Funktion gibt an, was ein ganzer Codeblock tut. Ein Entwickler in Ihrem Team möchte diesen Block möglicherweise nicht lesen, wenn er nicht damit arbeitet. In den meisten modernen IDEs kann ein anderer Entwickler besser verstehen, was Ihre Klasse tut, indem er die Funktionsnamen in einem Objektbrowser liest.
  • Navigation - Mit den meisten IDEs können Sie nach Funktionsnamen suchen. Außerdem können die meisten modernen IDEs die Quelle einer Funktion in einem anderen Fenster anzeigen. Dies gibt anderen Entwicklern die Möglichkeit, Ihre Langzeitfunktion auf zwei Bildschirmen (wenn es sich um Multi-Monitore handelt) zu betrachten, anstatt sie blättern zu lassen.

Die Liste geht weiter.....


2

Die Antwort ist, es kommt darauf an, aber Sie sollten das wahrscheinlich in eine Klasse verwandeln.


Es sei denn, Sie schreiben nicht in einer OOPL, in diesem Fall vielleicht nicht :)
Frank Shearar

dsimcha hat erwähnt, dass sie D und Python verwenden.
Frogstarr78

Klassen sind zu oft Krücke für Leute, die noch nie von Dingen wie lexikalischen Schließungen gehört haben.
Kaz

2

Ich mag die meisten verschachtelten Funktionen nicht. Lambdas fallen in diese Kategorie, kennzeichnen mich aber normalerweise nur, wenn sie mehr als 30-40 Zeichen haben.

Der Grund dafür ist, dass es sich um eine lokal sehr dichte Funktion mit interner semantischer Rekursion handelt, was bedeutet, dass es für mich schwierig ist, mein Gehirn in den Griff zu bekommen, und dass es einfacher ist, ein paar Dinge in eine Hilfsfunktion zu verschieben, die den Code-Raum nicht überfrachtet .

Ich denke, dass eine Funktion ihre Sache tun sollte. Andere Dinge tun, ist das, was andere Funktionen tun. Wenn Sie also eine 200-Zeilen-Funktion haben, die das Richtige tut und alles fließt, ist das A-OK.


Manchmal müssen Sie so viel Kontext an eine externe Funktion übergeben (auf die sie als lokale Funktion bequem zugreifen kann), dass der Aufruf der Funktion komplizierter ist als die Ausführung.
Kaz

1

Ist das akzeptabel? Das ist wirklich eine Frage, die nur Sie beantworten können. Erreicht die Funktion das, was sie benötigt? Ist es wartbar? Ist es für die anderen Mitglieder Ihres Teams "akzeptabel"? Wenn ja, dann ist das wirklich wichtig.

Edit: Ich habe die Sache mit den verschachtelten Funktionen nicht gesehen. Persönlich würde ich sie nicht benutzen. Ich würde stattdessen reguläre Funktionen verwenden.


1

Eine lange "Gerade" -Funktion kann eine eindeutige Methode sein, um eine lange Folge von Schritten anzugeben, die immer in einer bestimmten Folge auftreten.

Wie andere erwähnt haben, ist diese Form jedoch anfällig für lokale Komplexitätsabschnitte, in denen der Gesamtfluss weniger offensichtlich ist. Wenn Sie diese lokale Komplexität in eine verschachtelte Funktion einfügen (dh an einer anderen Stelle in der langen Funktion, möglicherweise oben oder unten), kann der Hauptlinienfluss wieder klarer dargestellt werden.

Ein zweiter wichtiger Gesichtspunkt ist die Steuerung des Bereichs von Variablen, die nur in einem lokalen Abschnitt einer langen Funktion verwendet werden sollen. Wachsamkeit ist erforderlich, um zu vermeiden, dass eine Variable, die in einen Codeabschnitt eingefügt wurde, unabsichtlich an einer anderen Stelle referenziert wird (z. B. nach Bearbeitungszyklen), da diese Art von Fehler nicht als Kompilierungs- oder Laufzeitfehler angezeigt wird.

In einigen Sprachen kann dieses Problem leicht abgewendet werden: Ein lokaler Codeabschnitt kann in einen eigenen Block eingeschlossen werden, z. B. mit "{...}", in dem neu eingeführte Variablen nur für diesen Block sichtbar sind. Einige Sprachen wie Python verfügen nicht über diese Funktion. In diesem Fall können lokale Funktionen hilfreich sein, um Bereiche mit kleinerem Gültigkeitsbereich zu erzwingen.


1

Nein, mehrseitige Funktionen sind nicht wünschenswert und sollten keine Codeüberprüfung bestehen. Ich habe auch lange Funktionen geschrieben, aber nachdem ich Martin Fowlers Refactoring gelesen hatte , hörte ich auf. Lange Funktionen sind schwer zu schreiben, schwer zu verstehen und schwer zu testen. Ich habe noch nie eine Funktion mit 50 Zeilen gesehen, die nicht einfacher zu verstehen und zu testen wäre, wenn sie in eine Reihe kleinerer Funktionen aufgeteilt wäre. Bei einer mehrseitigen Funktion sollten mit ziemlicher Sicherheit ganze Klassen herausgerechnet werden. Es ist schwer, genauer zu sein. Vielleicht solltest du eine deiner langen Funktionen bei Code Review posten und jemand (vielleicht ich) kann dir zeigen, wie man sie verbessert.


0

Wenn ich in Python programmiere, mache ich gerne einen Schritt zurück, nachdem ich eine Funktion geschrieben habe, und frage mich, ob sie dem "Zen of Python" entspricht (geben Sie "import this" in Ihren Python-Interpreter ein):

Schön ist besser als hässlich.
Explizit ist besser als implizit.
Einfach ist besser als komplex.
Komplex ist besser als kompliziert.
Flach ist besser als verschachtelt.
Spärlich ist besser als dicht.
Lesbarkeit zählt.
Sonderfälle sind nicht speziell genug, um die Regeln zu brechen.
Obwohl Zweckmäßigkeit die Reinheit übertrifft.
Fehler sollten niemals unbemerkt passieren.
Sofern nicht ausdrücklich zum Schweigen gebracht.
Lehnen Sie bei Zweideutigkeiten die Versuchung ab, zu raten.
Es sollte einen - und am besten nur einen - offensichtlichen Weg geben, dies zu tun.
Obwohl dieser Weg auf den ersten Blick nicht offensichtlich ist, es sei denn, Sie sind Holländer.
Jetzt ist besser als nie.
Obwohl nie oft besser als richtig istjetzt.
Wenn die Implementierung schwer zu erklären ist, ist es eine schlechte Idee.
Wenn die Implementierung einfach zu erklären ist, ist dies möglicherweise eine gute Idee.
Namespaces sind eine großartige Idee - machen wir mehr davon!


1
Wenn explizit besser ist als implizit, sollten wir in Maschinensprache codieren. Nicht einmal im Assembler; Unangenehme implizite Dinge wie das automatische Füllen von Verzweigungsverzögerungsschlitzen mit Anweisungen, das Berechnen relativer Verzweigungsversätze und das Auflösen symbolischer Verweise zwischen Globalen. Mann, diese dummen Python-Benutzer und ihre kleine Religion ...
Kaz

0

Legen Sie sie in ein separates Modul.

Angenommen, Ihre Lösung ist nicht aufgeblähter als die Notwendigkeit, dass Sie nicht zu viele Optionen haben. Sie haben die Funktionen bereits in verschiedene Unterfunktionen aufgeteilt, daher lautet die Frage, wo Sie sie platzieren sollten:

  1. Fügen Sie sie in ein Modul mit nur einer "öffentlichen" Funktion ein.
  2. Ordnen Sie sie einer Klasse mit nur einer "öffentlichen" (statischen) Funktion zu.
  3. Verschachteln Sie sie in einer Funktion (wie Sie beschrieben haben).

Jetzt ist sowohl die zweite als auch die dritte Alternative in gewisser Weise eine Verschachtelung, aber die zweite Alternative scheint für einige Programmierer nicht schlecht zu sein. Wenn Sie die zweite Alternative nicht ausschließen, sehe ich keinen Grund, die dritte auszuschließen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.