Soll ich bestimmte Funktionen in eine Funktion extrahieren und warum?

29

Ich habe eine große Methode, die 3 Aufgaben erledigt, von denen jede in eine separate Funktion extrahiert werden kann. Wenn ich für jede dieser Aufgaben zusätzliche Funktionen erstelle, verbessert oder verschlechtert dies meinen Code und warum?

Offensichtlich werden weniger Codezeilen in der Hauptfunktion erstellt, aber es werden zusätzliche Funktionsdeklarationen vorhanden sein, sodass meine Klasse zusätzliche Methoden hat, von denen ich glaube, dass sie nicht gut sind, weil sie die Klasse komplexer machen.

Sollte ich das tun, bevor ich den gesamten Code geschrieben habe, oder sollte ich es so lange belassen, bis alles erledigt ist, und dann Funktionen extrahieren?

Dhblah
quelle
19
"Ich lasse es, bis alles erledigt ist" ist normalerweise gleichbedeutend mit "Es wird niemals erledigt sein".
Euphorischer
2
Das ist im Allgemeinen richtig, aber denken Sie auch an das entgegengesetzte Prinzip von YAGNI (das in diesem Fall nicht gilt, da Sie es bereits benötigen).
jhocking
Ich wollte nur betonen, dass ich mich nicht so sehr auf die Reduzierung von Codezeilen konzentriere. Versuchen Sie stattdessen, in Abstraktionen zu denken. Jede Funktion sollte nur einen Job haben. Wenn Sie feststellen, dass Ihre Funktionen mehr als eine Aufgabe ausführen, sollten Sie die Methode im Allgemeinen überarbeiten. Wenn Sie diese Richtlinien befolgen, sollte es fast unmöglich sein, übermäßig lange Funktionen zu haben.
Adrian

Antworten:

35

Dies ist ein Buch, auf das ich oft verweise, aber ich gehe noch einmal: Robert C. Martins Clean Code , Kapitel 3, "Funktionen".

Offensichtlich werden weniger Codezeilen in der Hauptfunktion erstellt, aber es werden zusätzliche Funktionsdeklarationen vorhanden sein, sodass meine Klasse zusätzliche Methoden hat, von denen ich glaube, dass sie nicht gut sind, weil sie die Klasse komplexer machen.

Lesen Sie lieber eine Funktion mit +150 Zeilen oder eine Funktion, die 3 +50 Zeilenfunktionen aufruft? Ich denke, ich bevorzuge die zweite Option.

Ja , es verbessert Ihren Code in dem Sinne, dass er "lesbarer" wird. Machen Sie Funktionen, die eine und nur eine Sache ausführen, werden sie leichter zu warten und einen Testfall zu erstellen.

Auch eine sehr wichtige Sache, die ich mit dem oben genannten Buch gelernt habe: Wählen Sie gute und genaue Namen für Ihre Funktionen. Je wichtiger die Funktion ist, desto genauer sollte der Name sein. Machen Sie sich keine Sorgen über die Länge des Namens. Wenn er aufgerufen werden muss FunctionThatDoesThisOneParticularThingOnly, benennen Sie ihn so.

Schreiben Sie einen oder mehrere Testfälle, bevor Sie Ihren Refactor durchführen. Stellen Sie sicher, dass sie funktionieren. Sobald Sie mit Ihrem Refactoring fertig sind, können Sie diese Testfälle starten, um sicherzustellen, dass der neue Code ordnungsgemäß funktioniert. Sie können zusätzliche "kleinere" Tests schreiben, um sicherzustellen, dass Ihre neuen Funktionen gut voneinander getrennt sind.

Und schließlich, und dies steht nicht im Widerspruch zu dem, was ich gerade geschrieben habe, fragen Sie sich, ob Sie dieses Refactoring wirklich durchführen müssen, und lesen Sie die Antworten unter " Wann ist ein Refactoring durchzuführen ?". (Suchen Sie auch nach SO Fragen zum Thema "Refactoring", es gibt mehr und die Antworten sind interessant zu lesen)

Sollte ich das tun, bevor ich den gesamten Code schreibe, oder sollte ich es so lange belassen, bis alles erledigt ist, und dann Funktionen extrahieren?

Wenn der Code bereits vorhanden ist und funktioniert und die Zeit für die nächste Version knapp ist, berühren Sie ihn nicht. Ansonsten denke ich, sollte man kleine Funktionen machen, wann immer möglich und als solche immer dann umgestalten, wenn etwas Zeit zur Verfügung steht, während sichergestellt wird, dass alles wie zuvor funktioniert (Testfälle).

Jalayn
quelle
10
Tatsächlich hat Bob Martin mehrfach gezeigt, dass er 7 Funktionen mit 2 bis 3 Zeilen gegenüber einer Funktion mit 15 Zeilen bevorzugt (siehe sites.google.com/site/unclebobconsultingllc/… ). Und da werden viele erfahrene Entwickler Widerstand leisten. Persönlich denke ich, dass viele dieser "erfahrenen Entwickler" Probleme damit haben, zu akzeptieren, dass sie eine grundlegende Sache wie das Erstellen von Abstraktionen mit Funktionen nach> 10 Jahren Codierung noch verbessern können.
Doc Brown
+1 nur um auf ein Buch zu verweisen, das meiner bescheidenen Meinung nach in den Regalen eines Softwareunternehmens stehen sollte.
Fabio Marcolini
3
Ich könnte hier paraphrasieren, aber ein Satz aus diesem Buch, der fast jeden Tag in meinem Kopf widerhallt, lautet: "Jede Funktion sollte nur eines tun und es gut machen". Scheint hier besonders relevant, da das OP sagte "Meine Hauptfunktion macht drei Dinge"
Wakjah
Du hast absolut recht!
Jalayn,
Kommt darauf an, wie sehr die drei Funktionen miteinander verflochten sind. Es ist möglicherweise einfacher, einem Codeblock zu folgen, der sich an einer Stelle befindet, als drei Codeblöcke, die wiederholt aufeinander angewiesen sind.
user253751
13

Ja offensichtlich. Wenn es einfach ist, die verschiedenen "Aufgaben" einer einzelnen Funktion zu erkennen und zu trennen.

  1. Lesbarkeit - Funktionen mit guten Namen machen deutlich, was Code tut, ohne dass dieser Code gelesen werden muss.
  2. Wiederverwendbarkeit - Es ist einfacher, Funktionen zu verwenden, die eine Aufgabe an mehreren Stellen ausführen, als Funktionen zu haben, die Aufgaben ausführen, die Sie nicht benötigen.
  3. Testbarkeit - Es ist einfacher, eine Funktion zu testen, die eine definierte "Funktion" hat, die viele von ihnen hat

Aber es könnte Probleme damit geben:

  • Es ist nicht leicht zu erkennen, wie die Funktion zu trennen ist. Möglicherweise müssen Sie zuerst das Innere der Funktion umgestalten, bevor Sie mit der Trennung fortfahren.
  • Die Funktion hat einen riesigen internen Zustand, der weitergegeben wird. Dies erfordert normalerweise eine Art OOP-Lösung.
  • Es ist schwer zu sagen, welche Funktion ausgeführt werden soll. Testen Sie es und überarbeiten Sie es, bis Sie es wissen.
Euphorisch
quelle
5

Das Problem, das Sie aufwerfen, ist kein Problem der Codierung, Konventionen oder Codierungspraxis, sondern ein Problem der Lesbarkeit und der Art und Weise, wie Texteditoren den von Ihnen geschriebenen Code anzeigen. Das gleiche Problem taucht auch in der Post auf:

Ist es in Ordnung, lange Funktionen und Methoden in kleinere aufzuteilen, obwohl sie von nichts anderem aufgerufen werden?

Die Aufteilung einer Funktion in Unterfunktionen ist sinnvoll, wenn ein großes System mit der Absicht implementiert wird, die verschiedenen Funktionen, aus denen es bestehen soll, zusammenzufassen. Sie werden jedoch früher oder später mit einer Reihe großer Funktionen konfrontiert sein. Einige von ihnen sind unlesbar und schwierig zu pflegen, ob Sie sie als einzelne lange Funktionen behalten oder sie aufteilen, ist kleinere Funktionen. Dies gilt insbesondere für Funktionen, bei denen die von Ihnen ausgeführten Vorgänge an keiner anderen Stelle Ihres Systems erforderlich sind. Lassen Sie uns eine solch lange Funktion aufgreifen und sie in einer breiteren Sicht betrachten.

Profi:

  • Sobald Sie es gelesen haben, haben Sie eine vollständige Vorstellung von allen Funktionen, die die Funktion ausführt (Sie können es als Buch lesen).
  • Wenn Sie es debuggen möchten, können Sie es Schritt für Schritt ausführen, ohne zu einer anderen Datei oder einem anderen Teil der Datei zu springen.
  • Sie haben die Freiheit, auf jede Variable zuzugreifen / sie zu verwenden, die in jeder Phase der Funktion deklariert wurde.
  • Der Algorithmus, den die Funktion implementiert, ist vollständig in der Funktion enthalten (gekapselt).

Contra:

  • Es dauert viele Seiten Ihres Bildschirms;
  • Es dauert lange, es zu lesen;
  • Es ist nicht einfach, sich all die verschiedenen Schritte zu merken.

Stellen wir uns nun vor, die lange Funktion in mehrere Unterfunktionen aufzuteilen und sie mit einer breiteren Perspektive zu betrachten.

Profi:

  • Mit Ausnahme der Leave-Funktionen beschreibt jede Funktion die durchgeführten Schritte mit Worten (Namen von Unterfunktionen);
  • Das Lesen jeder einzelnen Funktion / Unterfunktion dauert sehr kurz.
  • Es ist klar, welche Parameter und Variablen in jeder Unterfunktion betroffen sind (Trennung von Bedenken);

Contra:

  • Es ist leicht vorstellbar, was eine Funktion wie "sin ()" tut, aber nicht so leicht vorstellbar, was unsere Unterfunktionen tun.
  • Der Algorithmus ist jetzt verschwunden, er ist jetzt in Mai-Unterfunktionen aufgeteilt (keine Übersicht);
  • Wenn Sie es Schritt für Schritt debuggen, können Sie leicht den Funktionsaufruf der Tiefenebene vergessen, von dem Sie kommen (hier und da in Ihren Projektdateien springen).
  • Sie können leicht den Kontext verlieren, wenn Sie die verschiedenen Unterfunktionen lesen.

Beide Lösungen haben Vor- und Nachteile. Die beste Lösung wäre, Editoren zu haben, die es erlauben, jeden Funktionsaufruf in seinen Inhalt zu erweitern, inline und für die gesamte Tiefe. Daher ist die Aufteilung von Funktionen in Unterfunktionen die einzig beste Lösung.

Antonello Ceravola
quelle
2

Für mich gibt es vier Gründe, Codeblöcke in Funktionen zu extrahieren:

  • Sie verwenden es wieder : Sie haben gerade einen Codeblock in die Zwischenablage kopiert. Anstatt es einfach einzufügen, fügen Sie es in eine Funktion ein und ersetzen Sie den Block durch einen Funktionsaufruf auf beiden Seiten. Wenn Sie also diesen Codeblock ändern müssen, müssen Sie nur diese einzelne Funktion ändern, anstatt den Code an mehreren Stellen zu ändern. Wenn Sie also einen Codeblock kopieren, müssen Sie eine Funktion erstellen.

  • Es ist ein Rückruf : Es ist eine Ereignisbehandlungsroutine oder eine Art Benutzercode, den eine Bibliothek oder ein Framework aufruft. (Ich kann mir das kaum vorstellen, ohne Funktionen zu machen.)

  • Sie glauben, dass es im aktuellen Projekt oder an einem anderen Ort wiederverwendet wird: Sie haben gerade einen Block geschrieben, der die längste gemeinsame Folge von zwei Arrays berechnet. Selbst wenn Ihr Programm diese Funktion nur einmal aufruft, würde ich glauben, dass ich diese Funktion irgendwann auch in anderen Projekten benötigen werde.

  • Sie möchten selbstdokumentierenden Code : Anstatt also eine Kommentarzeile über einen Codeblock zu schreiben, in der zusammengefasst wird, was er tut, extrahieren Sie das Ganze in eine Funktion und benennen es so, wie Sie es in einen Kommentar schreiben würden. Obwohl ich kein Fan davon bin, weil ich gerne den Namen des verwendeten Algorithmus ausschreibe, den Grund, warum ich diesen Algorithmus gewählt habe, usw. Die Funktionsnamen wären dann zu lang ...

Calmarius
quelle
1

Ich bin sicher, Sie haben den Rat gehört, dass Variablen so eng wie möglich abgegrenzt werden sollten, und ich hoffe, Sie stimmen dem zu. Nun, Funktionen sind Container des Gültigkeitsbereichs, und in kleineren Funktionen ist der Gültigkeitsbereich der lokalen Variablen kleiner. Es ist viel klarer, wie und wann sie verwendet werden sollen, und es ist schwieriger, sie in der falschen Reihenfolge oder vor der Initialisierung zu verwenden.

Funktionen sind auch Container des logischen Flusses. Es gibt nur einen Weg hinein, die Auswege sind klar gekennzeichnet, und wenn die Funktion kurz genug ist, sollten die internen Flüsse offensichtlich sein. Dies führt zu einer Verringerung der zyklomatischen Komplexität, was eine zuverlässige Möglichkeit darstellt, die Fehlerrate zu verringern.

John Wu
quelle
0

Nebenbei: Ich habe dies als Antwort auf Dallins Frage geschrieben (jetzt geschlossen), aber ich denke immer noch, dass es für jemanden hilfreich sein könnte


Ich denke, dass der Grund für die Zerstäubung von Funktionen 2-fach ist und wie @jozefg erwähnt, von der verwendeten Sprache abhängt.

Trennung von Bedenken

Der Hauptgrund dafür ist, verschiedene Codeteile getrennt zu halten, sodass jeder Codeblock, der nicht direkt zum gewünschten Ergebnis / zur gewünschten Absicht der Funktion beiträgt, ein separates Problem darstellt und extrahiert werden kann.

Angenommen, Sie haben eine Hintergrundaufgabe, die auch einen Fortschrittsbalken aktualisiert. Die Fortschrittsbalkenaktualisierung steht nicht in direktem Zusammenhang mit der ausgeführten Aufgabe. Sie sollte daher extrahiert werden, auch wenn dies der einzige Code ist, der den Fortschrittsbalken verwendet.

Angenommen, Sie haben in JavaScript eine Funktion getMyData (), die 1) eine Soap-Nachricht aus Parametern erstellt, 2) eine Dienstreferenz initialisiert, 3) den Dienst mit der Soap-Nachricht aufruft, 4) das Ergebnis analysiert, 5) das Ergebnis zurückgibt. Scheint vernünftig, ich habe genau diese Funktion viele Male geschrieben - aber das könnte wirklich in 3 private Funktionen aufgeteilt werden, einschließlich Code für 3 & 5 (falls das der Fall ist), da keiner der anderen Codes direkt für den Erhalt von Daten vom Service verantwortlich ist .

Verbessertes Debugging

Wenn Sie vollständig atomare Funktionen haben, wird Ihr Stack-Trace zu einer Aufgabenliste, die den gesamten erfolgreich ausgeführten Code auflistet, dh:

  • Meine Daten abrufen
    • Soap Message erstellen
    • Servicereferenz initialisieren
    • Analysierte Serviceantwort - FEHLER

Es wäre viel interessanter, herauszufinden, dass beim Abrufen der Daten ein Fehler aufgetreten ist. Einige Tools sind jedoch noch nützlicher für das Debuggen detaillierter Anrufbäume, z. B. Microsoft Debugger Canvas .

Ich verstehe auch Ihre Bedenken, dass es schwierig sein kann, auf diese Weise geschriebenem Code zu folgen, da Sie am Ende des Tages eine Funktionsreihenfolge in einer einzelnen Datei auswählen müssen, da Ihr Aufrufbaum dann viel komplexer wäre . Aber wenn Funktionen gut benannt sind (Intellisense erlaubt mir, 3-4 Wörter in Groß- und Kleinschreibung in einer beliebigen Funktion zu verwenden, ohne mich zu verlangsamen) und mit einer öffentlichen Schnittstelle am Anfang der Datei strukturiert, liest sich Ihr Code wie ein Pseudocode, der Dies ist bei weitem der einfachste Weg, eine Codebasis auf hohem Niveau zu verstehen.

Zu Ihrer Information: Dies ist eines der Dinge, bei denen es keinen Sinn macht, Code atomar zu halten, es sei denn, Sie stimmen unbarmherzig damit überein, IMHO, was ich nicht bin.

Dead.Rabit
quelle