Sind private Methoden mit einem einzigen Verweis einen schlechten Stil?

139

Im Allgemeinen verwende ich private Methoden, um Funktionen zu kapseln, die an mehreren Stellen in der Klasse wiederverwendet werden. Aber manchmal habe ich eine große öffentliche Methode, die sich in kleinere Schritte aufteilen lässt, jede in ihrer eigenen privaten Methode. Dies würde die öffentliche Methode kürzer machen, aber ich bin besorgt, dass die Lesbarkeit beeinträchtigt, wenn jemand, der die Methode liest, gezwungen wird, zu anderen privaten Methoden zu wechseln.

Gibt es einen Konsens darüber? Ist es besser, lange öffentliche Methoden zu haben oder sie in kleinere Teile zu zerlegen, selbst wenn jedes Teil nicht wiederverwendbar ist?

Jordak
quelle
7
Alle Antworten sind meinungsbasiert. Originalautoren denken immer, dass ihr Ravioli-Code besser lesbar ist. nachfolgende Redakteure nennen es Ravioli.
Frank Hileman
5
Ich schlage vor, Sie lesen Clean Code. Es empfiehlt diesen Stil und hat viele Beispiele. Es hat auch eine Lösung für das "Herumspringen" -Problem.
Kat
1
Ich denke, das hängt von Ihrem Team und den darin enthaltenen Codezeilen ab.
Hoffentlich
1
Basiert das gesamte STRUTS-Framework nicht auf Einweg-Getter / Setter-Methoden, die alle Einweg-Methoden sind?
Zibbobz

Antworten:

203

Nein, das ist kein schlechter Stil. In der Tat ist es ein sehr guter Stil.

Private Funktionen müssen nicht nur aus Gründen der Wiederverwendbarkeit vorhanden sein. Das ist sicherlich ein guter Grund, sie zu erschaffen, aber es gibt noch einen anderen: Zersetzung.

Betrachten Sie eine Funktion, die zu viel bewirkt. Es ist hundert Zeilen lang und unmöglich zu überlegen.

Wenn Sie diese Funktion in kleinere Teile "aufteilen", "funktioniert" immer noch so viel wie zuvor, jedoch in kleineren Teilen. Sie ruft andere Funktionen auf, die beschreibende Namen haben sollten. Die Hauptfunktion liest sich fast wie ein Buch: mache A, dann mache B, dann mache C usw. Die Funktionen, die es aufruft, können nur an einer Stelle aufgerufen werden, aber jetzt sind sie kleiner. Jede bestimmte Funktion unterscheidet sich notwendigerweise von den anderen Funktionen: Sie haben unterschiedliche Bereiche.

Wenn Sie ein großes Problem in kleinere Probleme zerlegen, auch wenn diese kleineren Probleme (Funktionen) nur einmal verwendet / gelöst werden, haben Sie mehrere Vorteile:

  • Lesbarkeit. Niemand kann eine monolithische Funktion lesen und verstehen, was sie vollständig bewirkt. Sie können entweder sich selbst belügen oder es in mundgerechte Stücke aufteilen, die Sinn machen.

  • Bezugsort. Es ist jetzt unmöglich, eine Variable zu deklarieren und zu verwenden, sie dann zu belassen und 100 Zeilen später erneut zu verwenden. Diese Funktionen haben unterschiedliche Bereiche.

  • Testen. Während es nur erforderlich ist, die öffentlichen Mitglieder einer Klasse einer Prüfung zu unterziehen, kann es wünschenswert sein, auch bestimmte private Mitglieder zu prüfen. Wenn es einen kritischen Abschnitt einer langen Funktion gibt, der vom Testen profitieren könnte, ist es unmöglich, ihn unabhängig zu testen, ohne ihn in eine separate Funktion zu extrahieren.

  • Modularität. Nachdem Sie nun über private Funktionen verfügen, können Sie eine oder mehrere Funktionen finden, die in eine separate Klasse extrahiert werden können, unabhängig davon, ob sie nur hier verwendet werden oder wiederverwendbar sind. Bis zum vorherigen Punkt ist diese separate Klasse wahrscheinlich auch einfacher zu testen, da sie eine öffentliche Schnittstelle benötigt.

Die Idee, großen Code in kleinere Teile aufzuteilen, die einfacher zu verstehen und zu testen sind, ist ein zentraler Punkt in Onkel Bobs Buch Clean Code . Zum Zeitpunkt des Schreibens dieser Antwort ist das Buch neun Jahre alt, aber heute genauso aktuell wie damals.


quelle
7
Vergessen Sie nicht, konsistente Abstraktionsebenen und gute Namen beizubehalten, die sicherstellen, dass Sie nicht überrascht werden, wenn Sie in die Methoden schauen. Wundern Sie sich nicht, wenn sich dort ein ganzes Objekt versteckt.
candied_orange
14
Manchmal können Aufteilungsmethoden gut sein, aber es kann auch Vorteile bringen, die Dinge in einer Linie zu halten. Wenn eine Grenzüberprüfung beispielsweise innerhalb oder außerhalb eines Codeblocks sinnvoll durchgeführt werden kann, können Sie leicht feststellen, ob die Grenzüberprüfung einmal, mehrmals oder überhaupt nicht durchgeführt wird . Das Ziehen des Codeblocks in eine eigene Unterroutine würde es schwieriger machen, solche Dinge festzustellen.
Supercat
7
Das Aufzählungszeichen "Lesbarkeit" könnte als "Abstraktion" bezeichnet werden. Es geht um Abstraktion. Die "mundgerechten Stücke" machen eine Sache , eine Kleinigkeit, die Sie nicht wirklich interessieren, wenn Sie die öffentliche Methode lesen oder durchgehen. Indem Sie es zu einer Funktion abstrahieren, können Sie es überspringen und sich auf das große Schema der Dinge konzentrieren / was die öffentliche Methode auf einer höheren Ebene tut.
Mathieu Guindon
7
"Testing" Kugel ist fraglich, IMO. Wenn Ihre privaten Mitglieder getestet werden möchten, gehören sie wahrscheinlich zu ihrer eigenen Klasse, als öffentliche Mitglieder. Zugegeben, der erste Schritt zum Extrahieren einer Klasse / Schnittstelle ist das Extrahieren einer Methode.
Mathieu Guindon
6
Eine Funktion nur durch Zeilennummern, Codeblöcke und Variablenbereiche aufzuteilen, ohne sich um die tatsächliche Funktionalität zu kümmern, ist eine schreckliche Idee, und ich würde argumentieren, dass die bessere Alternative dazu darin besteht, sie einfach in einer Funktion zu belassen. Sie sollten Funktionen entsprechend der Funktionalität aufteilen. Siehe DaveGauers Antwort . Sie verweisen in Ihrer Antwort etwas darauf (mit Erwähnung von Namen und Problemen), aber ich kann nicht anders, als der Meinung, dass dieser Aspekt aufgrund seiner Wichtigkeit und Relevanz in Bezug auf die Frage viel mehr Aufmerksamkeit erfordert.
Dukeling
36

Es ist wahrscheinlich eine großartige Idee!

Ich habe Probleme mit der Aufteilung langer linearer Aktionssequenzen in separate Funktionen, nur um die durchschnittliche Funktionslänge in Ihrer Codebasis zu verringern:

function step1(){
  // ...
  step2(zarb, foo, biz);
}

function step2(zarb, foo, biz){
  // ...
  step3(zarb, foo, biz, gleep);
}

function step3(zarb, foo, biz, gleep){
  // ...
}

Jetzt haben Sie tatsächlich Quelltextzeilen hinzugefügt und die Lesbarkeit insgesamt erheblich verringert. Vor allem, wenn Sie jetzt viele Parameter zwischen den einzelnen Funktionen übergeben, um den Status zu verfolgen. Huch!

Allerdings , wenn Sie es geschafft haben, auf eine oder mehrere Zeilen in eine reine Funktion zu extrahieren , die einen einzigen, klaren Zweck (dient nur einmal , auch wenn genannt ), dann haben Sie die Lesbarkeit verbessert:

function foo(){
  f = getFrambulation();
  g = deglorbFramb(f);
  r = reglorbulate(g);
}

In der Praxis wird dies wahrscheinlich nicht einfach sein, aber wenn Sie lange genug darüber nachdenken, können Sie häufig Teile der reinen Funktionalität herausfordern.

Sie werden wissen, dass Sie auf dem richtigen Weg sind, wenn Sie Funktionen mit schönen Verbnamen haben und wenn Ihre übergeordnete Funktion sie aufruft und das Ganze praktisch wie ein Absatz in der Prosa liest.

Wenn Sie dann Wochen später zurückkehren, um weitere Funktionen hinzuzufügen, und feststellen, dass Sie eine dieser Funktionen tatsächlich wiederverwenden können, dann oh, entzückende Freude! Was für eine wundersame strahlende Freude!

DaveGauer
quelle
2
Ich habe dieses Argument seit vielen Jahren gehört und es spielt sich nie in der realen Welt ab. Ich spreche speziell von einer oder zwei Leitungsfunktionen, die nur von einer Stelle aufgerufen werden. Während der ursprüngliche Autor immer denkt, dass es "lesbarer" ist, nennen nachfolgende Herausgeber es oft Ravioli-Code. Dies hängt im Allgemeinen von der Gesprächstiefe ab.
Frank Hileman
34
@FrankHileman Um fair zu sein, ich habe noch nie einen Entwickler gesehen, der dem Code seines Vorgängers ein Kompliment machte.
T. Sar
4
@TSar der Vorgänger ist die Quelle aller Probleme!
Frank Hileman
18
@TSar Ich habe dem vorherigen Entwickler noch nie ein Kompliment gemacht, als ich der vorherige Entwickler war.
IllusiveBrian
3
Das Schöne an der Zerlegung ist, dass Sie dann komponieren können foo = compose(reglorbulate, deglorbFramb, getFrambulation);
:;
19

Die Antwort ist, dass es stark von der Situation abhängt. @Snowman deckt die positiven Aspekte der Aufteilung großer öffentlicher Funktionen ab, es ist jedoch wichtig, sich daran zu erinnern, dass es auch negative Auswirkungen geben kann, da Sie zu Recht besorgt sind.

  • Viele private Funktionen mit Nebenwirkungen können das Lesen von Code erschweren und zu Problemen führen. Dies gilt insbesondere dann, wenn diese privaten Funktionen von den gegenseitigen Nebenwirkungen abhängen. Vermeiden Sie eng gekoppelte Funktionen.

  • Abstraktionen sind undicht . Es ist zwar nett vorzutäuschen, wie eine Operation ausgeführt wird oder wie Daten gespeichert werden, aber es gibt Fälle, in denen dies der Fall ist, und es ist wichtig, sie zu identifizieren.

  • Semantik und Kontext. Wenn Sie nicht klar erfassen, was eine Funktion in ihrem Namen tut, können Sie die Lesbarkeit verringern und die Fragilität der öffentlichen Funktion erhöhen. Dies gilt insbesondere dann, wenn Sie private Funktionen mit einer großen Anzahl von Eingabe- und Ausgabeparametern erstellen. Während Sie in der öffentlichen Funktion sehen, welche privaten Funktionen sie aufruft, sehen Sie in der privaten Funktion nicht, welche öffentlichen Funktionen sie aufrufen. Dies kann zu einem "Bugfix" in einer privaten Funktion führen, der die öffentliche Funktion unterbricht.

  • Stark zerlegter Code ist für den Autor immer klarer als für andere. Dies bedeutet nicht, dass es für andere nicht immer klar sein kann, aber es ist leicht zu sagen, dass es durchaus Sinn macht, das zum Zeitpunkt des Schreibens bar()vorher aufgerufen werden muss foo().

  • Gefährliche Wiederverwendung. Ihre öffentliche Funktion hat wahrscheinlich die Eingabemöglichkeiten für jede private Funktion eingeschränkt. Wenn diese Eingabeannahmen nicht richtig erfasst werden (es ist schwierig, ALLE Annahmen zu dokumentieren), kann jemand eine Ihrer privaten Funktionen falsch wiederverwenden und Fehler in die Codebasis einbringen.

Zerlegen Sie Funktionen anhand ihres inneren Zusammenhalts und ihrer Kopplung, nicht anhand der absoluten Länge.

dlasalle
quelle
6
Ignorieren Sie die Lesbarkeit, lassen Sie uns den Aspekt der Fehlerberichterstattung betrachten: Wenn der Benutzer meldet, dass ein Fehler aufgetreten ist MainMethod(leicht zu bekommen, normalerweise sind Symbole enthalten und Sie sollten eine Symboldereferenzdatei haben), die 2k Zeilen umfasst (Zeilennummern sind niemals enthalten) Fehlerberichte) und es ist ein NRE, das bei 1k dieser Zeilen auftreten kann, nun, ich werde verdammt sein, wenn ich mir das anschaue. Aber wenn sie mir sagen, dass es in SomeSubMethodA8 Zeilen passiert ist und die Ausnahme ein NRE war, dann werde ich das wahrscheinlich leicht genug finden und beheben.
410_Gone
2

IMHO, der Wert des Herausziehens von Codeblöcken nur als Mittel zum Aufbrechen der Komplexität, hängt häufig mit dem Unterschied in der Komplexität zusammen zwischen:

  1. Eine vollständige und genaue Beschreibung der Funktionsweise des Codes in menschlicher Sprache, einschließlich der Behandlung von Eckfällen und

  2. Der Code selbst.

Wenn der Code viel komplizierter als die Beschreibung ist, kann das Ersetzen des Inline-Codes durch einen Funktionsaufruf das Verständnis des umgebenden Codes erleichtern. Auf der anderen Seite können einige Konzepte besser in einer Computersprache als in der menschlichen Sprache ausgedrückt werden. Ich halte w=x+y+z;zum Beispiel für lesbarer als w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);.

Wenn eine große Funktion aufgeteilt wird, gibt es immer weniger Unterschiede zwischen der Komplexität von Unterfunktionen und ihren Beschreibungen, und der Vorteil weiterer Unterteilungen nimmt ab. Wenn die Dinge so aufgeteilt werden, dass Beschreibungen komplizierter sind als Code, wird der Code durch weitere Aufteilungen verschlimmert.

Superkatze
quelle
2
Ihr Funktionsname ist irreführend. Es gibt nichts auf dem w = x + y + z, das irgendeine Art von Überlaufkontrolle anzeigt, während der Methodenname an sich etwas anderes zu implizieren scheint. Ein besserer Name für Ihre Funktion wäre beispielsweise "addAll (x, y, z)" oder auch nur "Sum (x, y, z)".
T. Sar
6
Da es sich um private Methoden handelt , bezieht sich diese Frage möglicherweise nicht auf C. Wie auch immer, das ist nicht mein Punkt - Sie können dieselbe Funktion einfach als "Summe" bezeichnen, ohne die Annahme der Methodensignatur in Angriff nehmen zu müssen. Nach Ihrer Logik sollte jede rekursive Methode beispielsweise "DoThisAssumingStackDoesntOverflow" heißen. Gleiches gilt für "FindSubstringAssumingStartingPointIsInsideBounds".
T. Sar
1
Mit anderen Worten, Grundannahmen, egal wie sie lauten (zwei Zahlen laufen nicht über, Stapel läuft nicht über, Index wurde korrekt übergeben), sollten im Methodennamen nicht vorhanden sein. Diese Dinge sollten natürlich bei Bedarf dokumentiert werden, aber nicht auf der Methodensignatur.
T. Sar
1
@TSar: Ich war ein wenig umständlich mit dem Namen, aber der entscheidende Punkt ist, dass ein Programmierer, der "(x + y + z + 1) / 3" im Kontext sieht (auf Mittelwertbildung umschalten, weil es ein besseres Beispiel ist), dies sein wird weitaus besser in der Lage zu wissen, ob dies eine angemessene Methode zur Berechnung des Durchschnitts bei gegebenem Anrufcode ist, als jemand, der sich entweder die Definition oder die Anrufstelle von ansieht int average3(int n1, int n2, int n3). Das Aufteilen der Funktion "Durchschnitt" würde bedeuten, dass jemand, der prüfen möchte, ob sie für Anforderungen geeignet ist, an zwei Stellen suchen muss.
Supercat
1
Wenn wir zum Beispiel C # nehmen, hätten Sie nur eine einzige "Average" -Methode, mit der beliebig viele Parameter akzeptiert werden können. Tatsächlich funktioniert es in c # über jede beliebige Auflistung von Zahlen - und ich bin sicher, es ist viel einfacher zu verstehen, als grobe Mathematik in den Code einfließen zu lassen.
T. Sar
2

Es ist eine ausgleichende Herausforderung.

Feiner ist besser

Die private Methode liefert effektiv einen Namen für den enthaltenen Code und manchmal eine aussagekräftige Signatur (es sei denn, die Hälfte ihrer Parameter sind vollständig Ad-hoc- Daten mit unklaren, undokumentierten Abhängigkeiten).

Das Vergeben von Namen für Codekonstrukte ist im Allgemeinen gut, solange die Namen einen sinnvollen Vertrag für den Aufrufer andeuten und der Vertrag der privaten Methode genau dem entspricht, was der Name andeutet.

Indem der ursprüngliche Entwickler gezwungen ist, an sinnvolle Verträge für kleinere Teile des Codes zu denken, kann er einige Fehler erkennen und diese schmerzlos vermeiden, noch bevor er Entwickler- Tests durchführt. Dies funktioniert nur, wenn der Entwickler eine präzise Benennung anstrebt (einfach klingend, aber genau) und bereit ist, die Grenzen der privaten Methode so anzupassen, dass sogar eine präzise Benennung möglich ist.

Eine spätere Wartung wird ebenfalls unterstützt, da zusätzliche Benennungen dazu beitragen, dass sich der Code selbst dokumentiert .

  • Verträge über kleine Codestücke
  • Übergeordnete Methoden verwandeln sich manchmal in eine kurze Abfolge von Aufrufen, von denen jeder einen signifikanten und eindeutigen Namen hat - begleitet von der dünnen Schicht allgemeiner, äußerster Fehlerbehandlung. Eine solche Methode kann zu einer wertvollen Schulungsressource für jeden werden, der schnell ein Experte für die Gesamtstruktur des Moduls werden muss.

Bis es zu fein wird

Kann man es übertreiben, kleinen Codestücken Namen zu geben, und am Ende zu viele, zu kleine private Methoden haben? Sicher. Eines oder mehrere der folgenden Symptome weisen darauf hin, dass die Methoden zu klein werden, um nützlich zu sein:

  • Zu viele Überladungen, die nicht wirklich alternative Signaturen für dieselbe essentielle Logik darstellen, sondern einen einzelnen festen Aufrufstapel.
  • Synonyme, die nur verwendet werden, um sich wiederholt auf dasselbe Konzept zu beziehen ("unterhaltsame Benennung")
  • Viele flüchtige Namensdekorationen wie XxxInternaloder DoXxx, besonders wenn es kein einheitliches Schema für die Einführung dieser gibt.
  • Unbeholfene Namen, die fast länger als die Implementierung selbst sind, mögen LogDiskSpaceConsumptionUnlessNoUpdateNeeded
Jirka Hanika
quelle
1

Im Gegensatz zu dem, was die anderen gesagt haben, würde ich argumentieren, dass eine lange öffentliche Methode ein Designgeruch ist, der nicht durch Zerlegung in private Methoden korrigiert wird.

Aber manchmal habe ich eine große öffentliche Methode, die in kleinere Schritte unterteilt werden könnte

Wenn dies der Fall ist, würde ich argumentieren, dass jeder Schritt sein eigener erstklassiger Bürger mit jeweils einer Verantwortung sein sollte. In einem objektorientierten Paradigma würde ich vorschlagen, eine Schnittstelle und eine Implementierung für jeden Schritt so zu erstellen, dass jeder eine einzelne, leicht identifizierbare Verantwortung hat und auf eine Weise benannt werden kann, dass die Verantwortlichkeit klar ist. Auf diese Weise können Sie die (ehemals) große öffentliche Methode sowie jeden einzelnen Schritt unabhängig voneinander einem Komponententest unterziehen. Sie sollten auch alle dokumentiert werden.

Warum nicht in private Methoden zerlegen? Hier einige Gründe:

  • Enge Kopplung und Testbarkeit. Durch Verringern der Größe Ihrer öffentlichen Methode haben Sie die Lesbarkeit verbessert, aber der gesamte Code ist immer noch eng miteinander verbunden. Sie können die einzelnen privaten Methoden einzeln testen (mithilfe der erweiterten Funktionen eines Testframeworks), die öffentliche Methode kann jedoch nicht einfach unabhängig von den privaten Methoden getestet werden. Dies widerspricht den Grundsätzen des Unit-Testings.
  • Klassengröße und Komplexität. Sie haben die Komplexität einer Methode reduziert, aber die Komplexität der Klasse erhöht. Die öffentliche Methode ist einfacher zu lesen, aber die Klasse ist jetzt schwieriger zu lesen, da sie über mehr Funktionen verfügt, die ihr Verhalten definieren. Ich bevorzuge kleine Einzelunterrichtsstunden, daher ist eine lange Methode ein Zeichen dafür, dass die Klasse zu viel tut.
  • Kann nicht einfach wiederverwendet werden. Es ist oft der Fall, dass die Wiederverwendbarkeit nützlich ist, wenn ein Codeteil ausgereift ist. Wenn es sich bei Ihren Schritten um private Methoden handelt, können sie nirgendwo anders wiederverwendet werden, ohne sie zuvor irgendwie zu extrahieren. Außerdem kann das Kopieren und Einfügen gefördert werden, wenn ein Schritt an einer anderen Stelle erforderlich ist.
  • Eine Aufteilung auf diese Weise ist wahrscheinlich willkürlich. Ich würde argumentieren, dass das Aufteilen einer langen öffentlichen Methode nicht so viel Gedanken- oder Entwurfsüberlegung erfordert, als ob Sie die Verantwortlichkeiten in Klassen aufteilen würden. Jede Klasse muss mit einem korrekten Namen, einer Dokumentation und Tests begründet werden, wohingegen eine private Methode weniger berücksichtigt wird.
  • Versteckt das Problem. Sie haben sich also entschieden, Ihre öffentliche Methode in kleine private Methoden aufzuteilen. Jetzt gibt es kein Problem! Sie können immer mehr Schritte hinzufügen, indem Sie immer mehr private Methoden hinzufügen! Im Gegenteil, ich denke, das ist ein großes Problem. Es erstellt ein Muster, um einer Klasse Komplexität zu verleihen, auf das nachfolgende Fehlerkorrekturen und Feature-Implementierungen folgen. Bald werden Ihre privaten Methoden wachsen und sie müssen aufgeteilt werden.

Aber ich mache mir Sorgen, dass das Erzwingen, dass jeder, der die Methode liest, zu anderen privaten Methoden wechselt, die Lesbarkeit beeinträchtigt

Dies ist ein Streit, den ich kürzlich mit einem meiner Kollegen geführt habe. Er argumentiert, dass das gesamte Verhalten eines Moduls in derselben Datei / Methode die Lesbarkeit verbessert. Ich bin damit einverstanden, dass der Code insgesamt leichter zu befolgen ist , aber mit zunehmender Komplexität ist es weniger einfach, über den Code nachzudenken. Wenn ein System wächst, wird es unlösbar, über das gesamte Modul nachzudenken. Wenn Sie komplexe Logik in mehrere Klassen mit jeweils einer Verantwortlichkeit zerlegen, ist es viel einfacher, über jeden Teil nachzudenken.

Samuel
quelle
1
Ich würde nicht zustimmen müssen. Zu viel oder zu frühes Abstrahieren führt zu unnötig komplexem Code. Eine private Methode kann der erste Schritt auf einem Pfad sein, für den möglicherweise eine Schnittstelle und Abstraktion erforderlich ist. Ihr Ansatz hier schlägt jedoch vor, durch Orte zu wandern, die möglicherweise nie besucht werden müssen.
WillC
1
Ich verstehe, woher Sie kommen, aber ich denke, dass der Code, nach dem OP fragt, Anzeichen dafür sind, dass er für ein besseres Design bereit ist . Eine vorzeitige Abstraktion würde das Entwerfen dieser Schnittstellen bedeuten, bevor Sie überhaupt auf dieses Problem stoßen.
Samuel
Ich stimme diesem Ansatz zu. In der Tat, wenn Sie TDD folgen, ist es das natürliche Fortschreiten. Die andere Person sagt, es sei zu viel vorzeitige Abstraktion, aber ich finde es viel einfacher, eine Funktion, die ich in einer separaten Klasse (hinter einer Schnittstelle) benötige, schnell zu verspotten, als sie tatsächlich in einer privaten Methode zu implementieren, damit der Test erfolgreich ist .
Eternal21