Grug-Brain-Entwickler (2022)
(grugbrain.dev)- Anhand der Erfahrungen eines langjährigen „Small Brain“-Entwicklers wird betont, dass der größte Feind der Softwareentwicklung Komplexität ist und dass eine Haltung, die sie reduziert, im Mittelpunkt der Praxis stehen sollte
- Die grundlegende Waffe gegen Komplexität ist, „no“ zu sagen; wenn Kompromisse nötig sind, sucht man mit 80/20-Lösungen, Prototypen, kleinen Refactorings und später Abstraktion nach realistischen Lösungen
- Tests sollten, nachdem sich der Code einigermaßen gefestigt hat, vor allem auf Integrationstests setzen; Unit-Tests und wenige End-to-End-Tests dienen als Ergänzung, und Bugs werden zuerst per Regressionstest reproduziert und dann behoben
- Tools, Typsysteme, Logging, Debugger und einfache APIs reduzieren die Gedächtnis- und Denkbelastung von Entwicklern, während übermäßige Generics, Callbacks, Microservices und Frontend-Frameworks Komplexität erhöhen können
- Bestehendem Code und organisatorischen Prozessen sollte man demütig begegnen, unverstandenen Code nicht leichtfertig entfernen, und es braucht eine Kultur, in der man sagen kann: „Das ist zu komplex“
Komplexität ist der ewige Feind von Entwicklern
- Der gefährlichste Feind in der Softwareentwicklung ist Komplexität
- Komplexität schleicht sich nach und nach in eine Codebasis ein und sorgt dafür, dass eine Änderung an einer Stelle auch scheinbar unverbundene Stellen kaputtmacht
- Grug vergleicht sie mit einem unsichtbaren „Dämon“, den Entwickler nicht direkt sehen oder einfach erschlagen können
- Komplexität kann auch durch wohlmeinende Entwickler oder Projektmanager ins System gelangen
- Je mehr Features, Abstraktionen und Prozesse hinzukommen, desto schwerer wird der Code zu verstehen
- Grug räumt ein, dass auch er selbst manchmal derjenige war, der Komplexität eingebracht hat
Wie man „no“ und „ok“ verwendet
- Die stärkste Waffe gegen Komplexität ist „no“
- Keine unnötigen Features bauen
- Keine unnötigen Abstraktionen bauen
- Allerdings ist „no“ zwar guter Engineering-Rat, aber für die Karriere nicht immer vorteilhaft
- „yes“ kann zu mehr Belohnungen oder zu Managementpositionen führen
- Trotzdem ist „no“ wichtig, wenn man sich als Entwickler treu bleiben will
- Wenn ein Kompromiss nötig ist, antwortet man mit „ok“ und sucht nach einer 80/20-Lösung
- Eine 80/20 solution ist ein Ansatz, der mit 20 % des Codes 80 % der Anforderungen erfüllt
- Auch wenn nicht alle dekorativen Funktionen enthalten sind, lässt sich so der Großteil des Werts liefern und Komplexität begrenzen
- Manchmal sei es besser, eine einfache Lösung umzusetzen, ohne dem Projektmanager alle Implementierungsdetails zu erklären
Codestrukturierung und Refactoring
- Zu Beginn eines Projekts sollte man die Anwendung nicht zu früh aufteilen
- Am Anfang ist die Form des Systems noch unklar, und auch was genau gebaut wird, ist noch nicht vollständig verstanden
- Mit der Zeit werden gute Schnittpunkte (cut points) sichtbar
- Gute Schnittpunkte haben eine schmale Schnittstelle zum Rest des Systems
- Sie verbergen interne Komplexität hinter einer kleinen Zahl von Funktionen oder Abstraktionen
- Grug vergleicht das damit, den Komplexitätsdämon in einem Kristall einzusperren
- Wenn Big-Brain-Entwickler zu Projektbeginn viele Abstraktionen schaffen wollen, braucht es Wege, den Schaden zu begrenzen
- Man kann sie auf Artefakte wie UML-Diagramme lenken, die dem Code nicht direkt schaden
- Wenn man „morgen eine laufende Demo“ verlangt, sieht man schnell tatsächlich funktionierenden Code und die Realität
- Diesen Demo-Ansatz kann man Prototyp nennen
- Refactoring ist später im Projekt nützlich, nachdem sich der Code gefestigt hat
- Je größer ein Refactoring, desto wahrscheinlicher scheitert es
- Bevorzugt wird ein Vorgehen in kleinen Schritten, während das System weiterhin lauffähig bleibt
- End-to-End-Tests werden während des Refactorings zur Lebenslinie, können aber bei Fehlern schwer verständliche Ursachen haben
- Übermäßige Abstraktion kann zu gescheitertem Refactoring und Systemversagen führen
- J2EE erscheint als Beispiel für übermäßige Abstraktion
- Die Einführung von OSGi war ein Versuch, Komplexität zu reduzieren, erzeugte aber noch stärkere Komplexität und erforderte nach eigener Aussage mehrere Personenjahre an Nacharbeit
Teststrategie
- Tests sparen viel Zeit, aber gegenüber „Tests immer zuerst“ besteht Skepsis
- Solange man die Domäne noch nicht versteht, ist schwer zu wissen, was getestet werden soll
- Grug schreibt den Großteil der Tests lieber nach dem Prototyp, wenn der Code sich zu stabilisieren beginnt
- Auch wenn Tests später geschrieben werden, braucht es Disziplin
- Tests dürfen nicht übersprungen werden, nur weil etwas „auf meiner Maschine funktioniert“
- Es gibt keine Garantie, dass es auf einer anderen Maschine oder künftig auf derselben Maschine funktioniert
- Die Rollen von Unit-Tests, Integrationstests und End-to-End-Tests werden unterschieden
- unit tests helfen zu Projektbeginn, brechen aber bei Implementierungsänderungen leicht und können Refactoring erschweren
- end to end Tests zeigen das Verhalten des Gesamtsystems, sind aber bei Fehlern schwer zu diagnostizieren und können ignoriert werden, wenn sie häufig brechen
- integration tests liegen hoch genug, um die Korrektheit des Systems zu prüfen, und niedrig genug, um die Ursache im Debugger zu sehen; sie kommen dem „Sweet Spot“ nahe
- Die ideale Teststruktur sieht folgendermaßen aus
- Zu Beginn einige Unit-Tests
- Starke Integrationstests, sobald Schnittpunkte entstehen und das System stabiler wird
- Eine kleine, gut gepflegte Menge an End-to-End-Tests, die nur die häufigsten UI-Funktionen und wichtige Edge Cases abdeckt
- Mocks werden nur selten eingesetzt, und wenn nötig, eher an großen Einheiten wie Systemgrenzen
- Beim Beheben von Bugs wird ausnahmsweise zuerst ein Regressionstest geschrieben, der den Bug reproduziert, danach folgt der Fix
Prozesse, Agile und die Haltung zu bestehendem Code
- Agile ist nicht das Schlimmste, aber auch nicht einfach nur gut
- Als Art, Entwicklungsarbeit zu organisieren, kann es einigermaßen in Ordnung sein
- Skepsis gilt Agile-Experten, die bei jedem Scheitern sagen, man habe Agile nicht richtig gemacht
- Für Erfolg sind Prototypen, Tools und die Einstellung guter Entwickler wichtiger
- Agile-Prozesse können helfen, aber wenn man sie zu ernst nimmt, können sie schaden
- no silver club, also: Es gibt keine Silberkugel, die alle Softwareprobleme löst
- Chesterton’s Fence dient als Warnung beim Entfernen bestehenden Codes
- Wenn man den Zweck eines Zauns nicht kennt, sollte man ihn zuerst verstehen und ihn nicht sofort entfernen
- Auch hässlicher Code verdient Respekt, wenn er heute funktioniert
- Besonders bei großen Systemen sollte man erst verstehen und dann verbessern
- Tests können Hinweise darauf geben, warum ein bestimmter „Zaun“ existiert
Tools und Typsysteme
- Tools reduzieren die Belastung, die Entwickler sonst durch eigenes Erinnern und Schlussfolgern tragen müssten
- In einer neuen Umgebung kann es die Produktivität erhöhen, Zeit in das Erlernen der vorhandenen Tools zu investieren
- Wenn Dokumentation fehlt, muss man sich unter Umständen bei anderen Entwicklern durchfragen
- Code Completion in der IDE sorgt dafür, dass man sich nicht jede API merken muss
- Java-Programmierung wird als nahezu unmöglich ohne Code Completion beschrieben
- Ein guter Debugger ist sehr wichtig
- Funktionen wie bedingte Breakpoints, Ausdrucksauswertung und Stack-Navigation sollte man gründlich lernen
- Für neue Entwickler könne das Erlernen des Debuggers mehr über Computer lehren als eine Universitätsvorlesung
- Der größte Wert eines Typsystems liegt darin, dass „nach dem Drücken von Punkt (.) angezeigt wird, was möglich ist“
- Typkorrektheit ist ebenfalls gut, aber für Grug sind Tool-Unterstützung und Code Completion wertvoller
- Übermäßige Typabstraktion oder Generics können produktiven Code schwer verständlich machen
- Generics werden bevorzugt hauptsächlich auf Containerklassen beschränkt
Ausdrücke, DRY und Separation of Concerns
- Bevorzugt wird nicht kurzer Code, sondern leicht zu debuggender Code
- Statt komplexe Bedingungen in eine Zeile zu schreiben, macht eine Aufteilung in Zwischenvariablen die Ergebnisse und die Bedeutung der einzelnen Ausdrücke leichter sichtbar
- Auch wenn dadurch mehr Zeilen entstehen, werden Bedingungen leichter verständlich und besser debugbar
- DRY ist guter Rat, braucht aber Balance
- Einfache, klare Duplikation kann besser sein als komplexe Callbacks, Closures oder Objektmodelle
- Wenn die Beseitigung von Duplikation die Komplexität erhöht, ist das ein Verlust
- Gegenüber Separation of Concern ist die Haltung kritischer
- Das typische Beispiel in der Webentwicklung ist die Trennung von CSS, HTML und JavaScript
- Grug bevorzugt als Alternative locality of behavior
- Wenn zusammengehöriger Code nahe bei dem Objekt liegt, das sich verhält, erkennt man beim Betrachten dieses Objekts sofort, was es tut
- Closures sind für passende Zwecke wie die Abstraktion von Collection-Operationen nützlich
- Aber wie Salz, Typsysteme und Generics reicht schon wenig; zu viel ist schädlich
- JavaScripts „callback hell“ erscheint als Beispiel für übermäßige Nutzung von Closures
Logging, Nebenläufigkeit und Optimierung
- Logging ist besonders in Cloud-Deployment-Umgebungen sehr wichtig
- Bei jedem wichtigen logischen Zweig werden Logs geschrieben
- Wenn ein Request über mehrere Maschinen läuft, wird in alle Logs eine request ID aufgenommen, damit sie zusammengeführt werden können
- Wenn möglich, wird der Log-Level dynamisch angepasst
- Wenn möglich, wird auch der Log-Level pro Nutzer angepasst
- Java-Logging-Bibliotheken können komplex sein, aber eine richtige Investition in die Logging-Infrastruktur zahlt sich später stark aus
- Nebenläufigkeit ist etwas, das man fürchten sollte
- Wenn möglich, werden einfache Modelle wie zustandslose Web-Request-Handler und unabhängige Remote-Work-Queues verwendet
- optimistic concurrency funktioniere im Webbereich gut
- thread local variable wird gelegentlich hauptsächlich beim Schreiben von Framework-Code verwendet
- Auch Concurrent-Data-Structures wie Javas ConcurrentHashMap müssen weiterhin vorsichtig eingesetzt werden
- Optimierung sollte erst beginnen, wenn ein echtes Performance-Profil vorliegt
- Zustimmung zu „premature optimization is the root of all evil“
- Der tatsächliche Engpass kann anders sein als erwartet
- Man sollte nicht nur auf die CPU schauen; Netzwerkaufrufe können Hunderttausenden oder Millionen CPU-Zyklen entsprechen und sollten daher nach Möglichkeit reduziert werden
API, Parsing und Visitor Pattern
- Eine gute API sorgt dafür, dass Entwickler nicht viel nachdenken müssen
- Schlechte APIs entstehen, wenn sie an Implementierungsinterna oder am Domain Model ausgerichtet sind oder zu abstrakt werden
- Bevorzugt wird Schichtung: für einfache Fälle eine einfache API, für komplexe Fälle eine komplexere API
- Bei objektorientierten APIs sollten Funktionen eher am Zielobjekt hängen
- Als schlechtes Beispiel gilt der Java-Ablauf, eine Liste zum Filtern in einen Stream umzuwandeln und dann wieder als List zu sammeln
- Eine häufige Operation wie
filter()sollte an list hängen und wieder eine list zurückgeben
- Parser im Stil von recursive descent sind ein unterhaltsamer und schöner Ansatz
- Parser-Generatoren werden als schwer verständlich und schwer zu debuggen kritisiert
- Es wird behauptet, echte Production-Parser seien fast immer rekursiv absteigend
- Bob Nystroms Crafting Interpreters wird empfohlen
- Es ist online kostenlos verfügbar, aber auch der Kauf des Buchs wird empfohlen
- Das visitor pattern wird jedoch als Falle bezeichnet
- Die Bewertung des Visitor pattern lautet kurz: „bad“
Frontend, Microservices und Hypes
- Microservices sind gefährlich, weil sie „zum schwierigsten Problem, ein System richtig aufzuteilen, auch noch Netzwerkaufrufe hinzufügen“
- In der Frontend-Entwicklung ist Komplexität besonders stark
- Kritisiert wird eine Situation, in der selbst für einfaches Formularspeichern oder Broschüren-Websites SPA-Bibliotheken, GraphQL-JSON-APIs und HTTP-Backends eingesetzt werden
- Die Trennung von Frontend und Backend wird als Schaffung von zwei Wohnorten für Komplexität beschrieben
- Grug sagt, er habe htmx und hyperscript entwickelt, um Komplexität zu verringern
- Bevorzugt wird, einfaches HTML beizubehalten und nicht viel JavaScript zu verwenden
- Er räumt ein, dass React für Jobs und bestimmte Arten von Anwendungen besser sein kann
- In der Entwicklung gibt es viele Hypes, besonders im Frontend
- Das Backend sei langweiliger geworden, und viele schlechte Ideen seien dort bereits ausprobiert worden
- Revolutionären neuen Ansätzen solle man vorsichtig begegnen, wie mit einer Prise Salz
- Viele Ideen wurden schon einmal ausprobiert, und wiederverwertete schlechte Ideen können Zeit verschwenden
Angst und Impostor-Syndrom
- Es ist gut, wenn Senior-Entwickler öffentlich sagen: „Das ist zu komplex“
- Wegen Fear Of Looking Dumb(FOLD) fällt es Entwicklern schwer zu sagen, dass sie etwas nicht verstehen
- Wenn Seniors es zuerst zugeben, können auch Juniors über Komplexität und mangelndes Verständnis sprechen
- FOLD ist besonders bei jungen Entwicklern eine wichtige Quelle, aus der Komplexität Kraft gewinnt
- Hilfreich seien Humor und die Fähigkeit, sich an frühere Fehlschläge zu erinnern
- Auch das Impostor-Syndrom ist in der Entwicklung verbreitet
- Grug beschreibt sich selbst als zwischen dem Gefühl, alles zu beherrschen, und dem Gefühl, nicht zu wissen, was er tut
- Trotz der Open-Source-Erfolge von htmx und _hyperscript fürchtet er weiterhin Fehler und Scheitern
- Wenn alle das Impostor-Syndrom spüren, ist es vielleicht besser, es so zu sehen, dass niemand wirklich ein Betrüger ist
Leseempfehlungen und Fazit
- Leseempfehlungen
- Das abschließende Fazit ist einfach
- Komplexität ist sehr, sehr schlecht
1 Kommentare
Hacker-News-Kommentare
Falls Professor Carson die Kommentare liest, möchte ich mich aufrichtig für all seine Beiträge bedanken.
Damals verstand ich nicht, warum wir an der Universität HTMX lernten und warum Sie davon so begeistert waren, aber nach ein paar Jahren verstehe ich es jetzt. HTML over the wire war alles.
Während meiner Arbeit als Staff Ruby on Rails Engineer habe ich Ihre Arbeit an Hotwire gesehen, und es war auch großartig, Sie gelegentlich auf Hacker News auftauchen oder auf GitHub mit Hotwire-Entwicklern sprechen zu sehen. Sie werden als eine Art Lichtgestalt der Programmier-Community sehr respektiert und geschätzt.
Ich stimme der Aussage zu: „Ein guter Debugger ist so wertvoll wie ein glänzender Stein, eigentlich sogar noch wertvoller.“
Ich habe sowohl kleine Startups als auch „Elite“-Teams bei Big Tech erlebt, aber fast immer war ich im Team der Einzige, der einen Debugger benutzt hat. In der realen Welt, zumindest im Bereich Webtechnologien, scheinen die meisten mit
print-Anweisungen zu debuggen.Während eines Testlaufs an einer interessanten Codezeile anzuhalten und den Call Stack zu betrachten, der bis dorthin geführt hat, ist viel einfacher, als den Code im Kopf vorwärts auszuführen. Für junge grugs ist diese Fähigkeit eine kleine Superkraft; wenn möglich, lohnt es sich, Zeit zu investieren, damit sie in der eigenen Codebasis funktioniert.
Sie sagten, dass sie Debugger kaum über Stack Traces oder das Betrachten der Werte von ein, zwei Variablen hinaus nutzen; man könne sich leicht in den Details komplexer Datenstrukturen und Kontrollflüsse verlieren, und es sei produktiver, an den entscheidenden Stellen Ausgaben und selbstprüfenden Code einzubauen.
Dem stimme ich weitgehend zu. Bei fast allem, was ich mache, führt die Schleife Hypothese–Log–Ausführung viel schneller zur Antwort. Ich versuche nicht, den Code im Kopf auszuführen; ich habe bereits ein Arbeitsmodell davon, wie der Code läuft, weiß, welche Ausgabe erscheinen sollte, wenn dieses Modell stimmt, und kann aus einer falschen Ausgabe schnell ein Gefühl dafür bekommen, was tatsächlich passiert.
[0] The unreasonable effectiveness of print debugging (349 points, 354 comments) April 2021 https://news.ycombinator.com/item?id=26925570
In einer Microservice-Mesh-Architektur ist es schon schwierig, lokal überhaupt etwas korrekt auszuführen, und auch Testumgebungen sind oft nicht so eingerichtet, dass man einen Step-Through-Debugger anhängen kann. Am Ende bleibt nur
print-Debugging, und selbst das funktioniert nicht, wenn das Logging-System selbst das Problem ist oder das Programm abstürzt, bevor die Logs geflusht werden.Schon bald braucht man es garantiert. Allerdings war ich auch bei der Zusammenarbeit irritiert, Leute zu sehen, die überhaupt nicht wissen, wie man einen Debugger benutzt. Wenn ich sage: „Setz dort einen Breakpoint“, „jetzt in die Funktion hineinsteppen und den Zustand der Variablen ansehen“, „das überspringen wir“, kommt jedes Mal ein leerer Blick zurück.
Genau genommen füge ich keine
print-Anweisungen ein und entferne sie wieder, sondern baue Logging-Code ein, der dauerhaft bleiben soll. Bei wichtigen Interfaces beginne ich gewöhnlich damit, auf INFO-Level Funktions-Eintritt/-Austritt und Parameterwerte zu protokollieren, und wenn ich bei der Nutzung des Systems Stellen sehe, die genauer betrachtet werden sollten, füge ich detailliertere Logs hinzu.Auch in das Logformat stecke ich einiges an Mühe. Bei der Arbeit an verteilten Systemen war es sehr nützlich, den Präfix jeder Logzeile exakt abzustimmen. Wenn Node-ID, pid und Timestamp alle mit fester Breite enthalten waren, konnte ich die Logs des gesamten Clusters herunterladen und sortieren; dann sah man die Abläufe mehrerer Nodes interleaved in einer einzigen Datei.
Aber sobald der Debugger tief in die Libraries oder Frameworks eintaucht, die ich verwende, verliere ich den Überblick und mag es nicht mehr. In diesen Frameworks oder Libraries stecken Zehntausende Personenstunden Arbeit; das fühlt sich an, als läge es weit über meinem Niveau.
Hier gibt es viele Sätze, die wie kleine Juwelen sind, aber dieser über Microservices gefällt mir am besten: „grug fragt sich, warum kluge Leute das schwierigste Problem, ein System richtig aufzuteilen, nehmen und dann auch noch Netzwerkaufrufe hineinstecken“
Sie zerlegen bereitwillig eine triviale Web-App mit vielleicht fünf Formularen in „Microservices“, lassen sie dieselbe Datenbank teilen, hängen eine API-Management-Schicht, eine Queue für große (Megabyte-)Batch-Jobs, ein E-Mail-Benachrichtigungssystem und sogar eine selbstgebaute Observability-Plattform daran und machen dann aus einfachen Webformularen eine SPA, weil „das einfacher ist“
Inzwischen verstehe ich, dass „Architektur“ und „Patterns“ ein Beschäftigungsprogramm für nutzlose Entwickler sind. Sonst würden sie wohl auf der Straße ein Schild hochhalten: „Schreibe JavaScript für ein Sandwich“
Für sie ist es nur ein undurchsichtiger Codeklumpen, den man nicht verstehen oder wiederverwenden kann, wenn er nicht über API-Aufrufe exponiert wird
Mehrere Pakete können intern zusammenarbeiten, aber dem Rest der Codebasis nur eine kleine API bereitstellen. Dass es ein Netzwerk ist, zwingt Module dazu, nur Daten auszutauschen statt Callbacks oder Verhalten, und setzt sie unter Druck, Interfaces abwärtskompatibel weiterzuentwickeln. So kann man verschiedene Module zu unterschiedlichen Zeiten „hot reloaden“, ohne dass alles explodiert
Ich glaube, den Großteil davon könnte man auch ohne echten Netzwerk-Hop bekommen, aber ernsthafte Versuche habe ich noch nicht gesehen
Ohne Orchestratoren wie K8S kann man sie nicht betreiben, und Installation und Betrieb sind schwierig, was dem Verkauf von Managed Cloud zugutekommt. Sie verbrauchen mehr Netzwerkbandbreite und CPU, und beides wird abgerechnet
Sie machen es schwer, komplexen oder großen Zustand innerhalb der Anwendung zu teilen und zu pflegen, sodass man stattdessen Managed Databases oder Event-Queue-Services nutzt. In einem Monolithen würden Queues oder Channels reichen, aber bei Microservices will man ein riesiges Biest wie Kafka
Auch die lokale Ausführung wird schwieriger, sodass man Cloud-Entwicklungsumgebungen braucht, und vielleicht mehrere Entwicklungs- und Testumgebungen betreiben muss. Man wird abhängiger von Eigenschaften wie der Networking-Variante einer bestimmten Cloud, was auch den Cloud-Lock-in erhöht
Ich weiß nicht, ob ihr euch noch daran erinnert, als Cloud damit verkauft wurde, IT-Kosten zu senken. Das war lächerlich; seit den 2000ern war mir klar, dass das Unsinn ist und am Ende alle Kosten nur steigen würden
Weil diese Art leichter zu managen ist; es ist eher eine Frage der Entwicklungsorganisation als eine technische Entscheidung. Wenn die Alternative ein Monorepo ist, finde ich persönlich das noch schlimmer
„Wenn grug zwischen Komplexität und einem 1:1 mit einem Tyrannosaurus wählen müsste, nimmt grug den Tyrannosaurus. Wenigstens kann grug den Tyrannosaurus sehen“
An diesen Satz denke ich mindestens einmal pro Woche
Dieser grug befindet sich in einem 1:1 mit einem transparenten Tyrannosaurus und ist verflucht
Einer der Werte dieses Textes liegt darin, dass jemand, der zu ausgefeilteren und komplexeren Dingen fähig ist, sie aus Erfahrung bewusst zu vermeiden versucht
Es gibt definitiv Zeiten und Orte, an denen Ausgefeiltheit und höhere Abstraktion nötig sind. Aber die grug-Philosophie sagt, dass darin an sich kein intrinsischer Wert liegt, und das scheint mir ein ziemlich vernünftiger Rat zu sein
Ich habe auch gesehen, dass KI-Unterstützung bei konsistentem, gewöhnlichem und datenorientiertem Code besser funktioniert. Das kann je nach Situation variieren
Anfänger schreiben einfachen Code, fortgeschrittene Entwickler schreiben komplexen Code, und Experten schreiben wieder einfachen Code
Welche Vorlesungen man als bekannt voraussetzen kann, hängt vom jeweiligen Kontext ab
Eine der Ironien moderner Softwareentwicklung ist, dass man Komplexität einführt, weil man glaubt, sie werde „am Ende Zeit sparen“.
Manchmal stimmt das und spart tatsächlich Zeit, aber nicht immer – und vielleicht auch nicht besonders oft.
DRY führt bisweilen zu vorschnellen Abstraktionen. In dem Moment, in dem man denkt: „Dieses Pattern wird bestimmt auch anderswo verwendet, also ziehe ich den gemeinsamen Teil heraus“, schleicht sich der Komplexitätsdämon ein.
Man möchte möglichst viele Bugs zur Compile-Zeit abfangen, aber dafür muss der Compiler mehr darüber wissen, was wir tatsächlich tun wollen, und am Ende baut man komplexe Typen, die die Verständlichkeit untergraben.
Um Boilerplate zu vermeiden, erstellt man komplexe Makros oder DSLs, aber wegen des Gesetzes der leaky abstractions platzt einem der Kopf, sobald man die tatsächliche Implementierung kennen muss.
Das Schwierige an all diesen Beispielen ist, dass sie manchmal gute Ideen sind. Zu erkennen, wann das Einführen von Komplexität tatsächlich zu Vereinfachung führt, halte ich für ein Merkmal guter Software Engineers.
Business-Logik sollte idealerweise an einer Stelle definiert sein, aber andere Dinge darf man bei Bedarf duplizieren, und das ist an sich nichts Schlechtes.
Um DRY zu zügeln, betone ich auch die „Dreierregel“: Copy-and-paste-Code ist bis zu drei Mal in Ordnung; danach lohnt es sich, über eine Abstraktion nachzudenken. Natürlich gibt es keine Faustregel, die immer passt, und dieses Gespür ist schwer zu vermitteln.
Normalerweise baue ich Webapps mit Sinatra-artigen Frameworks wie Flask und schreibe Funktionen, die auf URL-Patterns reagieren. Ein „Screen“ kann aus einer oder mehreren zusammenarbeitenden Funktionen und den zugehörigen HTML-Templates bestehen.
Wenn die Anwendung Dinge wie Datenbankverbindungen, Dateipfade sowie HTML-Header und -Footer einrichtet, gibt es darüber hinaus kaum Kopplung zwischen den Screens. Wenn ein neuer Screen nötig ist, kann man einen bestehenden kopieren und anpassen oder ein LLM einen Screen oder Endpoint erstellen lassen; wenn das Ergebnis schlecht ist, macht man ihn eben neu.
In einem früheren Job baute ich ein Framework namens Themis zum Erstellen von ML-Trainingssets; wir verwendeten Microservices, React, Docker und so weiter. Die tatsächliche Anforderung war, ständig neue Aufgaben hinzuzufügen und für jede Aufgabe schnell einen einfachen, aber stark optimierten Screen zu bauen. Wenn man 20.000 Entscheidungen treffen muss, ist schon ein Klick pro Entscheidung mühsam; bei vier Klicks werden daraus 80.000, und man gibt auf.
In der damaligen Architektur musste man API-Endpoints in einer JAXB-Anwendung und Komponenten in einer monolithischen React-App schreiben und dann 20 Minuten warten, bis TypeScript, Docker und javac durchgelaufen waren. Mit Glück bootete es; wenn nicht, ging es wieder von vorn los.
Ich schrieb eine Kritik an Themis und entwarf Nemesis mit dem Ziel, neue Aufgaben schnell entwickeln zu können, aber in meinem früheren Job wurde dieser Weg nicht gewählt. Trotzdem verarbeiten Nemesis und ich seither Millionen von Aufgabeninstanzen.
Es bedeutet nicht, dass man einen Helper herausziehen soll, der nur an einer Stelle verwendet wird. Selbst wenn sehr viel Logik in einer Funktion, Klasse, Datei usw. steckt, ist sie weiterhin DRY, solange sie nicht kopiert wurde.
Vorschnelle Abstraktion gibt es tatsächlich. Dass CS-Kurse im Allgemeinen genau dazu erziehen, hilft auch nicht. Gibt man einem Junior eine MySQL-Datenbank, könnte er als Erstes versuchen, MySQL zu abstrahieren.
Ob komplex oder einfach: Wenn das Ergebnis keinen Mehrwert schafft, ist es egal. Konzentriere dich zuerst darauf, mehr Wert hinzuzufügen, als du wegnimmst, und kümmere dich danach um die Komplexität.
Eine meiner liebsten Arten, LLMs zu nutzen, ist, diesen Essay hineinzulegen und sie zu bitten, in der Persona des grug-brained developer das Issue zu kommentieren, mit dem ich mich gerade beschäftige. Das hilft gut beim Stressabbau.
Ob es reicht, den Prompt etwa mit „Verhalte dich wie der Grug Brained Developer aus diesem Essay“ zu beginnen?
„Komplexität sehr schlecht“ ist wirklich wahr
In all den Jahren, in denen ich Software Engineering betrieben habe, hat sich dieser Gedanke als eines der wenigen Prinzipien erwiesen, das in jeder Situation zuverlässig zutraf. Manche Probleme sind von Natur aus komplex, aber selbst dann ist es viel besser, sich die Zeit zu nehmen, zur einfachsten Lösung zu gelangen
Meine effektivsten Arbeiten entstanden, nachdem ich frühere Ansätze infrage gestellt und radikal vereinfacht hatte. Man verliert vielleicht ein wenig potenzielle Flexibilität, aber in den meisten Fällen brauchte man tatsächlich nicht so viel Flexibilität, wie man glaubte
Seit es zum Beispiel recht gute, agentisch arbeitende LLMs gibt, vermeide ich übermäßig komplexe TypeScript-Typen, die fragil und schwer zu debuggen sind, schreibe stattdessen Code wie eine Spezifikation und lasse das LLM darauf basierenden Code statisch generieren
Die ESLint-Abhängigkeit des Projekts ging nach Versionsupdates ständig kaputt, viele Regeln waren nicht ausgefeilt genug, um False Positives zu vermeiden, und sie in TypeScript und VSCode sauber zu pflegen war ebenfalls komplex. Der Wechsel zu Biome.js war einfacher und ausreichend effektiv, aber in letzter Zeit treten dort Bugs auf. Trotzdem wurde mir klar, dass Linting etwas Nützliches ist, aber nichts, das man mit übermäßig viel Zeit pflegen sollte; deshalb habe ich es aus der Build-Toolchain entfernt, und auch in VSCode muss es nicht mehr ständig aktiviert sein. Es reicht, gelegentlich Biome auszuführen und nur Code Style und Formatierung zu prüfen
Beim Bau eines eigenen Datenmigrations-Tools für ein Projekt kam ich zu dem Schluss, dass Vorwärtsmigrationen nötig sind, Rückwärtsmigrationen den Aufwand und die Komplexität der Implementierung aber nicht wert sind. Wenn man eine Datenbank mit Daten zurückrollen muss, stellt man ein Backup wieder her; wenn keine Daten vorhanden sind oder es keine Produktionsdatenbank ist, startet man mit versionierten Initialisierungsskripten aus einem sauberen Zustand
Ich bin mir auch nicht sicher, inwiefern das dritte einfacher ist. Mit mathematischem Denken lässt sich leicht ein bijektiver Raum schaffen. Rückwärtsmigrationen mit anderen Mitteln nachzubilden, könnte schwieriger sein. Natürlich hängt das von den Details ab und ist keine allgemeine Regel
Der kluge Rich sagte, complect bedeute, Dinge miteinander zu verflechten, und dem stimme ich zu. Rich sagte, Komplexität sei schlecht, aber dem stimme ich nicht zu. Dinge miteinander zu verbinden, ist notwendig. Wenn sie nicht miteinander verbunden sind, kann man Probleme nicht lösen
Ich kann kaum glauben, dass dieser Text von 2022 ist
Ich hätte selbstbewusst behauptet, ich hätte ihn vor 10 Jahren gelesen und er sei damals schon ein Klassiker gewesen
Traurig, aber wahr: Nachdem man „yes“ gelernt hat, zu lernen, wie man bei einem Fehlschlag einem anderen Grug die Schuld gibt, ist der ideale Karriereratschlag
Als ich zum ersten Mal in die Unternehmenswelt kam, dachte ich, das könne nicht stimmen und es mangele den Technikteams nur an Kommunikation. Ich lag falsch, und Grug hatte recht