4 Punkte von GN⁺ 2024-10-30 | 1 Kommentare | Auf WhatsApp teilen
  • Sobald Code geschrieben wird, entstehen Wartungskosten; deshalb ist eine Struktur, die sich später leicht löschen oder ersetzen lässt, oft wichtiger als Wiederverwendbarkeit
  • Je mehr Nutzer eine API hat, desto höher werden die Änderungskosten; je tiefer die Abhängigkeit von Third-Party-APIs ist, desto stärker erschüttern externe Änderungen die Codebasis
  • Duplikation, Boilerplate, Layering, große Codeblöcke, Modultrennung und Feature Flags werden je nach Situation zu Werkzeugen des Abhängigkeitsmanagements
  • Gute Trennung bedeutet weniger, gemeinsame Funktionen zu bündeln, sondern eher, Designentscheidungen, die schwer zu ändern sind oder sich wahrscheinlich ändern werden, voreinander zu verbergen
  • Guter Code ist nicht von Anfang an perfekter Code, sondern Legacy-Code, der mit der Zeit weniger im Weg steht – letztlich also Code, der leicht zu löschen ist

Code ist Kosten, und Löschen senkt Kosten

  • Jeder Code erzeugt in dem Moment, in dem er geschrieben wird, Wartungskosten; Wiederverwendung kann zwar die Menge an Code reduzieren, kann es aber später erschweren, die Meinung zu ändern
  • Je mehr Code eine API nutzt, desto mehr Umschreibaufwand bringt eine Änderung der API mit sich
  • Je stärker man von einer Third-Party-API abhängt, desto größer ist auch die Auswirkung, wenn sich diese API ändert
  • In großen Systemen wird es mit der Zeit zu einem immer schwierigeren Problem, wie Code miteinander verzahnt ist und welche Teile von welchen anderen Teilen abhängen
  • Wenn man Codezeilen nicht als „produzierte Zeilen“, sondern als „entstandene Kosten“ betrachtet, dann senkt das Löschen von Code die Wartungskosten
  • Das Ziel ist nicht nur, wiederverwendbare Software zu bauen, sondern wegwerfbare Software

Schritt 0: Keinen Code schreiben

  • Die Anzahl der Codezeilen sagt nicht alles, aber Größenordnungen wie 50, 500, 5.000, 10.000 oder 25.000 Zeilen sind wichtig
  • Ein Monolith mit einer Million Zeilen braucht mehr Zeit, Geld und Aufwand, um ersetzt zu werden, als ein Monolith mit 10.000 Zeilen
  • Je mehr Code es gibt, desto schwerer ist er loszuwerden; aber eine einzelne Codezeile einzusparen spart fast nichts
  • Der am leichtesten zu löschende Code ist der Code, der gar nicht erst geschrieben wurde

Schritt 1: Copy-and-paste verwenden

  • Wiederverwendbarer Code lässt sich leichter erstellen, nachdem mehrere Anwendungsfälle tatsächlich entstanden sind, statt ihn im Voraus für zukünftige Nutzungen vorherzusagen
  • Wenn man in einer Codebasis ein paarmal Copy-and-paste macht, versteht man die tatsächlichen Nutzungsmuster besser
  • In dem Moment, in dem man Code zu einer gemeinsamen API macht, wird dieser Code schwerer zu ändern
  • Code, der eine Funktion aufruft, hängt nicht nur vom dokumentierten Verhalten ab, sondern auch von beabsichtigtem und unbeabsichtigtem Verhalten, das in der Implementierung beobachtbar ist
  • Code innerhalb einer Funktion zu löschen ist einfacher, als die Funktion selbst zu löschen

Schritt 2: Mit Copy-and-paste aufhören

  • Code, der sich oft genug wiederholt, ist bereit, in eine Funktion gehoben zu werden
  • Dazu gehört häufig benötigter Utility-Code oberhalb der Standardbibliothek, etwa Code, der eine Konfigurationsdatei öffnet und eine Hash-Tabelle zurückgibt, oder Code zum Löschen von Verzeichnissen
  • util sollte eher ein Verzeichnis als eine einzelne Datei sein, und verschiedene Utilities sollten in verschiedenen Dateien liegen
    • Eine einzelne util-Datei wächst immer weiter, und wenn sie zu groß geworden ist, lässt sie sich schwer aufteilen
  • Code, der weniger stark auf eine Anwendung oder ein Projekt zugeschnitten ist, lässt sich leichter wiederverwenden und wird mit geringerer Wahrscheinlichkeit geändert oder gelöscht
    • Dazu gehört Library-Code für Logging, Third-Party-APIs, File Handles oder Prozesse
    • Listen, Hash-Tabellen und Collections werden nicht nur wegen ihrer einfachen Interfaces selten gelöscht, sondern auch, weil ihr Umfang mit der Zeit nicht immer weiter wächst
  • Der Kern ist, schwer zu löschende Teile so weit wie möglich von leicht zu löschenden Teilen fernzuhalten

Schritt 3: Mehr Boilerplate schreiben

  • Wenn man eine Library baut, kann man Copy-and-paste vermeiden; in der Praxis schreibt man aber oft viel Boilerplate, um die Library zu verwenden
  • Boilerplate ähnelt Copy-and-paste darin, dass man jedes Mal leicht andere Stellen ändert
  • Diese Art von Duplikation nimmt Ausführlichkeit in Kauf, um Abhängigkeiten zu reduzieren und Flexibilität zu gewinnen
  • Libraries, die Boilerplate erfordern, treten oft dort auf, wo sich Policy und Protokoll schwer vermischen lassen, etwa bei Netzwerkprotokollen, Wire-Formaten oder Parsing-Tools
    • Protokolle betreffen das, was ein Programm tun kann
    • Policy betrifft das, was ein Programm tun soll
  • Solcher Code ist oft eine Anforderung, um mit anderen Computern zu kommunizieren oder andere Dateien zu verarbeiten, und ist deshalb schwer zu löschen
  • Wichtig ist, Business-Logik nicht in solchen Code zu verstreuen
  • Selbst wenn man mehr Zeilen schreibt, ist es besser, diese Zeilen in den leicht zu löschenden Teilen zu schreiben

Schritt 4: Keine Boilerplate schreiben

  • Wenn die Boilerplate zu viel wird, ist es Zeit, um eine flexible Library herum eine Library zu bauen, die eine Meinung zu Policy, Workflow und Zustand hat
  • Eine einfach zu nutzende API zu bauen, ähnelt dem Library-ifizieren von Boilerplate
  • Der Python-HTTP-Client requests ist ein Beispiel dafür, wie auf dem ausführlicheren urllib3 ein einfaches Interface bereitgestellt wird
    • requests deckt übliche Workflows bei der HTTP-Nutzung ab und verbirgt praktische Details
    • urllib3 bietet Pipelining, Verbindungsverwaltung usw. und verbirgt Details nicht vor den Nutzern
  • Eine Library mit einer anderen Library zu umhüllen bedeutet nicht nur, Details zu verbergen, sondern ist Separation of Concerns
  • In ein util-Verzeichnis sollte keine Business-Logik; besser ist es, auf einfach zu implementierenden Libraries nutzerfreundliche Libraries aufzubauen
  • Manchmal ist es auch sinnvoll, Third-Party-Libraries zu umhüllen
    • So kann man eine Library bauen, die zum eigenen Code passt, ohne das gesamte Projekt auf eine bestimmte Entscheidung festzulegen
  • Eine angenehm nutzbare API und eine erweiterbare API stehen oft im Konflikt miteinander
  • Layering bedeutet weniger, Code zu schreiben, den man später löscht, sondern eher, schwer zu löschenden Code nutzerfreundlich zu machen, ohne ihn mit Business-Logik zu verunreinigen

Schritt 5: Große Codeblöcke schreiben

  • Auch mit Copy-and-paste, Refactoring, Layering und Komposition muss Code am Ende etwas tun; manchmal braucht man deshalb einen großen Codeblock, der den Rest zusammenhält
  • Business-Logik kann durch endlose Edge Cases und schnelle Hacks geprägt sein
  • Auch Game-Code oder Gründer-Code kann man als dieselbe Art von Code sehen, der Abkürzungen nimmt, um erheblich Zeit zu sparen
  • Manchmal ist es einfacher, einen großen Fehler zu löschen, als 18 kleine, miteinander verflochtene Fehler zu beseitigen
  • Viel Programmieren ist explorativ; statt von Anfang an richtig zu liegen, kann es schneller sein, ein paarmal falsch zu liegen und zu iterieren
  • Wenn man sein erstes Spiel baut, sollte man nicht mit der Engine anfangen; und man sollte kein Web-Framework bauen, bevor man eine Anwendung schreibt
  • Ein Monorepo ist ein ähnlicher Trade-off
    • Es ist schwer, im Voraus zu wissen, wie Code aufgeteilt werden sollte, und ein großer Fehler ist leichter zu deployen als 20 stark gekoppelte
  • Wenn man weiß, dass Code bald weggeworfen, gelöscht oder leicht ersetzt wird, kann man mehr Abkürzungen nehmen
  • Das Ziel ist nicht, denselben Klumpen Schlamm zehnmal zu wiederholen und den Fehler zu perfektionieren, sondern jedes Mal neue Fehler zu machen, neue Risiken einzugehen und durch Iteration aufzubauen
  • Projekte scheitern am Ende oder werden zu Legacy-Code; Scheitern kommt häufiger vor als Erfolg
  • Es ist einfacher, Code als Ganzes zu löschen, als ihn Stück für Stück zu löschen

Schritt 6: Code in Stücke teilen

  • Ein großer Klumpen Schlamm ist am einfachsten zu bauen, hat aber die höchsten Wartungskosten
  • Eine scheinbar einfache Änderung kann dazu führen, dass fast jede Stelle der Codebasis notdürftig angefasst werden muss
  • Code, der als Ganzes leicht zu löschen war, wird in Einzelteilen schwer zu löschen
  • Module sollte man besser nicht nach gemeinsamer Funktionalität aufteilen, sondern nach dem, was sie nicht mit dem Rest teilen, und nach Designentscheidungen, die verborgen werden müssen
  • Wie bei D. Parnas’ Kriterium kann man schwierige oder wahrscheinlich veränderliche Designentscheidungen auflisten und jedes Modul so entwerfen, dass es solche Entscheidungen vor anderen Modulen verbirgt
  • Module werden nicht für Wiederverwendung gebaut, sondern für Veränderbarkeit
  • Das Single-Responsibility-Prinzip kann man als „Jedes Modul sollte nur ein schwieriges Problem behandeln“ verstehen; wichtiger ist aber: „Jedes schwierige Problem sollte nur in einem Modul behandelt werden“
  • Wenn ein Modul zwei Dinge tut, liegt das oft daran, dass man beim Ändern eines Teils auch den anderen ändern muss
  • Eine schreckliche Komponente mit einfachem Interface kann leichter sein als zwei Komponenten, die sorgfältige Abstimmung erfordern

Lose Kopplung und gemeinsame Interfaces

  • Ein System, in dem man Teile löschen kann, ohne andere Teile neu zu schreiben, wird meist lose gekoppelt genannt
  • Lose Kopplung ist ein Zustand, in dem man nicht zu viel Code ändern muss, wenn man seine Meinung ändert
  • Eine Variable einmal hart zu codieren oder statt einer Variable ein Command-Line-Flag zu verwenden, kann je nach Fall bereits lose Kopplung sein
  • Microsoft Windows erreicht dieses Ziel, indem es externe und interne APIs trennt
    • Externe APIs sind an den Lebenszyklus von Desktop-Programmen gebunden
    • Interne APIs sind an den zugrunde liegenden Kernel gebunden
    • Wenn man APIs verbirgt, kann man Flexibilität gewinnen, ohne viel Software zu beschädigen
  • Auch HTTP liefert ein Beispiel für lose Kopplung
    • Man kann einen Cache vor einen HTTP-Server setzen
    • Wenn man Bilder auf ein CDN verschiebt und nur die Links ändert, geht der Browser nicht kaputt
    • HTTP-Fehlercodes geben gemeinsamen Problemen eindeutige Codes, sodass Clients viele Fehler stellvertretend behandeln können
  • Die Art der Fehlerbehandlung sollte mitbedacht werden, wenn Code in kleine Stücke geteilt wird

Fehlerbehandlung und Kopplung

  • Erlang/OTP verwendet mit Supervisor Trees eine vergleichsweise einzigartige Art, Fehler zu behandeln
  • Jeder Prozess in einem Erlang-System wird ungefähr von einem Supervisor gestartet und überwacht
    • Wenn ein Prozess ein Problem hat, wird er beendet
    • Wenn ein Prozess beendet wird, startet der Supervisor ihn neu
    • Der Supervisor wird von einem Bootstrap-Prozess gestartet; wenn der Supervisor ausfällt, startet der Bootstrap-Prozess ihn neu
  • Die Kernidee ist, dass es schneller ist, schnell zu scheitern und neu zu starten, als Fehler zu behandeln
  • Vorübergehende Störungen lassen sich manchmal dadurch eindämmen, dass man etwas aus- und wieder einschaltet
  • Fehlerbehandlung und Recovery sollten besser in den äußeren Schichten der Codebasis stattfinden; das ist als End-to-End-Prinzip bekannt
  • Es ist einfacher, Fehler an den beiden Enden einer Verbindung zu behandeln als in der Mitte; selbst wenn man sie innen behandelt, ist am Ende trotzdem eine Prüfung auf oberster Ebene nötig
  • Fehlerbehandlung ist eine von mehreren Arten, wie Systeme stark gekoppelt werden

IMAP, Dateisysteme, SQL, Middleware

  • IMAP ist schmerzhaft bei der Fehlerbehandlung, weil fast jede Operation eine Ausnahmeform mit eigenen Optionen und eigener Verarbeitung ist
  • In IMAP können Fehler mitten zwischen anderen Operationsergebnissen auftauchen
  • Statt UUIDs werden eindeutige Tokens erzeugt, um einzelne Nachrichten zu identifizieren; auch diese Tokens können sich mitten zwischen Operationsergebnissen ändern
  • Viele IMAP-Operationen sind nicht atomar
  • Es dauerte mehr als 25 Jahre, bis es eine zuverlässige Methode gab, E-Mails von einem Ordner in einen anderen zu verschieben
  • Es gibt außerdem eine spezielle UTF-7-Codierung und eine eigene base64-Codierung
  • Dateisysteme und Datenbanken sind bessere Vergleichsfälle für Remote Storage
    • Dateisysteme haben eine feste Menge von Operationen und mehrere Objekte
    • SQL wirkt wie ein breiteres Interface als ein Dateisystem, folgt aber dem Muster mehrerer Operationen auf Mengen und mehrerer Zeilen
  • Datenbanken lassen sich nicht immer beliebig austauschen, aber es ist leichter, etwas zu finden, das mit SQL funktioniert, als mit einer selbstgebauten Query Language
  • Twitters Finagle nutzt eine gemeinsame API für Services, damit Timeout-Behandlung, Retry-Mechanismen und Authentifizierungsprüfungen leicht zu Client- und Server-Code hinzugefügt werden können
  • Gute Beispiele für lose Kopplung sind oft auch Beispiele für ein einheitliches Interface
  • Eine gesunde Codebasis muss nicht perfekt modularisiert sein, aber zwischen den beweglichen Teilen sollte genug Abstand liegen
  • Lose gekoppelter Code ist nicht zwangsläufig leicht zu löschen, aber er ist deutlich leichter zu ersetzen und zu ändern

Schritt 7: Weiter Code schreiben

  • Wenn man neuen Code schreiben kann, ohne alten Code anzufassen, ist es viel leichter, neue Ideen auszuprobieren
  • Entscheidend ist nicht, ob es Microservices oder ein Monolith sind, sondern dass man ein oder zwei Experimente auf das System setzen kann, während man herausfindet, was es tun soll
  • Feature Flags sind eine Möglichkeit, später die Meinung ändern zu können
  • Feature Flags erlauben nicht nur Experimente mit Funktionen, sondern auch, Änderungen auszurollen, ohne Software neu zu deployen
  • Google Chrome stellte fest, dass im regelmäßigen Release-Zyklus das Zusammenführen langlebiger Feature-Branches der schwierigste Teil war
  • Wenn man neuen Code ohne Neukompilierung ein- und ausschalten kann, lassen sich große Änderungen in kleine Merges zerlegen und bestehender Code muss nicht beeinflusst werden
  • Wenn neue Funktionen früher in derselben Codebasis auftauchen, wird klarer sichtbar, welche Auswirkungen langfristige Feature-Entwicklung auf andere Teile hat
  • Feature Flags sind nicht bloß Command-Line-Switches, sondern eine Methode, Feature-Releases vom Branch-Merge und vom Code-Deployment zu entkoppeln
  • Wenn das Deployment neuer Software Stunden, Tage oder Wochen dauern kann, wird die Fähigkeit, zur Laufzeit die Meinung zu ändern, wichtiger

Guter Code ist Legacy-Code, der nicht im Weg steht

  • Wichtiger als die Tatsache, dass man iteriert, ist eine Feedback-Schleife zu haben
  • Entscheidend ist, Komponenten für Änderungen zu isolieren, statt Module für Wiederverwendung zu bauen
  • Auf Veränderungen zu reagieren umfasst nicht nur die Entwicklung neuer Funktionen, sondern auch das Entfernen alter Funktionen
  • Erweiterbaren Code zu schreiben bedeutet, zu hoffen, dass die ursprüngliche Entscheidung auch in drei Monaten noch richtig war
  • Löschbarer Code geht von der gegenteiligen Annahme aus
  • Layering, Isolation, gemeinsame Interfaces und Komposition sind Methoden, um Software zu bauen, die sich mit der Zeit ändern kann, nicht unbedingt gute Software an sich
  • Man muss nicht alles wegwerfen, aber manches muss gelöscht werden
  • Guter Code ist nicht Code, der von Anfang an richtig war, sondern Legacy-Code, der nicht im Weg steht
  • Guter Code ist Code, der leicht zu löschen ist

1 Kommentare

 
GN⁺ 2024-10-30
Meinungen auf Hacker News
  • Eine meiner Lieblingsaussagen ist: Einfachheit ist Robustheit.
    Ähnlich wie Lehmans Gesetz der fortlaufenden Veränderung bedeutet das: Je geringer die Komplexität eines Systems ist, desto leichter lässt es sich ändern.
    Statt Code zu schreiben, der für die Zukunft erweiterbar ist, halte ich es für besser, mit intuitivem Code für die Zukunft gerüstet zu sein.
    Zum Beispiel abstrahiert man nur dann, wenn es wirklich nötig ist, lässt einfache Duplikation zu, beginnt zunächst mit einem Monolithen und setzt vor horizontaler Skalierung zuerst auf vertikale Skalierung.
    Ich habe mehrere 0→1-Systeme gebaut, und der gemeinsame Verlauf ging immer in diese Richtung.
    https://en.m.wikipedia.org/wiki/Lehman%27s_laws_of_software_...

    • Stimmt, aber wenn man das Prinzip Einfachheit ist Robustheit anwendet, muss man auch inhärente Komplexität verstehen.
      Code wird nicht robuster, nur weil man Randfälle nicht behandelt – egal, wie viel einfacher er dadurch auch wirken mag.
    • Meine Regel lautet: Beim ersten Mal einfach schreiben, beim zweiten Mal kopieren, beim dritten Mal Refactoring in Betracht ziehen.
    • Ich stimme zu, bin mir aber nicht sicher, ob die Formulierung simple is robust intuitiv genug ist.
      Sie eröffnet die Debatte darüber, was „Einfachheit“ ist und wie sie auf Systeme angewendet wird – eine Frage, die komplex genug ist, dass Rich Hickey sie behandelt hat.
      Vielleicht trifft „Dumm ist robust“ oder „Direkt ist robust“ die Absicht besser.
    • Ich stimme dem sehr stark zu. Viel zu viel Müll in Software entsteht beim Versuch, eingebildete Probleme zu lösen.
      Schreib einfach Code, der das tut, was nötig ist. Erfinde keine hypothetischen Skalierungsprobleme, baue keine cleveren Abstraktionen, nur um klug zu wirken, und schreib einen Monolithen, den du auf eine VM packst – dann kann er direkt in Produktion gehen.
      Wenn Probleme auftauchen, löst du sie dann; idealerweise erst, wenn der Cashflow positiv ist.
      Warum sorgt sich ein „Airbnb für Hunde“-Startup mit 0 Nutzern um C100K? Hat AWS dich wohl zu Serverless überredet, weil es in deinem Interesse war – oder um Geld aus dir herauszuholen?
    • Die Komplexität der Geschäftslogik verschwindet nicht, nur weil man es sich wünscht. Wenn sie riesig und miteinander verflochten ist, wird der Code es ebenfalls sein.
  • Verwandte Beiträge:
    Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=24989351 - Nov 2020 (30 comments)
    Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=23914486 - July 2020 (109 comments)
    Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=18761739 - Dec 2018 (2 comments)
    Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=11093733 - Feb 2016 (133 comments)

  • Kurz gesagt, wenn ich die Fehler zusammenfasse, die ich in jungen Jahren gemacht habe: Inzwischen glaube ich im Gegenteil an Design fürs Löschen.
    Früher dachte ich, ich könnte ein großartiges Kunstwerk schaffen, das alle Situationen vorhersieht und alle Anforderungen erfüllt. Aber niemand kann zukünftige Anforderungen so gut vorhersagen.
    Irgendwann wird das, was ich gebaut habe, für jemanden „dieser dumme Kram“ sein, und egal, wie stolz ich heute darauf bin: Es kann völlig berechtigt sein, dass sie alles einreißen.
    Deshalb ist es besser, Energie darauf zu verwenden, Dinge leicht entfernbar zu machen. Das reduziert oft die Kopplung, aber wichtig ist: Es ist nicht dasselbe wie die Entkopplung eines enthusiastischen jungen Entwicklers, der alles in ein meta-konfigurierbares Framework auslagern will.
    Manchmal ist eine leicht verständliche starke Kopplung besser.
    https://news.ycombinator.com/item?id=41219130

    • Man kann Dinge zwar leicht entfernbar machen, aber andere Leute können bereitwillig Abstraktionen und Logik bauen, sie ins Herz des Projekts pflanzen und später so verfestigen, dass man sie praktisch nicht mehr entfernen kann.
      Zum Beispiel entstehen dann Dinge wie CommonExcelFileParser, CommonExcelFileParserUtilities, HasExcelParseStatus, ProductImportExcelParser, ProductImportExcelParserView, ProductImportExcelParserResultHandler, und sie werden zur Grundlage des umgebenden Codes.
      Das ist ähnlich wie bei einem Frontend-Projekt, das man mit React oder Angular beginnt: Der Umstieg auf etwas anderes wird zur Sisyphusarbeit.
      In der Praxis bauen Leute gleich ganze Plattformen, und selbst wenn darin Entscheidungen stecken, die in Zukunft Probleme verursachen, ist das Refactoring wegen der Kopplung viel schwieriger als bei einer Codebasis mit zu wenig Abstraktion.
      Es scheint, als würden Leute so etwas lieber tun, als KISS und YAGNI anzuwenden und Code zu schreiben, der leicht zu löschen ist – und ich weiß nicht, was man in solchen Fällen machen soll.
    • Trotzdem hängt es vom Kontext ab. Bei Business-Anwendungen stimmt das, und zwar zehnfach.
      Geschäftsanforderungen ändern sich und bewegen sich, also versuch nicht, sie vorherzusagen; schreib Dinge, die leicht zu ersetzen oder wegzuwerfen sind.
      Frameworks und Bibliotheken sind etwas anderes. Sie müssen sich zwar ebenfalls an Veränderungen in der Welt anpassen, aber in einem deutlich gemäßigteren Tempo.
      Das größte Problem entsteht, wenn Entwickler in Business-Anwendungen, die bereits ein Framework wie Rails oder Asp.Net verwenden, noch ein weiteres „Framework“ bauen wollen.
    • Manche Dinge ändern sich, und manchmal wählt man die falsche Abstraktion.
      Wenn du nicht den Linux-Kernel schreibst, solltest du nicht so schreiben, als würdest du den Linux-Kernel schreiben.
  • Es ist ziemlich merkwürdig, dass dieser Artikel Tests und Beobachtbarkeit überhaupt nicht behandelt.
    Tests verursachen zwar ebenfalls Wartungskosten, senken aber das Risiko, beim Entfernen von etwas etwas kaputtzumachen.
    Wenn man einen Dienst externen Aufrufern zugänglich gemacht hat, braucht man außerdem sowohl eine robuste Methode, um bestimmte Aufrufe als zur Stilllegung vorgesehen zu markieren und später zu löschen, als auch eine Möglichkeit zu beobachten, ob sie noch aufgerufen werden und wer sie aufruft.
    Kürzlich habe ich zum ersten Mal einen exponierten GraphQL-Resolver halbautomatisch entfernt. Da es bereits Metriken dazu gab, wie häufig bestimmte Resolver genutzt werden, konnte ich diese parsen und eine Liste von Resolvern erhalten, die nicht gelöscht werden können.
    GraphQL hat zwar bereits eine deprecated-Annotation, aber unser Service hat diese Annotation nicht speziell behandelt.
    Deshalb haben wir Beobachtbarkeit hinzugefügt, die anzeigt, wenn eine deprecated Function aufgerufen wird, sie lange genug in Produktion laufen lassen und konnten danach extern exponierten Code sicher löschen.

    • Etwas vereinfacht gesagt: Wenn man Dinge baut, die leicht zu löschen sind, erzeugt man beim Löschen keine unbeabsichtigten Bugs.
      Wenn man anfängt, sie übermäßig komplex zu machen, wird alles zu einem miteinander verknüpften Durcheinander, und Entwickler können nicht mehr wissen, welche Auswirkungen eine Änderung haben wird.
      Natürlich gibt es viele Möglichkeiten, es zu vermasseln. Man kann dummen „Best-Practice“-Prinzipien folgen oder „Microservices“ so betreiben, dass man nicht weiß, wer welche Services konsumiert. Aber dann hat man es eben nicht leicht löschbar gemacht.
      Externer Konsum ist ein gutes Beispiel. Es ist sinnvoll, Konsumenten eine angemessene Warnung zur Stilllegung eines Dienstes zu geben, aber wenn man ihn nicht tatsächlich abschalten kann, wann man will, ist das kein System, das auf leichtes Löschen ausgelegt ist.
      Wenn dieser Ansatz der richtige ist, kann man ihn wählen. Nur darauf zu hoffen, dass Tests und Beobachtbarkeit einem sagen, ob etwas kaputtgeht, wird wahrscheinlich nicht gut funktionieren.
      Ich bin nicht grundsätzlich gegen Tests, aber als Schutzmechanismus, der einem sagt, ob man irgendwo in einer langen, komplexen Kette etwas kaputtgemacht hat, sind sie kaum besonders hervorragend. Denn es ist sehr schwierig, tatsächlich die Testabdeckung zu haben, die einen schützt.
    • Wenn es viele Codezeilen gibt, kann man erwarten, dass es proportional dazu auch eine gewisse Menge an Testzeilen gibt.
      Wenn man einen Teil des Codes löscht, kann man auch einen Teil der Tests löschen.
      Man kann das so sehen, dass der Artikel zwar nur über Code spricht, die entsprechenden Auswirkungen auf Tests aber implizit mitgemeint sind.
      Nur weil der Artikel Tests nicht erwähnt, kann man nicht annehmen, dass er damit sagt, man solle keine Tests schreiben.
    • Tests sind gut, aber Programmieren endet nicht damit, Tests zu schreiben. Nicht jeder Artikel muss Tests erwähnen.
  • Bei diesem Teil habe ich das Gefühl, dass der Titel nicht immer stimmt: Code, der leicht zu löschen ist, ist meistens auch leicht zu erweitern.
    Weil er geschichtet und modular ist und unterschiedliche Teile über Abstraktionen wie Interfaces oder andere Typverträge voneinander isoliert.

  • Ich habe Studierenden der Computerphysik immer gesagt, dass die beste Berechnung die ist, die man gar nicht erst durchführen muss.

  • Persönlich teile ich Code in zwei Teile auf: Business-Logik und die tatsächliche Implementierung.
    Business-Logik kann ihrem Wesen nach dupliziert sein, aber technische Details sollten nicht zu stark dupliziert werden.
    Die tatsächliche Implementierung darf beliebig unordentlich sein, solange sie keine Business-Logik direkt enthält und unabhängig von der Anwendung bleibt.
    Wenn man das so macht, hat man, sobald man merkt, dass etwas ein Chaos ist und nicht gut läuft, die Option, die gesamte Implementierung zu löschen, statt die eigentliche Spezifikation aus der Implementierung zurückzuverfolgen und sie mühsam zu reparieren.

  • Die Aussage im ersten Absatz, das Problem der „Code-Wiederverwendung“ sei, dass sie einen später daran hindere, seine Meinung zu ändern, ist ein klarer Fehler.
    Allgemein gesagt ist das falsch. Wenn man seine Meinung ändert und der Code an zehn Stellen kopiert und eingefügt wurde, muss man zehn Stellen korrigieren.
    Steckt er dagegen in einer Function, muss man ihn nur einmal ändern. Selbst wenn man später feststellt, dass einer der zehn Aufrufe nicht geändert werden darf, kann man ihn dann kopieren und einfügen oder die Function allgemeiner machen.
    Wie beim Überqueren einer Straße, ohne hinzusehen, ist Copy-and-paste fast immer eine schlechte Idee.

    • Meiner Erfahrung nach endet schlechter Copy-and-paste-Code mit einem nervigen Nachmittag Abbau technischer Schulden und Fixes.
      Schlechte Abstraktionen dagegen führen zu monatelangem Abbau technischer Schulden.
      Natürlich lautet die Antwort „schlechte Abstraktionen vermeiden“, aber wir wissen alle, wie das in Teams und bei sich ändernden Produktanforderungen läuft.
    • Wiederverwendeter Code ist oft an mehreren Stellen der richtige Code; wenn man ihn ändern will, muss man langsamer werden und diese Stellen trennen.
      Wir haben ein git-Submodul mit gemeinsamen UI-Widgets, und eines davon zu ändern ist inzwischen nahezu unmöglich; es ist einfacher, die Komponente ins Projekt zu kopieren und lokal zu ändern.
      Das ist ein Problem. Gemeinsam genutzter Code sollte so minimal wie möglich sein, und das Teilen selbst macht Änderungen schwieriger.
    • Was passiert, wenn 3 von 10 Function-Aufrufen auf eine Weise geändert werden müssen, 5 auf eine andere und die übrigen 2 gar nicht mehr dieselbe Abstraktion verwenden und komplett neu geschrieben werden müssen?
      Wenn alles in einer Function steckt, werden die meisten Entwickler versuchen, diese Function so zu ändern, dass sie alle 10 Fälle abdeckt. Obwohl es von Anfang an keine einzelne Function hätte sein sollen.
      Es ist viel einfacher, zehn kopierte und eingefügte Stellen zu ändern, als einen falsch geknüpften Knoten zu lösen, der einmal gebunden wurde und Teile des Systems zusammenhält.
    • Der Autor würde wahrscheinlich antworten, dass dieser Code in ein Modul oder eine Function hätte verschoben werden sollen.
      Oberflächlich betrachtet scheint er sich bei diesem Thema selbst zu widersprechen, aber wenn man langsam liest, nutzt er Copy-and-paste als Signal dafür, welcher Code abstrahiert werden sollte und was das eigentliche Muster ist, dem man folgen sollte.
  • Es ist merkwürdig, wie ständig alle möglichen Gebote über Software, fast religiöse Prinzipien, wiederholt werden.
    Auf dem Papier sieht alles großartig aus und fühlt sich wie gesunder Menschenverstand an, aber auch nach 50 Jahren ist Software in 90 % der Fälle Müll.
    Trotzdem werden solche Dinge immer wieder als geniale Einsichten oder Silver Bullets hervorgeholt.

    • Ich denke, diese 90 % Müll werden von Leuten geschrieben, die solche Artikel nicht lesen oder schreiben.
  • Hier gibt es ein schönes Korollar: Schlechter Code bleibt länger bestehen, weil er viel schwieriger zu entfernen ist.