3 Punkte von GN⁺ 2023-09-17 | 1 Kommentare | Auf WhatsApp teilen
  • Statt den Abstraktionsgrad durch Aufteilen in kleine Funktionen zu trennen, ist linearer Code, der von oben nach unten verläuft, leichter entlang des Gesamtflusses zu verfolgen
  • Wenn man durch Funktionsauslagerung eine Top-down-Struktur erzeugt, muss man möglicherweise hin- und herwechseln, um zwischen ähnlich benannten Funktionen wie bake und bakePizza nachzusehen
  • Kleine Funktionen können zwar die Absicht offenlegen, zugleich aber das tatsächliche Verhalten verbergen, etwa wo das Vorheizen des Ofens stattfindet oder was passiert, wenn man dieselbe Pizza zweimal weitergibt
  • Wenn man linearen Code mit schrittweisen Kommentaren versieht, kann man die Arbeitsabsicht erklären, ohne indirekte Verweise zu vermehren; das kann lesbarer sein als zusätzliche Abstraktion
  • Das Auslagern kleiner Funktionen, die nur einmal verwendet werden, führt zu Verlust an Linearität; wie bei der Art, den Ofen im Beispiel zu erzeugen, können in realem Code sogar Performance-Probleme sichtbar werden

Wenn Linearität wichtiger ist als Funktionsauslagerung

  • Das Beispiel im Google Testing Blog vergleicht zwei Implementierungen von createPizza und bewertet die rechte Implementierung als leichter lesbar und Top-down, weil sie keine Abstraktionsebenen mischt
  • Die Gegenposition hält es für wichtiger, dass die linke Implementierung linear lesbarer Code ist, der sich auf dem Bildschirm von oben nach unten lesen lässt
    • Um das Gesamtverhalten der rechten Implementierung zu verstehen, muss man zu mehreren kleinen Funktionsdefinitionen springen
    • Auch in der Darstellung wirkt die Größe beider Implementierungen ähnlich, weil ein Teil des rechten Codes ausgelassen wird; tatsächlich ist die rechte Version länger
  • Funktionsauslagerung kann es erschweren, das Verhalten allein anhand des Namens ausreichend zu verstehen
    • Wenn es sowohl bake als auch bakePizza gibt, ist nicht sofort klar, welche Funktion den Ofen aufheizt
    • Ob das zweimalige Weitergeben derselben Pizza idempotent ist oder das Ergebnis ruiniert, lässt sich nur durch Blick in die Implementierung prüfen

Linearer Code mit Kommentaren und das Ofenbeispiel

  • Die am leichtesten lesbare Form wird als eine Version des linken linearen Codes bewertet, bei der die Funktionsnamen der rechten Seite als Kommentare eingefügt sind
    • Kommentare wie Prepare pizza, Add toppings, Heat oven, Bake pizza, Box and slice machen die Absicht jedes Schritts sichtbar
    • Lesbarkeit entsteht nicht durch zusätzliche Abstraktionsebenen und indirekte Verweise, sondern dadurch, dass klar erklärt wird, was gerade geschieht
  • Das Fazit geht eher in die Richtung, kleine Funktionen, die nur einmal genutzt werden, nicht aus linearem Code herauszulösen
    • Der Nutzen kleiner ausgelagerter Funktionen gleicht den Verlust an Linearität nicht aus
  • Auch die Ofenbehandlung im Beispiel wirkt strukturell ungeschickt
    • Das Vorheizen des Ofens ist ein in sich abgeschlossener Vorgang und wäre daher passender als Methode des Ofens selbst
    • Für jede einzelne Pizza einen neuen Ofen zu erzeugen und vorzuheizen, passt nicht zu einem realistischen Nutzungsverhalten
    • Auch in echtem Code taucht eine solche Struktur auf und kann manchmal Performance-Probleme verursachen
  • Der Ofen sollte wahrscheinlich eher als Parameter übergeben werden, statt innerhalb von createPizza neu erzeugt zu werden
    • Das Bereitstellen des Ofens liegt eher in der Verantwortung des Aufrufers
    • Wenn der Ablauf darin besteht, die Pizza in eine Schachtel zu legen, könnte ein Interface, das nicht die Pizza, sondern die Schachtel zurückgibt, natürlicher sein

1 Kommentare

 
GN⁺ 2023-09-17
Hacker-News-Kommentare
  • Es ist eine Frage des Stils, und wie beim Kochen ruiniert sowohl zu viel als auch zu wenig Salz das Gericht.
    Ich hoffe, hier schlägt niemand eine einzelne 1000-Zeilen-Gottfunktion vor, aber maximal 5 Zeilen pro Funktion sind auch nicht gut lesbar. Wo man trennt, erfordert Urteilsvermögen, ein gutes Gespür und Iteration. Nur weil die erste Abstraktion nicht besonders gut war, sollte man Abstraktion nicht aufgeben; nach ein paar Refactorings können Klassen und APIs entstehen, die gut zur Business-Domain passen.
    Gleichzeitig darf man nicht zu voreilig abstrahieren oder so tun, als seien ein paar doppelte Zeilen eine tödliche Verletzung. Voreilige Abstraktion bündelt leicht Code, der sich gar nicht gemeinsam weiterentwickeln muss. Eine Funktion herauszuziehen, die nur an einer Stelle aufgerufen wird, um eine Arbeitseinheit zu verbergen, kann einen Algorithmus aufräumen und ist besonders nützlich, wenn sie Boilerplate oder die Vermischung von Business-Logik und Infrastrukturbelangen wie DB-Verbindungsbehandlung verbirgt. Man sollte es aber mit Bedacht einsetzen und vermeiden, Schritte zu zerlegen, die auf derselben Abstraktionsebene liegen sollten.

    • Der Kern ist genau dieser Punkt. Anfänger neigen dazu, riesige Funktionen zu schreiben, und begeisterte Entwickler, die zum ersten Mal Bücher wie Clean Code gelesen haben, wollen alles in eine Million Funktionen von ein paar Zeilen aufteilen.
      Eine Person, mit der ich gearbeitet habe, hat jede boolesche Bedingung in eine Funktion ausgelagert, weil das „lesbarer“ sei, und gar keine Kommentare geschrieben, weil „Kommentare schlecht“ seien. Ich mag dieses Buch nicht, weil es Fanatiker hervorbringt, die solche schlechten Ratschläge blind befolgen.
    • Warum sollte eine 1000-Zeilen-Gottfunktion nicht gehen? Wer hat gesagt, dass sie schlechter ist, und welche Studie ist zu diesem Schluss gekommen?
      Manchmal kann die Domain eine 1000-Zeilen-Gottfunktion verlangen, und wenn Logik und Arbeit an einem Ort gebündelt sind, kann das viel lesbarer sein als zwanzig 50-Zeilen-Funktionen. Um das Ganze zu verstehen, muss man diese 20 Funktionen ohnehin alle lesen, und jemand könnte versuchen, Teile davon wiederzuverwenden, sie dann an zwei oder drei Anforderungen anzupassen, die es in der ursprünglichen Aufgabe gar nicht gab, und bestimmte Logik mit einem nicht verwandten Use Case verknüpfen.
      Wenn diese Funktion eine pure function ist, finde ich 1000 oder auch 10000 Zeilen egal; sie kann trotzdem in Ordnung sein.
    • Um bei der Kochanalogie zu bleiben: Wenn man jemandem erklärt, wie man eine Mahlzeit zubereitet, und an einer Stelle Fond hinzugeben muss, ist es sinnvoll, wie man Fond macht in einem eigenen Abschnitt zu erklären. Fond ist etwas Eigenständiges und hat nur eine Berührungsstelle mit dem Gericht, daher ist es gut und sogar hilfreich, ihn auszulagern.
      Kochrezepte sind bereits stark abstrahiert. Wenn dort steht „Zwiebeln leicht anschwitzen“, wird vorausgesetzt, dass man weiß, wie man Zwiebeln schneidet und wie der Algorithmus des leichten Anschwitzens funktioniert. Wenn man alles inline ausschreibt, wird es unlesbar.
      Bei Code ist es ähnlich. Wenn man Abstraktion strikt ausschließt, landet man auf der niedrigsten Ebene, die die Sprache erlaubt, und das ist definitiv kein gut lesbarer Code. Wenn man in Python zum Beispiel statt der Methode decode Unicode-Decoding selbst implementieren wollte, wäre es sehr schwer zu verstehen, was das Programm tatsächlich tut. Niemand macht das, weil die Sprache einfache und gut geprüfte Abstraktionen bereitstellt; worin unterscheidet sich das davon, selbst einfache, gut geprüfte Abstraktionen zu bauen und sie über die Business-Logik hinweg zu verwenden?
      Der schwierige Teil ist, eine Abstraktion so gut zu wählen, dass niemand sie später noch einmal anfassen muss.
    • Feedback, das ich Teams im Unternehmen oft gebe, ist: einen Schritt zurücktreten, die größere Problem-Domain betrachten und überlegen, ob diese Dinge zwangsläufig gleich oder nur zufällig gleich sind.
      Nur weil Codezeilen jetzt ähnlich aussehen, heißt das nicht, dass sie auch künftig gleich sein müssen oder gleich gehalten werden sollten. Wenn man zwei unterschiedliche Use Cases nur deshalb gewaltsam zusammenlegt, weil „der Code fast dupliziert ist“, entsteht mit der Zeit leicht eine Abstraktion, die gar nichts mehr abstrahiert.
      Wenn die Use Cases zu stark auseinanderdriften, verschiebt die Implementierung entweder viel Logik nach oben zum Aufrufer oder legt Unterschiede als Flags offen und hält intern zwei verschiedene Implementierungen nebeneinander. Ersteres ist eine flache Abstraktion mit geringem Wert, Letzteres ist weniger klar als zwei unabhängige Implementierungen.
    • Eine gut strukturierte 1000-Zeilen-Funktion würde ich jederzeit schlechtem Spaghetti-Code aus Hunderten kleiner Funktionen vorziehen.
  • Der Beispielcode ist so einfach, dass linearer Code natürlich besser lesbar ist, aber die Idee skaliert nicht gut.
    Man muss auch Wiederverwendbarkeit und Testbarkeit mit Unit Tests berücksichtigen, und wenn man den gesamten Code in eine einzige Funktion steckt, liegen alle lokalen Variablen im Scope – auch solche, die mit dem gerade gelesenen Codeblock vielleicht gar nichts zu tun haben. Das kann das Nachvollziehen erschweren.
    Wenn ich allerdings auf meine weniger erfahrene Zeit zurückblicke, habe ich oft völlig ordentlichen linearen Code zu stark modularisiert und ihn in weniger wartbaren Code verwandelt, bei dem man überall hin- und herspringen musste. Die zuerst geschriebene Form liegt näher am damaligen Gedankengang im Kopf, und Leser werden sie wahrscheinlich auch eher so interpretieren. Übermäßiges Refactoring kann diesen Vorteil zerstören.
    Letztlich ist Programmieren eher ein Handwerk, und Erfahrung hilft bei der situationsgerechten Entscheidung.

    • Eine der Funktionen, die bei der Arbeit die besten Reviews bekam, war ein 2000-Zeilen-Monster in linearem Stil mit 9 separaten Variablen-Scopes als Stufen.
      Sie hatte genau einen Zweck: einzelne HTML-Seiten, die in einer Ecke der App auf einer Plattform genutzt wurden, in ein Karussell umzuwandeln, das auf einer anderen Plattform ein natives Gefühl nachahmte. Sie war extrem spezifisch für diese Plattform und diesen Bereich der App.
      Man hätte die 9 Scopes jeweils zu Funktionen machen können, aber dann hätten Entwickler sie wiederverwenden wollen. Jede Stufe enthielt subtile Annahmen darüber, was in der vorherigen Stufe passiert war. Um daraus separate Funktionen zu machen, hätte man diese Annahmen erneut prüfen, verallgemeinern und verifizieren müssen, dass jede Methode unabhängig funktioniert. Es gab keinen Grund, diesen Aufwand in Code zu stecken, der fast nirgendwo anders gebraucht wurde.
      Das Debugging war nicht schwieriger, es gab End-to-End-Tests, und Zwischenzustände sind nicht aus der Funktion herausgeleakt. Tatsächlich haben im Laufe der Zeit 2 andere Entwickler Änderungen beigesteuert, es funktionierte gut, und es war schnell geschrieben.
      Linearer Code skaliert gut und löst Probleme. Er ist nicht immer die gewünschte Form, macht das Leben aber in mehr Situationen deutlich einfacher, als man denkt.
      Als ich das 2000-Zeilen-Monster zum ersten Mal sah, war meine Reaktion nicht positiv, aber nach 5 Minuten Hinsehen war es schwer, echte Mängel zu finden; mit ein paar Tests blieben nur Ängste, die sich in der Praxis nicht bestätigten.
    • Wo ist der Beleg dafür, dass in Funktionen aufgeteilter Code skaliert? Wenn die Gesamtkomplexität des Codes wächst, wächst auch der Teil mit, der in Dutzende Funktionen zerschnitten und unlesbar wird.
      Irgendwann merkt man, dass diese Dutzenden Funktionen in einer bestimmten Reihenfolge aufgerufen werden müssen und jeweils nur einmal verwendet werden. Am Ende zwingt man jeden, der diese Funktionen sinnvoll nutzen will, die magische Kombinationsreihenfolge zu kennen.
    • „Die Idee skaliert nicht“ ist falsch, und „Programmieren ist ein Handwerk, bei dem Erfahrung beim Urteilen in der jeweiligen Situation hilft“ ist richtig.
      Der zentrale Grund, warum riesige lineare Funktionen oft lesbarer und wünschenswert sind, liegt darin, dass man mehrere Konzepte und Beziehungen ohne Kontextwechsel als Ganzes gleichzeitig im Kopf behalten kann, was das Verständnis erleichtert. Ein extremer Vertreter ist Arthur Whitney, der Erfinder der Sprache K: Um möglichst viel auf einen Bildschirm zu bekommen, schreibt er extrem knappen Code, der für andere fast unverständlich ist.
      Ein persönliches Beispiel: Eine riesige Windows-Message-Processing-Funktion, also WndProc, mit Business-Logik in einem großen switch-Statement zu lesen, zu verstehen und zu debuggen, war für mich viel einfacher als die Visual-C++-Version, in der die Message-Handler in separate Funktionen aufgeteilt waren.
      Außerdem gab es bei Beispielcode für Mikrocontroller eine Version, in der das ADC-Beispiel komplett in einer Datei stand, und eine Version, die auf mehrere Dateien wie main.c, config.c, interrupts.c, timer.c usw. verteilt war. Obwohl es nicht einmal 200 Zeilen waren, war die zweite wegen der Kontextwechsel schwer zu verstehen.
    • Ich habe oft gesehen, dass Leute linearen Code, der nicht wiederverwendet werden wird, aus Gewohnheit in viele separate Funktionen auslagern.
      Solche Codefragmente werden meist zu private-Funktionen einer Klasse und haben Zustand. Als private-Funktionen sind sie in der Praxis auch schwer zu testen.
      Jetzt hat man jede Menge private-Funktionen, die nur einmal aufgerufen werden und meist nebenwirkungsbehafteten Zustand verändern. Wenn sie direkt neben dem Aufrufer stehen, ist das in einfachen Fällen noch lesbar, aber mit der Zeit fügt jemand zwischen der aufrufenden Funktion und der herausgezogenen Funktion weitere Funktionen ein.
      Dann verändern Codefragmente, von denen man ohne Blick auf den Call Graph oder Suche in der Klassendatei nicht weiß, woher sie aufgerufen werden, unterschiedliche nebenwirkungsbehaftete Zustände.
      Wenn man Code nichtlinear machen will, sollte man – sofern die Sprache es unterstützt – zumindest erwägen, die herausgezogene private-Funktion zu einer inneren Funktion der aufrufenden Funktion zu machen. Dann ist klar, dass sie nicht von anderswo aufgerufen wird.
      In echten Codebasen ist auch das keine Entweder-oder-Frage, sondern eher die Kunst, beides so zu kombinieren, dass es lesbar und wartbar bleibt.
    • Wenn eine Funktion wirklich linear ist, sind lange Funktionen gar nicht so schlimm. Das tatsächliche Beispiel ist aber nicht linear, sondern enthält mehrere Verzweigungen.
      Werden Leute all diese Verzweigungen testen? Oder schreiben sie nur einen Test, der eine Pizza hineinsteckt, und prüfen grob, ob es funktioniert? Mehrere Verzweigungen von außen zu testen ist meist umständlich und lästiger, als kleine, spezialisierte Funktionen zu testen; daher wirkt Letzteres wahrscheinlicher.
  • Die Aussage „linearer Code skaliert nicht“ ist eher das Gegenteil. Was in großen Codebasen zum echten Albtraum wird, sind kleine, knappe Funktionen mit tief verschachtelten Call Stacks
    Es ist nicht klar, wo neuer Code eingefügt werden soll, man muss alle Pfade nachverfolgen, über die der Code aufgerufen werden kann, und dadurch steigt die Schwierigkeit, die Auswirkungen von Änderungen zu erfassen, exponentiell; außerdem entstehen doppelte Unterroutinen
    In 99 % der Fälle hat man keine gute Abstraktion geschaffen, also ist es besser, einfach linearen Code zu schreiben. Copy/Paste ist mir lieber als fragwürdige Funktionssemantik

    • Eine weitere Gefahr ist: Wenn man print_table() hinzufügt, findet es jemand und verwendet es in seinem eigenen Code und hängt ein kleines Flag an, um die Ausgabe an seinen Use Case anzupassen
      12 Monate später sieht es so aus:
      print_table(
      rows,
      headers = None,
      is_unicode = False,
      left_align = False,
      align = [],
      remove_emoji = None,
      max_width = 80,
      potato_mode = 7,
      _debug_frontend = not FLAGS.dont_debug,
      ellipsis_for = 0,
      no_print = False,
      )
    • Das beschreibt ein Lesbarkeitsproblem und sagt im Kern, dass Lesbarkeit der Skalierbarkeit schadet
      Wenn man die beiden Konzepte orthogonal betrachtet, abgesehen davon, dass Lesbarkeit die Skalierbarkeit beeinflussen kann, skaliert linearer Code nicht so gut wie modularer Code. Diese Dichotomie ist es wert, sie zu kennen und je nach Situation zu berücksichtigen
      Trotzdem stimme ich weiterhin nicht zu. Wenn kleine Funktionen pure functions sind, verursachen sie keine Lesbarkeitsprobleme. Das bedeutet, dass sie keinen Zustand anfassen, keine Logik in den Code injizieren, und dass Dependency Injection sowie das Übergeben von Funktionen an andere Funktionen ausdrücklich minimiert werden sollten
      Wenn man eine Pipeline aus pure functions baut, die nur Daten weiterreichen, wird sie gut lesbar und erweiterbar. Es kommt viel seltener vor, dass man wegen Designfehlern Logik neu schreiben muss, und wenn man pure functions kombiniert, wird Code wie Lego. Refactoring ähnelt dann eher dem Umordnen und Neukombinieren bestehender primitiver Bausteine
  • Der Beispielcode wäre weniger ablenkend gewesen, wenn er wenigstens versucht hätte, die Pizza-Metapher sinnvoll beizubehalten, oder wenn es kein schwacher Go-Code gewesen wäre
    prepare ist ein furchtbarer Funktionsname. Ein erfahrener Gopher hätte vermutlich einen Namen wie NewPizzaFromOrder gewählt
    Ich sehe keinen Grund, addToppings als separate Funktion zu haben. Wenn es unbedingt nötig wäre, würde ich persönlich daraus eine Methode von Pizza machen, etwa func (p *Pizza) WithToppings(topping ...Topping) *Pizza { /* ... */ }. Eine echte Pizza ist veränderlich, also verändert die Methode den Receiver
    Ich verstehe auch nicht, warum bei jeder Pizza ein neuer Ofen instanziiert wird. Man sollte mit einem vorhandenen Ofen beginnen, oven.Preheat() ausführen und oven.Bake(pizza) aufrufen. Noch besser könnte oven.Preheat() einen neuen Typ von Oven zurückgeben, der .Bake() exponiert, sodass der Fehler, ohne Vorheizen zu backen, schon zur Compile-Zeit verhindert wird. An anderer Stelle könnte es ein Baker-Interface geben und vielleicht eine ToasterOven-Implementierung, bei der Vorheizen nicht so wichtig und daher nicht nötig ist
    Auch ohne den Code zu ändern, hätte ich die Reihenfolge der Deklarationen so umgestellt, dass sie dem erwartbaren Ablauf entspricht. Dann muss man beim Überfliegen der Funktionen, die sich gegenseitig aufrufen, nicht auf der Seite hoch- und runterspringen
    Ich höre hier auf, weil ich keine Zeit habe, aber dieser Code ist schon ein viel zu schlechtes Beispiel, um überhaupt eine Debatte darüber zu beginnen, „welche Seite leichter zu lesen ist“

  • John Carmack hat fast dasselbe gesagt, und seitdem halte ich mich daran. Linearer Code ist natürlich leicht zu lesen, weil er der Ausführungsreihenfolge folgt, und minimiert Blicksprünge
    Manche Codeabschnitte müssen für Wiederverwendung nichtlinear sein, und dann wird die Ausführung zu einem Graphen. Wenn der Code die Wiederverwendung einer Graphstruktur nicht nutzt, muss man keinen Knoten einführen, wo eine einzige Kante ausreicht
    http://number-none.com/blow/blog/programming/2014/09/26/carm...

    • Was Carmack gesagt hat, aber im Originaltext nicht vorkommt: Wenn man Logik ohne Nebenwirkungen in eine separate Funktion auslagern kann, ist das meist eine gute Idee
      In diesem Fall wäre der Code links wohl besser gewesen, wenn er etwas wie pizza.Toppings = get_pizza_toppings(order.kind) gemacht hätte; dann wäre die Änderung an der Pizza in der Hauptfunktion im Mittelpunkt geblieben
  • Ich stimme bis zu einem gewissen Grad zu, dass linearer Code leichter zu lesen ist, aber das allein macht ihn noch nicht zu guter Code-Praxis.
    Guten linearen Code halte ich zwar für leichter lesbar, aber Wartbarkeit und Testbarkeit sind deutlich schlechter. Ich habe jahrzehntelange Erfahrung und begutachte auch CS-Studierende extern; über viele Jahre hinweg war die einzige gute Praxis, die ich in der Realität sicher gesehen habe: Funktionen klein halten.
    Ich mag Abstraktionen nicht besonders, und ich glaube auch nicht, dass man Code-Duplikation um jeden Preis vermeiden muss. Aber wenn man Funktionen so weit wie möglich auf einen einzelnen Zweck zuschneidet, wird das zukünftige Ich einem dankbar sein.
    Wenn Code wie im Beispiel zehn Jahre lang in Produktion läuft, ändern sich einzelne Abschnitte. Mit Glück werden auch die Kommentare aktualisiert, meistens aber nicht. Unit-Tests werden groß und schwer handhabbar, dabei nach und nach schlampiger, und jemand kann vergessen, einen Teil eines Tests anzupassen, der nicht offensichtlich mit der Änderung zusammenhängt. Auch der Code selbst wird mit der Zeit sehr wahrscheinlich weniger gut lesbar. Nicht aus Absicht oder Unfähigkeit, sondern aus menschlichen Gründen wie Zeitdruck.
    In einer perfekten Welt müssten wir Concerns nicht trennen, aber wir leben in einer unvollkommenen Welt. Je kleiner Funktionen sind und je weniger Verantwortung sie tragen, desto leichter lässt sich diese Unvollkommenheit über die Zeit handhaben.

    • Stimmt, es ist weniger gut testbar, aber in diesem Fall geht es um Zustandsänderungen, die in einer bestimmten Reihenfolge ausgeführt werden müssen.
      Wenn man ein Objekt durch eine Abfolge bestimmter Zustände schleust, sollte man das meiner Meinung nach entweder aufteilen und die Übergänge über Typen ausdrücken, oder es als eine große Funktion schreiben. Wenn zum Beispiel bakePizza eine RawPizza entgegennimmt und eine BakedPizza zurückgibt, lässt sich die Aufrufreihenfolge zur Compile-Zeit erzwingen.
      Aus Gründen der Lesbarkeit, Korrektheit und Testbarkeit bevorzuge ich Ersteres. In den meisten Programmiersprachen muss man jedoch ein neues Objekt erstellen, um den Objekttyp zu ändern, was Laufzeitkosten verursacht. In einem heißen Codepfad ist In-place-Mutation sinnvoll, und in diesem Fall ist es besser, alles in einer linearen Funktion zu belassen.
    • Ich habe vor Kurzem angefangen, Sussmans Software Design for Flexibility zu lesen, und das knüpft direkt daran an.
      https://mitpress.mit.edu/9780262045490/
  • Eine einschlägige E-Mail von John Carmack: http://number-none.com/blow/blog/programming/2014/09/26/carm...
    Diskussion: https://news.ycombinator.com/item?id=12120752

  • Stimme stark zu. Früher war ich im gegnerischen Lager.
    Die grundlegende Spannung liegt hier zwischen Locality of Behaviour auf der einen Seite und dem Wunsch, auf der anderen Seite eine High-Level-„Inhaltsverzeichnis“-Ansicht klar sichtbar zu machen. Für lesbaren Code ist Lokalität wichtiger. Wie im Text gesagt, lässt sich die Inhaltsverzeichnis-Perspektive mit Abschnittskommentaren ausreichend deutlich machen.
    Es gibt noch einen wichtigeren Grund, linearen Code zu bevorzugen. Wenn man sich durch eine gesamte Codebase bewegt, ist es viel einfacher, wenn „Brocken“, also Funktionen, Klassen oder sprachlich erzwungene Einheiten, grob den Business-Use-Cases entsprechen. Andernfalls wird der Suchraum zu groß, und man muss sich aus den Einzelteilen selbst das Ganze rekonstruieren. Die Codestruktur sollte diese Arbeit übernehmen.
    Wenn mehrere „Dinge“ alle zu einer einzigen Aufgabe gehören, etwa Registrierung oder Kauf, sollte man sie auch im Code zusammenlassen. Das ist viel leichter zu finden und zu ändern. Nur wenn Wiederverwendung nötig ist, sollte man in Unterfunktionen aufteilen, nicht bloß zur Organisation.
    [0] https://htmx.org/essays/locality-of-behaviour/

    • Ich bin in die entgegengesetzte Richtung gegangen. Früher war ich im Lager des linearen Codes, heute bin ich für mehr Funktionen.
      Der wichtigste Grund ist Zustand. Je länger eine Funktion ist, desto größer ist der Scope lokaler Variablen. Überall in der Funktion kann jede Variable geändert werden, und der Datenfluss ist nicht sofort klar. Mit mehr Funktionen bleibt der Scope klein, und der Datenfluss ist expliziter.
      Als Nebeneffekt gibt es auch weniger Einrückung.
      Gleichzeitig mag ich keine zu kleinen Funktionen, weil es dann schwer wird, herauszufinden, wo die eigentliche Arbeit passiert.
    • Wie hältst du es mit Tests bei der Aussage „Nur wenn Wiederverwendung nötig ist, sollte man in Unterfunktionen aufteilen, nicht bloß zur Organisation“? Was ist damit, den Zustand zu reduzieren, den man im Kopf behalten muss? Ressourcenfreigabe? Verständnis der Auswirkungen von Änderungen?
      Stell dir eine Tagesabschlussverarbeitung mit 10 nicht wiederverwendbaren Schritten vor, die der Reihe nach ausgeführt werden müssen und jeweils 100 Zeilen lang sind. Jeder Schritt verwendet Daten, die den vorherigen ähneln, aber nicht identisch sind. Würdest du wirklich eine einzelne Funktion mit 1000 Zeilen wählen?
  • Beides liest sich linear. Die Version mit herausgezogenen kleinen Funktionen hat oben auf der Seite ein Inhaltsverzeichnis und fasst den Datenfluss zwischen den Schritten zusammen. Wenn man das Ganze lesen will, wirkt das wie eine ansprechende Lesereihenfolge.
    Um diese Lesbarkeit zu erhalten, muss man allerdings auch die Position der Funktionen verschieben, wenn sich die Reihenfolge der Schritte ändert. Wenn es private-Funktionen sind und sie nur aus dem Inhaltsverzeichnis heraus aufgerufen werden, ist das in Ordnung. Aber nichts erzwingt, dass die Reihenfolge erhalten bleibt, und nichts zwingt einen, an den gesamten Lesefluss zu denken.
    Sobald Funktionen wiederverwendet werden, lässt sich das oft nicht mehr linearisieren. Manchmal geben Leute dann auf und sortieren alphabetisch, oder es wird einfach zufällig.

  • Erfahrungsgemäß glauben manche: Je vertrauter jemand mit dem Code ist, desto eher hält er es für den richtigen Weg, Code in kleine Funktionen zu schieben.
    Weil diese Person das mentale Modell dieses Codes bereits aufgebaut hat, ist für sie die sauberste Implementierung eine mit sehr wenigen Zeilen.
    Wenn aber die nächste Person dazukommt, muss sie ohne den ursprünglichen Kontext hin und her springen und ihren gedanklichen Stack pushen/poppen, um dasselbe mentale Modell aufzubauen — und das ist deutlich schwieriger.

    • Wenn der Code Sinn ergibt, ist das nicht so. Bei gut geschriebenem Code mit eleganter Abstraktion, schlanken Interfaces und angemessener Dokumentation muss man nicht so viel hin und her springen.
      Wie oft liest man zum Beispiel den Sourcecode der Standardbibliothek der verwendeten Sprache? Fast nie; normalerweise schaut man sich die Methodensignatur an und liest die Dokumentation, wenn es etwas komplexer oder neu ist.
      Der Kern eines Interfaces ist, dass man sich nicht darum kümmern muss, wie eine Methode implementiert ist, sondern nur darum, was sie tut. Das wird durch eine Kombination aus Kontext, Namen und Dokumentation erklärt. Viele Entwickler verstehen das jedoch nicht oder kümmern sich nicht darum und schreiben daher Code, der keinen Sinn ergibt — egal ob linear oder modular.
      Wenn man zum Beispiel in einer Service-Klasse eine Methode aufrufen muss, um bestimmte Daten zu bekommen, eine andere Methode für weitere Daten und eine dritte Methode für Daten, die mit den beiden ersten kombiniert werden müssen — was ist dann die Bedeutung dieses Service? Damit legt man die gesamte interne Komplexität nach außen offen.
      Es geht nicht darum, kleine Methoden zu erzwingen. Zwanzig Fünfzeiler-Funktionen, die jeweils nur einmal aufgerufen werden, eine sehr spezifische Aufgabe erledigen und in der richtigen Reihenfolge aufgerufen werden müssen, ergeben keinen Sinn. Das ist kein sauberer Code, sondern eher Cargo-Cult-Programmierung.
      Entscheidend ist, angemessen zu abstrahieren, sodass der Code sowohl für neue als auch für erfahrene Teammitglieder Sinn ergibt, leicht nachvollziehbar ist und Komplexität an der passenden Stelle verborgen wird. Das ist nicht einfach, aber möglich.
    • Ich stimme nicht zu, aber vielleicht gibt es einen Unterschied zwischen Menschen, die bottom-up lesen und denken, und solchen, die top-down denken.
      Mein Sohn war durchaus klug, hatte aber Schwierigkeiten in der Schule, und einer von mehreren Fachleuten erklärte, dass Schulen meist bottom-up unterrichten, mein Sohn aber ein sehr stark top-down orientierter Lerner sei. Er braucht zuerst einen Überblick, bevor er in Details einsteigt; andere erfassen zuerst die Details und setzen daraus den Überblick zusammen. Schulen unterrichten normalerweise für die zweite Gruppe.
      Unter Programmierern könnte es ähnliche Unterschiede geben.
    • „Die nächste Person muss hin und her springen“ gilt nur, wenn diese Person den Code nicht lesen kann. Code sollte zumindest anfangs so gelesen werden, wie er geschrieben ist; zu versuchen, ihn in der Reihenfolge zu lesen, in der er ausgeführt wird, ist der falsche Ansatz.
      Wenn der vorherige Entwickler eine Funktion BakePizza geschrieben hat, kann man davon ausgehen, dass die Pizza richtig gebacken wird, und zur nächsten Zeile weitergehen. Wenn man beim Versuch, den Betrieb eines Restaurants zu verstehen, in Details wie der Ofentemperatur stecken bleibt, versteht man am Ende weder, wie das Restaurant funktioniert, noch behält man die genaue Ofentemperatur.
    • Deshalb brauchen wir bessere Werkzeuge wie projektionsbasierte Code-Editoren.
      Der Editor sollte einen Toggle haben, mit dem Funktionen temporär inline angezeigt werden können. Dann muss man nicht mehr hin und her springen.