Ist es immer eine bewährte Methode, eine Funktion für alles zu schreiben, was zweimal wiederholt werden muss?

131

Ich selbst kann es kaum erwarten, eine Funktion zu schreiben, wenn ich mehr als zweimal etwas tun muss. Aber wenn es um Dinge geht, die nur zweimal auftauchen, ist es etwas kniffliger.

Für Code, der mehr als zwei Zeilen benötigt, schreibe ich eine Funktion. Aber wenn man sich Dingen wie:

print "Hi, Tom"
print "Hi, Mary"

Ich zögere zu schreiben:

def greeting(name):
    print "Hi, " + name

greeting('Tom')
greeting('Mary')

Der zweite scheint zu viel, nicht wahr?


Aber was ist, wenn wir haben:

for name in vip_list:
    print name
for name in guest_list:
    print name

Und hier ist die Alternative:

def print_name(name_list):
    for name in name_list:
        print name

print_name(vip_list)
print_name(guest_list)

Die Dinge werden schwierig, nein? Es ist jetzt schwer zu entscheiden.

Was ist deine Meinung dazu?

Zen
quelle
145
Ich behandle alwaysund neverals rote Fahnen. Es gibt eine Sache namens "Kontext", in der die alwaysund never-Regeln, auch wenn sie im Allgemeinen gut sind, möglicherweise nicht so angemessen sind. Hüten Sie sich vor Software-Entwicklern, die absolut handeln. ;)
asynchrone
7
In Ihrem letzten Beispiel können Sie tun: from itertools import chain; for name in chain(vip_list, guest_list): print(name).
Bakuriu,
14
@ user16547 Wir nennen diese "Sithware-Entwickler"!
Brian
6
Aus dem Zen von Python von Tim Peters:Special cases aren't special enough to break the rules. Although practicality beats purity.
Zenon
3
Überlegen
Ian

Antworten:

201

Obwohl es ein Faktor bei der Entscheidung ist, eine Funktion abzuspalten, sollte die Häufigkeit, mit der etwas wiederholt wird, nicht der einzige Faktor sein. Oft ist es sinnvoll, eine Funktion für etwas zu erstellen, das nur einmal ausgeführt wird . Im Allgemeinen möchten Sie eine Funktion aufteilen, wenn:

  • Es vereinfacht jede einzelne Abstraktionsschicht.
  • Sie haben gute, aussagekräftige Namen für die Abspaltungsfunktionen, sodass Sie normalerweise nicht zwischen den Abstraktionsebenen wechseln müssen, um zu verstehen, was vor sich geht.

Ihre Beispiele erfüllen diese Kriterien nicht. Sie wechseln von einem Einzeiler zu einem Einzeiler, und die Namen bieten Ihnen in Bezug auf Klarheit keine wirklichen Vorteile. Abgesehen davon sind solche einfachen Funktionen außerhalb von Tutorials und Schulaufgaben selten. Die meisten Programmierer neigen dazu, zu weit in die andere Richtung zu irren.

Karl Bielefeldt
quelle
Dies ist der Punkt, den ich in Robert Harveys Antwort vermisse.
Doc Brown
1
Tatsächlich. Ich habe oft komplexe Funktionen in inline Unterprogramme aufgeteilt - kein Volltreffer, aber sauber. Anonyme Bereiche sind auch gut.
Imallett
3
Es gibt auch eine Frage des Umfangs. Es ist wahrscheinlich nicht sinnvoll, eine Funktion print_name (name_list) zu schreiben , die eine ganze Klasse oder ein ganzes Modul sehen kann, aber es kann sinnvoll sein (in diesem Fall immer noch eine Strecke), eine lokale Funktion innerhalb einer Funktion zum Bereinigen von a zu erstellen einige Zeilen wiederholten Codes.
Joshua Taylor
6
Ein weiterer Punkt, den ich vielleicht hinzufügen möchte, ist die Standardisierung. Im ersten Beispiel wird eine Begrüßung durchgeführt, und es könnte sinnvoll sein, diese Funktion zuzuweisen, damit Sie sicher sind, dass Sie beide Personen gleich begrüßen. Vor allem, wenn man bedenkt, dass sich der Gruß in Zukunft ändern kann :)
Svish
1
+1 nur für Es ist oft sinnvoll, eine Funktion für etwas zu erstellen, das nur einmal ausgeführt wird. Ich wurde einmal gefragt, wie ich Tests für eine Funktion entwerfen soll, die das Verhalten eines Raketenmotors modelliert. Die zyklomatische Komplexität dieser Funktion war in den 90er Jahren und es war keine switch-Anweisung mit 90 Fällen (hässlich, aber testbar). Es war stattdessen ein kompliziertes Durcheinander, das von Ingenieuren geschrieben wurde und absolut nicht zu testen war. Meine Antwort war nur, dass es nicht testbar war und umgeschrieben werden musste. Sie folgten meinem Rat!
David Hammen
104

Nur wenn die Vervielfältigung beabsichtigt und nicht zufällig ist .

Oder anders ausgedrückt:

Nur wenn Sie erwarten , dass sie sich in Zukunft gemeinsam entwickeln.


Hier ist der Grund:

Manchmal zwei Teile des Codes nur passieren das gleiche worden , obwohl sie nichts miteinander zu tun haben. In diesem Fall müssen Sie sich dem Drang widersetzen, sie zu kombinieren, da diese Person beim nächsten Wartungsvorgang nicht erwartet , dass die Änderungen an einen zuvor nicht vorhandenen Aufrufer weitergegeben werden, und diese Funktion möglicherweise unterbrochen wird. Daher müssen Sie Code nur dann ausklammern, wenn es sinnvoll ist, und nicht immer dann, wenn es so aussieht, als würde es die Codegröße verringern.

Faustregel:

Wenn der Code nur neue Daten zurückgibt und keine vorhandenen Daten ändert oder andere Nebenwirkungen hat, ist es sehr wahrscheinlich, dass er als separate Funktion herausgerechnet werden kann. (Ich kann mir kein Szenario vorstellen, in dem dies zu einem Bruch führen würde, ohne die beabsichtigte Semantik der Funktion vollständig zu ändern. Zu diesem Zeitpunkt sollte sich auch der Funktionsname oder die Signatur ändern, und in diesem Fall müssten Sie trotzdem vorsichtig sein. )

Mehrdad
quelle
12
+1 Ich denke, dass dies die wichtigste Überlegung ist. Niemand refaktoriert Code, der erwartet, dass seine Änderung die letzte sein wird. Überlegen Sie also, wie sich Ihre Änderungen auf zukünftige Änderungen im Code auswirken würden.
YoniLavi
2
Es ist rubinrot, aber dieser Screencast über zufällige Vervielfältigung von Avdi Grimm bezieht sich immer noch auf die Frage: youtube.com/watch?v=E4FluFOC1zM
DGM
60

Nein, es ist nicht immer eine bewährte Methode.

Alle anderen Dinge, bei denen es sich um gleichen, linearen Code handelt, sind einfacher zu lesen als Funktionsaufrufe. Ein nicht-trivialer Funktionsaufruf benötigt immer Parameter, daher müssen Sie all das sortieren und mentale kontextbezogene Sprünge von Funktionsaufruf zu Funktionsaufruf durchführen. Bevorzugen Sie immer eine bessere Code-Klarheit, es sei denn, Sie haben einen triftigen Grund für Unklarheiten (z. B. den Erhalt der erforderlichen Leistungsverbesserungen).

Warum wird Code dann in separate Methoden umgestaltet? Verbesserung der Modularität. Sammeln wichtiger Funktionen hinter einer einzelnen Methode und Vergeben eines aussagekräftigen Namens. Wenn Sie dies nicht erreichen, benötigen Sie diese separaten Methoden nicht.

Robert Harvey
quelle
58
Die Lesbarkeit des Codes ist am wichtigsten.
Tom Robinson
27
Ich hätte diese Antwort positiv bewertet, wenn Sie nicht einen der wichtigsten Gründe für die Erstellung von Funktionen vergessen hätten: Eine Abstraktion zu erstellen, indem Sie einer Funktion einen guten Namen geben. Wenn Sie diesen Punkt hinzugefügt hätten, wäre klar, dass linearer, zeilenweiser Code nicht immer einfacher zu lesen ist als Code, der wohlgeformte Abstraktionen verwendet.
Doc Brown
17
Ich stimme Doc Brown zu, Code ist nicht unbedingt Zeile für Zeile besser lesbar als bei richtiger Abstraktion. Gute Funktionsnamen machen die Funktionen auf hoher Ebene SEHR einfach zu lesen (weil Sie die Absicht lesen, nicht die Implementierung) und Funktionen auf niedriger Ebene einfacher zu lesen, weil Sie eine Aufgabe, genau eine Aufgabe und nichts als diese Aufgabe ausführen. Dies macht die Funktion kurz und präzise.
Jon Story
19
Why take the performance hit of a function call when you don't get any significant benefit by doing so?Welche Leistung hat geschlagen? Im allgemeinen Fall werden kleine Funktionen eingeblendet und der Overhead ist für große Funktionen vernachlässigbar. CPython ist wahrscheinlich nicht inline, aber niemand verwendet es für die Leistung. Ich frage auch das line by line code...bisschen. Es ist einfacher, wenn Sie versuchen, die Ausführung in Ihrem Gehirn zu simulieren. Aber ein Debugger wird einen besseren Job machen und zu versuchen, etwas über eine Schleife zu beweisen, indem man ihrer Ausführung folgt, ist wie zu versuchen, eine Aussage über ganze Zahlen zu beweisen, indem man sie alle durchläuft.
Doval
6
@Doval wirft einen SEHR gültigen Punkt auf - wenn der Code kompiliert wird, wird die Funktion inline platziert (und dupliziert) und daher wird die Laufzeitleistung NICHT beeinträchtigt, obwohl es eine kleine in der Kompilierung gibt. Dies gilt nicht für interpretierte Sprachen, aber der Funktionsaufruf ist dennoch ein winziger Teil der Ausführungszeit.
Jon Story
25

In Ihrem speziellen Beispiel mag es übertrieben erscheinen, eine Funktion zu erstellen. stattdessen würde ich die Frage stellen: ist es möglich, dass sich dieser besondere Gruß in Zukunft ändert? Wie?

Funktionen werden nicht nur verwendet, um die Funktionalität einzuschließen und die Wiederverwendung zu vereinfachen, sondern auch, um die Änderung zu vereinfachen. Wenn sich die Anforderungen ändern, muss kopierter Code manuell gesucht und geändert werden, während der Code mit einer Funktion nur einmal geändert werden muss.

Ihr Beispiel würde davon nicht über eine Funktion profitieren - da die Logik so gut wie nicht existiert - sondern möglicherweise über eine GREET_OPENINGKonstante. Diese Konstante könnte dann aus einer Datei geladen werden, so dass Ihr Programm beispielsweise leicht an verschiedene Sprachen angepasst werden kann. Beachten Sie, dass dies eine grobe Lösung ist. Ein komplexeres Programm würde wahrscheinlich eine verfeinerte Methode zum Nachverfolgen von i18n benötigen, dies hängt jedoch wiederum von den Anforderungen und der zu erledigenden Arbeit ab.

Letztendlich geht es um mögliche Anforderungen und darum, Dinge im Voraus zu planen, um die Arbeit Ihres zukünftigen Selbst zu erleichtern.

Svalorzen
quelle
Bei Verwendung einer GREET_OPENING-Konstante wird davon ausgegangen, dass alle möglichen Begrüßungen in derselben Reihenfolge aufgeführt sind. Mit jemandem verheiratet zu sein, dessen Muttersprache fast alles andere als Englisch kann, lässt mich diese Annahme mißachten.
Loren Pechtel,
22

Persönlich habe ich die Dreierregel übernommen - was ich mit einem Anruf bei YAGNI rechtfertigen werde . Wenn ich etwas tun muss, sobald ich diesen Code schreibe, kann ich es zweimal einfach kopieren / einfügen (ja, ich habe gerade zugegeben, dass ich kopiere / einfügen muss!), Weil ich es nicht noch einmal brauche, aber wenn ich dasselbe tun muss Ding wieder , dann werde ich diese Brocken in seine eigene Methode Refactoring und extrahieren, ich habe mir selbst bewiesen, dass ich werde es wieder brauchen.

Ich stimme dem zu, was Karl Bielefeldt und Robert Harvey gesagt haben, und meine Interpretation dessen, was sie sagen, ist, dass die vorrangige Regel die Lesbarkeit ist. Wenn es das Lesen meines Codes erleichtert, sollten Sie eine Funktion erstellen. Denken Sie dabei an DRY und SLAP . Wenn ich vermeiden kann, dass sich die Abstraktionsebene in meiner Funktion ändert, ist die Verwaltung in meinem Kopf einfacher. Wenn ich also nicht zwischen Funktionen wechseln kann (wenn ich nicht verstehe, was die Funktion durch einfaches Lesen ihres Namens bewirkt), bedeutet dies, dass ich weniger Schalter in meinem Kopf habe mentale Prozesse.
Ebenso muss ich den Kontext nicht zwischen Funktionen und Inline-Code wechseln, wie es print "Hi, Tom"für mich funktioniert. In diesem Fall könnte ich eine Funktion iff extrahierenPrintNames() Der Rest meiner Gesamtfunktion bestand hauptsächlich aus Funktionsaufrufen.

Simon Martin
quelle
3
Das c2-Wiki hat eine gute Seite über die Dreierregel
pimlottc
13

Es ist selten eindeutig, so dass Sie Optionen abwägen müssen:

  • Deadline (Fix Burning Serverraum so schnell wie möglich)
  • Lesbarkeit des Codes (kann die Auswahl beeinflussen)
  • Abstraktionsebene der geteilten Logik (im Zusammenhang mit oben)
  • Wiederverwendungsanforderung (dh es ist genau dieselbe Logik wichtig oder gerade praktisch)
  • Schwierigkeiten beim Teilen (hier leuchtet Python, siehe unten)

In C ++ befolge ich normalerweise die Dreierregel (dh, wenn ich dasselbe zum dritten Mal benötige, baue ich es in ein ordnungsgemäß teilbares Teil um). Mit der Erfahrung ist es jedoch einfacher, diese Wahl zu treffen, da Sie mehr über den Umfang wissen. Software & Domain, mit der Sie arbeiten.

In Python ist es jedoch recht einfach, Logik wiederzuverwenden, anstatt sie zu wiederholen. Mehr als in vielen anderen Sprachen (zumindest historisch), IMO.

Überlegen Sie sich also, die Logik nur lokal wiederzuverwenden, indem Sie beispielsweise eine Liste aus lokalen Argumenten erstellen:

def foo():
    for name_list in (vip_list, guest_list): # can be list of tuples, for many args
        for name in name_list:
            print name

Sie können ein Tupel von Tupeln verwenden und es direkt in die for-Schleife aufteilen, wenn Sie mehrere Argumente benötigen:

def foo2():
    for header, name_list in (('vips': vip_list), ('people': guest_list)): 
        print header + ": "
        for name in name_list:
            print name

Oder machen Sie eine lokale Funktion (vielleicht ist dies Ihr zweites Beispiel), die die Wiederverwendung der Logik explizit macht, aber auch deutlich zeigt, dass print_name () nicht außerhalb der Funktion verwendet wird:

def foo():
    def print_name(name_list):
        for name in name_list:
            print name

    print_name(vip_list)
    print_name(guest_list)

Funktionen sind besonders dann vorzuziehen, wenn Sie die Logik in der Mitte abbrechen müssen (dh Return verwenden), da Unterbrechungen oder Ausnahmen die Dinge unnötig durcheinander bringen können.

Entweder ist es besser, dieselbe Logik, IMO, zu wiederholen, als eine globale / Klassenfunktion zu deklarieren, die nur von einem Aufrufer verwendet wird (wenn auch zweimal).

Macke
quelle
Ich habe einen ähnlichen Kommentar zum Umfang der neuen Funktion gepostet, bevor ich diese Antwort gesehen habe. Ich bin froh, dass es eine Antwort auf diesen Aspekt der Frage gibt.
Joshua Taylor
1
+1 - das ist vielleicht nicht die Art von Antwort, nach der das OP sucht, aber zumindest in Bezug auf die Art der Frage, die er gestellt hat, denke ich, dass dies die vernünftigste Antwort ist. Dies ist die Art von Dingen, die wahrscheinlich irgendwie für Sie in die Sprache eingebrannt werden.
Patrick Collins
@PatrickCollins: Danke für die Abstimmung! :) Ich habe einige Überlegungen hinzugefügt, um die Antwort vollständiger zu machen.
Macke
9

Alle Best Practices haben einen bestimmten Grund, auf den Sie sich beziehen können, um eine solche Frage zu beantworten. Sie sollten Welligkeiten immer dann vermeiden, wenn eine mögliche zukünftige Änderung auch Änderungen an anderen Komponenten impliziert.

In Ihrem Beispiel: Wenn Sie davon ausgehen, dass Sie eine Standardbegrüßung haben und sicherstellen möchten, dass sie für alle Personen gleich ist, schreiben Sie

def std_greeting(name):
    print "Hi, " + name

for name in ["Tom", "Mary"]:
    std_greeting(name)   # even the function call should be written only once

Andernfalls müssten Sie aufpassen und die Standardbegrüßung an zwei Stellen ändern, falls sie sich ändert.

Wenn jedoch das "Hallo" zufällig dasselbe ist und das Ändern einer der Begrüßungen nicht notwendigerweise zu Änderungen für die andere führt, halten Sie sie getrennt. Bewahren Sie sie daher getrennt auf, wenn es logische Gründe für die Annahme gibt, dass die folgende Änderung wahrscheinlicher ist:

print "Hi, Tom"
print "Hello, Mary"

Für Ihren zweiten Teil hängt es wahrscheinlich von zwei Faktoren ab, wie viel in Funktionen "gepackt" werden soll:

  • Halten Sie die Blöcke zur besseren Lesbarkeit klein
  • Halten Sie die Blöcke so groß, dass Änderungen nur in wenigen Blöcken auftreten. Es muss nicht zu viel gruppiert werden, da es schwierig ist, das Anrufmuster zu verfolgen.

Im Idealfall werden Sie denken, dass bei einer Änderung "Ich muss den größten Teil des folgenden Blocks ändern" und nicht "Ich muss den Code irgendwo in einem großen Block ändern ".

Gerenuk
quelle
Nur eine blöde Nippelei in Anbetracht Ihrer Schleife - wenn es wirklich nur eine Handvoll hartcodierter Namen geben muss, um sie zu durchlaufen, und wir nicht erwarten, dass sie sich ändern, würde ich argumentieren, dass es ein bisschen lesbarer ist, a nicht zu verwenden Liste. for name in "Tom", "Mary": std_greeting(name)
YoniLavi
Nun, um fortzufahren, könnte man sagen, dass eine hartcodierte Liste nicht in der Mitte des Codes erscheint und trotzdem eine Variable sein wird :) Aber richtig, ich habe tatsächlich vergessen, dass Sie die Klammern dort überspringen können.
Gerenuk
5

Der wichtigste Aspekt benannter Konstanten und Funktionen ist nicht so sehr, dass sie den Umfang der Eingabe verringern, sondern dass sie die verschiedenen Stellen "anhängen", an denen sie verwendet werden. Betrachten Sie für Ihr Beispiel vier Szenarien:

  1. Es ist notwendig, beide Begrüßungen gleich zu ändern, [zB zu Bonjour, Tomund Bonjour, Mary].

  2. Es ist notwendig, eine Begrüßung zu ändern, aber die andere so zu lassen, wie sie ist [zB Hi, Tomund Guten Tag, Mary].

  3. Es ist notwendig, beide Begrüßungen unterschiedlich zu ändern [zB Hello, Tomund Howdy, Mary].

  4. Keine der Begrüßungen muss jemals geändert werden.

Wenn es nie notwendig ist, eine der Begrüßungen zu ändern, spielt es keine Rolle, welcher Ansatz gewählt wird. Wenn Sie eine gemeinsame Funktion verwenden, hat dies den natürlichen Effekt, dass Begrüßungen auf dieselbe Weise geändert werden. Wenn sich alle Grüße auf die gleiche Weise ändern sollten, wäre das eine gute Sache. Sollte dies jedoch nicht der Fall sein, muss die Begrüßung jeder Person separat codiert oder angegeben werden. Alle Arbeiten, die unter Verwendung einer gemeinsamen Funktion oder Spezifikation durchgeführt wurden, müssen rückgängig gemacht werden (und wären besser nicht von vornherein erledigt worden).

Es ist zwar nicht immer möglich, die Zukunft vorherzusagen, aber wenn man Grund zu der Annahme hat, dass sich beide Begrüßungen mit größerer Wahrscheinlichkeit gemeinsam ändern müssen, ist dies ein Faktor für die Verwendung von gemeinsamem Code (oder einer benannten Konstante). ; Wenn man Grund zu der Annahme hat, dass es wahrscheinlicher ist, dass sich einer oder beide ändern müssen, damit sie sich unterscheiden, ist dies ein Faktor, der gegen die Verwendung von gemeinsamem Code spricht.

Superkatze
quelle
Diese Antwort gefällt mir, weil man (zumindest für dieses einfache Beispiel) die Wahl fast über eine Spieltheorie berechnen könnte.
RubberDuck
@RubberDuck: Ich denke, der Frage, welche Dinge "verzerrt" sein sollten oder nicht, wird generell viel zu wenig Aufmerksamkeit geschenkt - in vielerlei Hinsicht, und man würde erwarten, dass die beiden häufigsten Ursachen für Fehler (1) der Glaube sind aliasiert / angeheftet, wenn sie nicht vorhanden sind, oder (2) etwas ändern, ohne zu bemerken, dass andere Dinge daran hängen. Solche Probleme treten sowohl beim Entwurf von Code- als auch von Datenstrukturen auf, aber ich kann mich nicht erinnern, dass dem Konzept jemals viel Aufmerksamkeit geschenkt wurde. Aliasing spielt im Allgemeinen keine Rolle, wenn es nur eines von etwas gibt oder wenn es sich nie ändern wird, aber ...
Superkatze
@RubberDuck: ... wenn zwei von etwas übereinstimmen, ist es wichtig zu wissen, ob das Ändern des einen den Wert des anderen konstant lassen oder die Beziehung konstant halten soll (was bedeutet, dass sich der Wert des anderen ändern würde). Ich sehe oft großes Gewicht auf die Vorteile, Werte konstant zu halten, aber viel weniger auf die Wichtigkeit von Beziehungen .
Superkatze
Ich stimme vollkommen zu. Ich habe nur darüber nachgedacht, wie konventionelle Weisheit dazu sagt, es zu trocknen, aber statistisch gesehen ist es wahrscheinlicher, dass der wiederholte Code später kein wiederholter Code sein wird. Sie haben im Grunde 2 zu 1 Gewinnchancen.
RubberDuck
@RubberDuck: Dass ich zwei Szenarien gebe, in denen Ablösung wichtig ist, und eines, in dem Anhaftung besser ist, sagt nichts über die relativen Wahrscheinlichkeiten der verschiedenen auftretenden Szenarien aus. Es ist wichtig zu erkennen, wann zwei Codeteile "zufällig" übereinstimmen oder weil sie dieselbe fundamentale Bedeutung haben. Wenn mehr als zwei Elemente vorhanden sind, entstehen zusätzliche Szenarien, in denen sich eines oder mehrere von den anderen unterscheiden und dennoch zwei oder mehrere auf identische Weise ändern sollen. auch, wenn eine Aktion nur an einem Ort verwendet wird ...
Supercat
5

Ich denke, Sie sollten einen Grund haben, ein Verfahren zu erstellen. Es gibt eine Reihe von Gründen, eine Prozedur zu erstellen. Die, die ich für am wichtigsten halte, sind:

  1. Abstraktion
  2. Modularität
  3. Code-Navigation
  4. Aktualisieren Sie die Konsistenz
  5. Rekursion
  6. testen

Abstraktion

Eine Prozedur kann verwendet werden, um einen allgemeinen Algorithmus von den Details bestimmter Schritte im Algorithmus zu abstrahieren. Wenn ich eine passend zusammenhängende Abstraktion finde, hilft mir dies, die Art und Weise zu strukturieren, in der ich über die Funktionsweise des Algorithmus nachdenke. Zum Beispiel kann ich einen Algorithmus entwerfen, der mit Listen arbeitet, ohne unbedingt die Listendarstellung berücksichtigen zu müssen.

Modularität

Prozeduren können verwendet werden, um Code in Modulen zu organisieren. Die Module können oft separat behandelt werden. Zum Beispiel separat gebaut und getestet.

Ein gut gestaltetes Modul erfasst normalerweise eine zusammenhängende sinnvolle Funktionseinheit. Daher können sie häufig verschiedenen Teams gehören, vollständig durch eine Alternative ersetzt oder in einem anderen Kontext wiederverwendet werden.

Code-Navigation

In großen Systemen kann es schwierig sein, den mit einem bestimmten Systemverhalten verknüpften Code zu finden. Die hierarchische Organisation des Codes in Bibliotheken, Modulen und Prozeduren kann bei dieser Herausforderung hilfreich sein. Insbesondere, wenn Sie versuchen, a) Funktionen unter aussagekräftigen und vorhersehbaren Namen zu organisieren, b) die Prozeduren in Bezug auf ähnliche Funktionen nebeneinander zu platzieren.

Aktualisieren Sie die Konsistenz

In großen Systemen kann es schwierig sein, den gesamten Code zu finden, der geändert werden muss, um eine bestimmte Verhaltensänderung zu erzielen. Die Verwendung von Prozeduren zum Organisieren der Funktionalität des Programms kann dies erleichtern. Insbesondere, wenn jede Funktion Ihres Programms nur einmal und in einer zusammenhängenden Gruppe von Prozeduren vorkommt, ist es (meiner Erfahrung nach) weniger wahrscheinlich, dass Sie irgendwo fehlen, wo Sie eine Aktualisierung hätten vornehmen oder eine inkonsistente Aktualisierung vornehmen müssen.

Beachten Sie, dass Sie Prozeduren basierend auf der Funktionalität und den Abstraktionen im Programm organisieren sollten. Nicht basierend darauf, ob zwei Codebits momentan gleich sind.

Rekursion

Für die Verwendung der Rekursion müssen Sie Prozeduren erstellen

Testen

In der Regel können Sie Abläufe unabhängig voneinander testen. Das unabhängige Testen verschiedener Teile des Hauptteils einer Prozedur ist schwieriger, da Sie normalerweise den ersten Teil der Prozedur vor dem zweiten Teil ausführen müssen. Diese Beobachtung trifft häufig auch auf andere Ansätze zur Spezifizierung / Überprüfung des Verhaltens von Programmen zu.

Fazit

Viele dieser Punkte beziehen sich auf die Verständlichkeit des Programms. Sie können sagen, dass Verfahren eine Möglichkeit sind, eine neue Sprache für Ihre Domäne zu erstellen, mit der Sie Probleme und Prozesse in Ihrer Domäne organisieren, schreiben und lesen können.

flamingpenguin
quelle
4

Keiner der von Ihnen angegebenen Fälle scheint eine Umgestaltung wert zu sein.

In beiden Fällen führen Sie eine Operation aus, die klar und prägnant ausgedrückt wird, und es besteht keine Gefahr, dass eine inkonsistente Änderung vorgenommen wird.

Ich empfehle, dass Sie immer nach Möglichkeiten suchen, Code zu isolieren, wenn:

  1. Es ist eine identifizierbare, sinnvolle Aufgabe , die Sie trennen könnten, und

  2. Entweder

    ein. die Aufgabe ist komplex auszudrücken (unter Berücksichtigung der Ihnen zur Verfügung stehenden Funktionen) oder

    b. Die Aufgabe wird mehrmals ausgeführt, und Sie müssen sicherstellen, dass sie an beiden Stellen auf dieselbe Weise geändert wird.

Wenn die Bedingung 1 nicht erfüllt ist, werden Sie nicht die richtige Schnittstelle für den Code finden. Wenn Sie versuchen, sie zu erzwingen, werden Sie am Ende eine Menge Parameter, mehrere Dinge, die Sie zurückgeben möchten, eine Menge kontextbezogener Logik und möglicherweise eine Menge davon haben keinen guten Namen finden. Versuchen Sie zunächst, die Schnittstelle zu dokumentieren, an die Sie denken. Auf diese Weise können Sie die Hypothese testen, dass es sich um die richtige Bündelung von Funktionen handelt, und mit dem zusätzlichen Vorteil, dass der Code beim Schreiben bereits definiert und dokumentiert ist!

2a ist ohne 2b möglich: Manchmal habe ich Code aus dem Flow entfernt, auch wenn ich weiß, dass er nur einmal verwendet wird, einfach weil das Verschieben und Ersetzen durch eine einzelne Zeile dazu führt, dass der Kontext, in dem er aufgerufen wird, plötzlich viel besser lesbar ist (insbesondere, wenn es sich um ein einfaches Konzept handelt, dessen Implementierung die Sprache schwierig macht). Beim Lesen der extrahierten Funktion wird auch deutlich, wo die Logik beginnt und endet und was sie tut.

2b ist ohne 2a möglich: Der Trick ist, ein Gefühl dafür zu haben, welche Art von Änderungen wahrscheinlicher sind. Ist es wahrscheinlicher, dass sich Ihre Unternehmensrichtlinie von "Hallo" überall zu "Hallo" ändert, oder dass sich Ihre Begrüßung zu einem formaleren Ton ändert, wenn Sie eine Zahlungsaufforderung oder eine Entschuldigung für einen Serviceausfall senden? Manchmal liegt es vielleicht daran, dass Sie eine externe Bibliothek verwenden, bei der Sie sich nicht sicher sind und die Sie schnell gegen eine andere austauschen möchten: Die Implementierung kann sich ändern, auch wenn die Funktionalität dies nicht tut.

Meistens haben Sie jedoch eine Mischung aus 2a und 2b. Und Sie müssen Ihr Urteilsvermögen einsetzen und dabei den geschäftlichen Wert des Codes, die Häufigkeit seiner Verwendung, den Zustand Ihrer Testsuite usw. berücksichtigen.

Wenn Sie die gleiche Logik bemerken, die mehr als einmal verwendet wurde, sollten Sie auf jeden Fall die Gelegenheit nutzen, über ein Refactoring nachzudenken. Wenn Sie nin den meisten Sprachen mit neuen Anweisungen beginnen, die mehr als die Einrückungsstufen überschreiten, ist dies ein weiterer, geringfügig weniger wichtiger Auslöser (wählen Sie einen Wert, mit dem nSie für die jeweilige Sprache vertraut sind: Python könnte beispielsweise 6 sein).

Solange Sie über diese Dinge nachdenken und die großen Spaghetti-Code-Monster niederschlagen, sollte es Ihnen gut gehen - machen Sie sich keine Gedanken darüber, wie feinkörnig Ihr Refactoring sein muss, dass Sie keine Zeit haben für Dinge wie Tests oder Dokumentation aufgearbeitet. Wenn es getan werden muss, wird die Zeit zeigen.

user52889
quelle
3

Im Allgemeinen stimme ich Robert Harvey zu, wollte aber einen Fall hinzufügen, um eine Funktionalität in Funktionen aufzuteilen. Zur Verbesserung der Lesbarkeit. Betrachten Sie einen Fall:

def doIt(smth,smthElse)
    for x in getDataFromSomething(smth,smthElse):
        if not check(x,smth):
            continue
        process(x,smthElse)
        store(x) 

Obwohl diese Funktionsaufrufe nirgendwo anders verwendet werden, wird die Lesbarkeit erheblich verbessert, wenn alle drei Funktionen sehr lang sind und verschachtelte Schleifen usw. aufweisen.

UldisK
quelle
2

Es gibt keine feste Regel, die angewendet werden muss. Sie hängt von der tatsächlich verwendeten Funktion ab. Für den einfachen Namensdruck würde ich keine Funktion verwenden, aber wenn es eine mathematische Summe ist, wäre das eine andere Geschichte. Sie würden dann eine Funktion erstellen, auch wenn sie nur zweimal aufgerufen wird. Dies soll sicherstellen, dass die mathematische Summe immer dieselbe ist, wenn sie jemals geändert wird. In einem anderen Beispiel, wenn Sie irgendeine Form der Validierung durchführen, würden Sie eine Funktion verwenden. Wenn Sie also in Ihrem Beispiel überprüfen müssen, ob der Name länger als 5 Zeichen ist, würden Sie eine Funktion verwenden, um sicherzustellen, dass immer die gleichen Validierungen durchgeführt werden.

Ich denke, Sie haben Ihre eigene Frage beantwortet, als Sie sagten: "Für Codes, die mehr als zwei Zeilen benötigen, muss ich eine Funk schreiben". Im Allgemeinen würden Sie eine Funktion verwenden, aber Sie müssen auch Ihre eigene Logik verwenden, um festzustellen, ob die Verwendung einer Funktion einen Mehrwert bietet.

W.Walford
quelle
2

Ich bin zu der Überzeugung gelangt, dass etwas über das Refactoring hier nicht erwähnt wurde. Ich weiß, dass es hier bereits viele Antworten gibt, aber ich denke, das ist neu.

Ich bin seit jeher ein skrupelloser Umgestalter und ein starker Anhänger von DRY. Meistens liegt es daran, dass ich Probleme habe, eine große Codebasis in meinem Kopf zu behalten, und teilweise daran, dass mir DRY-Codierung und C & P-Codierung nichts Spaß machen. Tatsächlich ist es für mich schmerzhaft und furchtbar langsam.

Die Sache ist, auf DRY zu bestehen, hat mir viel Übung in einigen Techniken gegeben, die ich selten bei anderen sehe. Viele Leute bestehen darauf, dass es schwierig oder unmöglich ist, Java zu DRY zu machen, aber sie versuchen es einfach nicht.

Ein Beispiel von vor langer Zeit, das Ihrem Beispiel etwas ähnlich ist. Die Leute neigen dazu zu denken, dass die Erstellung einer Java-GUI schwierig ist. Natürlich ist es so, wenn Sie wie folgt codieren:

Menu m=new Menu("File");
MenuItem save=new MenuItem("Save")
save.addAction(saveAction); // I forget the syntax, but you get the idea
m.add(save);
MenuItem load=new MenuItem("Load")
load.addAction(loadAction)

Jeder, der das für verrückt hält, hat absolut Recht, aber es ist nicht Javas Schuld - Code sollte niemals so geschrieben werden. Diese Methodenaufrufe sind Funktionen, die in andere Systeme eingebunden werden sollen. Wenn Sie ein solches System nicht finden können, bauen Sie es!

Offensichtlich können Sie so keinen Code erstellen, also müssen Sie einen Schritt zurücktreten und sich das Problem ansehen. Was macht dieser wiederholte Code tatsächlich? Es spezifiziert einige Zeichenketten und ihre Beziehung (einen Baum) und verbindet die Blätter dieses Baums zu Aktionen. Also, was Sie wirklich wollen, ist zu sagen:

class Menu {
    @MenuItem("File|Load")
    public void fileLoad(){...}
    @MenuItem("File|Save")
    public void fileSave(){...}
    @MenuItem("Edit|Copy")
    public void editCopy(){...}...

Sobald Sie Ihre Beziehung auf eine Art und Weise definiert haben, die prägnant und beschreibend ist, schreiben Sie eine Methode, um damit umzugehen. In diesem Fall durchlaufen Sie die Methoden der übergebenen Klasse und erstellen einen Baum. Verwenden Sie dann diese Methode, um Ihre Menüs zu erstellen und Aktionen sowie (offensichtlich) ein Menü anzeigen. Sie werden keine Duplikate haben und Ihre Methode ist wiederverwendbar ... und wahrscheinlich einfacher zu schreiben, als es eine große Anzahl von Menüs gewesen wäre, und wenn Sie tatsächlich Spaß am Programmieren haben, hatten Sie viel mehr Spaß. Das ist nicht schwer - die Methode, die Sie schreiben müssen, besteht wahrscheinlich aus weniger Zeilen als das Erstellen Ihres Menüs von Hand!

Um dies gut zu machen, muss man viel üben. Sie müssen genau analysieren können, welche eindeutigen Informationen in den wiederholten Abschnitten enthalten sind, diese Informationen extrahieren und herausfinden, wie Sie sie gut ausdrücken können. Das Erlernen der Verwendung von Tools wie Zeichenfolgenanalyse und Anmerkungen ist sehr hilfreich. Es ist auch sehr wichtig, zu lernen, wie man Fehlerberichte und -dokumentationen wirklich klar macht.

Sie erhalten "freies" Üben, indem Sie einfach gut codieren - wenn Sie erst einmal gut darin sind, werden Sie feststellen, dass das Codieren von DRY (einschließlich des Schreibens eines wiederverwendbaren Tools) schneller ist als das Kopieren und Einfügen und alle Fehler, duplizierten Fehler und schwierige Änderungen, die diese Art der Codierung verursacht.

Ich glaube nicht, dass ich meine Arbeit genießen könnte, wenn ich nicht so viel DRY-Techniken und Werkzeuge üben würde, wie ich könnte. Wenn ich eine Lohnkürzung vornehmen müsste, um keine Programme kopieren und einfügen zu müssen, würde ich das übernehmen.

Also meine Punkte sind:

  • Kopieren und Einfügen kostet mehr Zeit, es sei denn, Sie wissen nicht, wie Sie gut umgestalten können.
  • Sie lernen, gut umzugestalten, indem Sie auf DRY bestehen, selbst in den schwierigsten und trivialsten Fällen.
  • Sie sind ein Programmierer. Wenn Sie ein kleines Tool benötigen, um Ihren Code DRY zu machen, erstellen Sie es.
Bill K
quelle
1

Im Allgemeinen ist es eine gute Idee, etwas in eine Funktion zu unterteilen, die die Lesbarkeit Ihres Codes verbessert, oder wenn der Teil, der oft wiederholt wird, ein Kern des Systems ist. Wenn Sie im obigen Beispiel die Benutzer je nach Gebietsschema begrüßen müssen, ist eine separate Begrüßungsfunktion sinnvoll.

user90766
quelle
1

Als Folge des Kommentars von @ DocBrown an @RobertHarvey zur Verwendung von Funktionen zur Erstellung von Abstraktionen: Wenn Sie keinen ausreichend informativen Funktionsnamen finden können, der nicht wesentlich prägnanter oder klarer als der dahinter stehende Code ist, abstrahieren Sie nicht viel . Fehlt ein anderer wichtiger Grund, um eine Funktion zu erstellen, ist dies nicht erforderlich.

Außerdem erfasst ein Funktionsname selten seine vollständige Semantik, sodass Sie möglicherweise seine Definition überprüfen müssen, wenn er nicht häufig genug verwendet wird, um vertraut zu sein. Insbesondere in einer anderen Sprache als einer funktionalen Sprache müssen Sie möglicherweise wissen, ob sie Nebenwirkungen hat, welche Fehler auftreten können und wie auf diese reagiert wird, wie komplex sie ist, ob sie Ressourcen zuweist und / oder freigibt und ob es sich um eine Thread-Sprache handelt. sicher.

Natürlich betrachten wir hier per definitionem nur einfache Funktionen, aber das geht in beide Richtungen - In solchen Fällen ist es unwahrscheinlich, dass Inlining die Komplexität erhöht. Darüber hinaus wird der Leser wahrscheinlich nicht erkennen, dass es sich um eine einfache Funktion handelt, bis er nachschaut. Selbst mit einer IDE, die Sie mit der Definition verknüpft, ist das visuelle Herumspringen ein Hindernis für das Verständnis.

Im Allgemeinen führt die rekursive Zerlegung von Code in mehrere kleinere Funktionen zu einem Punkt, an dem die Funktionen keine eigenständige Bedeutung mehr haben, da die Codefragmente, aus denen sie bestehen, durch den erstellten Kontext eine größere Bedeutung erhalten nach dem umgebenden Code. Stellen Sie es sich als Analogie wie ein Bild vor: Wenn Sie zu stark zoomen, können Sie nicht erkennen, was Sie sehen.

Sdenham
quelle
1
Auch wenn es sich um die einfachste Anweisung handelt, die sie in eine Funktion einschließt, sollte sie besser lesbar sein. Es gibt Zeiten, in denen das Einschließen in eine Funktion zu viel des Guten ist. Wenn Sie jedoch keinen Funktionsnamen finden können, der präziser und klarer ist als die Anweisungen, ist dies wahrscheinlich ein schlechtes Zeichen. Ihre Negative für die Verwendung von Funktionen sind keine große Sache. Die Negative für Inlining sind viel größer. Inlining erschwert das Lesen, die Wartung und die Wiederverwendung.
Pllee
@pllee: Ich habe Gründe dargelegt, warum das Zerlegen in Funktionen im Extremfall kontraproduktiv wird (beachten Sie, dass die Frage sich speziell darauf bezieht, sie ins Extreme zu treiben, nicht auf den allgemeinen Fall.) Sie haben diesen Punkten kein Gegenargument vorgelegt, aber behaupten nur, es sollte anders sein. Das Schreiben , dass das Namensproblem ist ‚wahrscheinlich‘ ein schlechtes Zeichen ist kein Argument , dass es hier als in den Fällen vermeidbar ist (mein Argument, natürlich, ist , dass es ist zumindest eine Warnung - , dass Sie den Punkt erreicht haben , können nur von um seiner selbst willen zu abstrahieren.)
sdenham
Ich erwähnte 3 Negative von Inlining, die nicht sicher sind, wo ich nur behaupte, dass es "anders sein sollte". Ich sage nur, wenn Sie sich keinen Namen einfallen lassen, tut Ihre Inline wahrscheinlich zu viel. Auch ich denke, dass Sie hier einen großen Punkt über sogar kleine einmal verwendete Methoden vermissen. Es gibt winzige, gut benannte Methoden, sodass Sie nicht wissen müssen, was der Code versucht (kein IDE-Sprung erforderlich). Zum Beispiel wird sogar die einfache Aussage von if (day == 0 || day == 6) vs if (isWeekend (day)) einfacher zu lesen und mental abzubilden. Jetzt, wenn Sie diese Aussage wiederholen müssen, isWeekendwird ein Kinderspiel.
pllee
@pllee Sie behaupten, dass es "wahrscheinlich ein schlechtes Zeichen" ist, wenn Sie nicht für jedes wiederholte Codefragment einen deutlich kürzeren, klareren Namen finden können, aber das scheint eine Glaubensfrage zu sein - Sie geben kein stützendes Argument an (um eine allgemeine Aussage zu treffen) In diesem Fall brauchen Sie mehr als nur Beispiele.) isWeekend hat einen aussagekräftigen Namen. Die einzige Möglichkeit, meine Kriterien zu verfehlen, besteht darin, dass es weder wesentlich kürzer noch wesentlich klarer als seine Implementierung ist. Indem Sie argumentieren, dass Sie denken, dass es das letztere ist, behaupten Sie, dass es kein Gegenbeispiel zu meiner Position ist (FWIW, ich habe isWeekend selbst verwendet.)
sdenham
Ich habe ein Beispiel angegeben, in dem sogar eine einzelne Zeilenanweisung besser lesbar ist, und ich habe Negative von Inlining-Code angegeben. Das ist meine bescheidene Meinung und alles, was ich sage, und nein, ich argumentiere nicht gegen oder spreche über die "unbenennbare Methode". Ich bin mir nicht sicher, was daran so verwirrend ist oder warum ich meiner Meinung nach einen "theoretischen Beweis" brauche. Ich habe die Lesbarkeit verbessert. Die Wartung ist schwieriger, da der Code-Hunk in N Stellen geändert werden muss (siehe DRY). Die Wiederverwendung ist offensichtlich schwieriger, es sei denn, Sie denken, kopieren und einfügen, um Code wiederzuverwenden.
Pllee