- Komplexität ist der gefährlichste Faktor in der Entwicklung
- Echte Effizienz entsteht aus einem pragmatischen Ansatz, der Komplexität vermeidet, etwa durch eine „80/20-Lösung“
- Es ist wichtig, eine ausgewogene und flexible Haltung zu Tests und Refactoring zu bewahren
- Betont wird die Nutzung von Tools sowie die Gewohnheit, leicht lesbaren und wartbaren Code zu schreiben
- Vor übermäßiger Abstraktion und Trends wird gewarnt, stattdessen wird eine Haltung empfohlen, die auf Einfachheit setzt
Einleitung
- Dieser Text ist eine Sammlung von Gedanken eines Grug-Brain-Entwicklers, der aus Erfahrung gelernte Erkenntnisse aus vielen Jahren Softwareentwicklung zusammenfasst
- Der Grug-Brain-Entwickler hält sich selbst nicht für besonders klug, hat aber durch langjähriges Programmieren vieles gelernt
- In der Hoffnung, dass andere aus Fehlern lernen, teilt er seine Einsichten auf einfache und witzige Weise
- Komplexität ist der größte Feind im Entwicklerleben
- Komplexität schleicht sich heimlich in die Codebasis ein und bringt selbst anfangs leicht verständlichen Code nach und nach an einen Punkt, an dem Änderungen kaum noch möglich sind
Den Dämon der Komplexität bändigen
- Komplexität dringt lautlos ein wie ein unsichtbarer Geist, und Projektmanager sowie Nicht-Grug-Entwickler bemerken sie oft nicht
- Der beste Weg, Komplexität zu verhindern, ist, „Nein“ zu sagen
- „Wir bauen dieses Feature nicht“
- „Wir führen diese Abstraktion nicht ein“
- Natürlich kann es der Karriere eher nützen, „Ja“ zu rufen, aber dem Grug-Brain-Entwickler ist eine ehrliche Entscheidung sich selbst gegenüber wichtiger
- Je nach Situation sind auch Kompromisse („ok“) nötig; in solchen Fällen wird bevorzugt ein Problem mit einer 80/20-Lösung (nach dem Pareto-Prinzip) einfach gelöst
- Dem Projektmanager nicht alles im Detail zu sagen und es in Wirklichkeit per 80/20-Ansatz zu erledigen, kann ebenfalls eine kluge Strategie sein
Codestruktur und Abstraktion
- Die passende Einheit im Code (Cut-Point) zeigt sich mit der Zeit ganz natürlich; deshalb ist es besser, frühe Abstraktion zu vermeiden
- Ein guter Cut-Point hat idealerweise nur eine schmale Schnittstelle zum Rest des Systems
- Frühe Versuche der Abstraktion scheitern leicht, und erfahrene Entwickler versuchen erst dann langsam zu strukturieren, wenn die Form des Codes sich einigermaßen gesetzt hat
- Weniger erfahrene oder „Big-Brain“-Entwickler neigen dazu, zu Beginn eines Projekts übermäßig zu abstrahieren und hinterlassen damit Wartungslast
Teststrategie
- Bei Tests sind Maß und Ausgewogenheit wichtig
- Bevorzugt wird, Tests erst nach dem Prototyping zu schreiben, wenn sich der Code einigermaßen stabilisiert hat
- Unit-Tests werden anfangs genutzt, in der Praxis zeigen aber Tests auf mittlerer Ebene (Integrationstests) den größten Effekt
- End-to-End-Tests sind ebenfalls nötig, aber zu viele davon führen schnell zu unwartbaren Zuständen; daher sollten nur wenige wirklich notwendige Pfade abgedeckt werden
- Bei Bug-Reports sollte immer zuerst ein reproduzierender Test ergänzt und der Bug danach behoben werden
Prozesse, Agile und Refactoring
- Agile ist für Grug-Entwickler nicht schlecht und auch nicht das Schlimmste, aber überzogene Erwartungen an „Agile-Schamanen“ sind riskant
- Prototyping, Tools und gute Kollegen sind in Wirklichkeit wichtigere Erfolgsfaktoren
- Refactoring ist ebenfalls eine gute Gewohnheit, aber großes und erzwungenes Refactoring ist riskant
- Das gewaltsame Einführen komplexer Abstraktionen führt eher zum Scheitern eines Projekts
Wartung, Perfektionismus und Bescheidenheit
- Bestehende Systeme ohne Grund auseinanderzunehmen ist riskant, und Strukturen zu entfernen, deren Zweck man nicht versteht, ist keine gute Gewohnheit
- Idealismus, der von perfektem Code träumt, verursacht in der Realität meist Probleme
- Mit wachsender Erfahrung spürt man immer stärker, dass man funktionierenden Code respektieren sollte
Tools und Produktivität
- Gute Entwickler-Tools (IDE-Autovervollständigung, Debugger usw.) steigern die Produktivität erheblich, und es ist wichtig, sie gründlich zu verstehen
- Der tatsächliche Wert eines Typsystems liegt in Autovervollständigung und Fehlervermeidung; übermäßige Abstraktion und Generics sind dagegen eher riskant
Code-Stil und Wiederholung
- Empfohlen wird ein Stil, bei dem etwa Bedingungen auf mehrere Zeilen aufgeteilt werden, damit Code leichter lesbar und einfacher zu debuggen ist
- Das DRY- (Don’t Repeat Yourself-)Prinzip wird respektiert, aber wichtiger als das erzwungene Entfernen von Duplikaten ist ein ausgewogenes Verhältnis
- In vielen Situationen ist einfache Wiederholung besser als eine komplexe DRY-Implementierung
Prinzipien des Softwaredesigns
- Gegenüber dem SoC- (Separation of Concerns-)Prinzip wird Lokalität des Verhaltens bevorzugt: „Code, der ein bestimmtes Verhalten umsetzt, sollte in dem Objekt liegen, damit Wartung leichter fällt“
- Vor Callback-/Closure-Konstrukten, Typsystemen, Generics und Abstraktion wird gewarnt, wenn sie nur in kleinen Mengen sinnvoll eingesetzt werden sollten
- Der Missbrauch von Closures kann in JavaScript sogar zur „Callback-Hölle“ führen
Logging, Betrieb
- Logging ist sehr wichtig: Es sollte an wichtigen Verzweigungen vorhanden sein, und in Cloud-Umgebungen sollte Nachverfolgbarkeit etwa über Request-IDs möglich sein
- Wenn dynamische Log-Level und nutzerspezifische Logs genutzt werden können, hilft das enorm bei der Fehlersuche im Betrieb
Nebenläufigkeit, Optimierung
- Bei Nebenläufigkeit wird nur den möglichst einfachen Modellen vertraut (zustandslose Web-Requests, getrennte Worker-Queues usw.)
- Optimierung sollte nur dann wirklich erfolgen, wenn reale Performance-Profildaten vorliegen
- Auf versteckte Kosten wie Netzwerk-I/O muss geachtet werden; nur auf CPU-Komplexität zu schauen, ist riskant
API-Design
- Eine gute API muss leicht zu benutzen sein; zu komplexes Design oder zu viel Abstraktion verschlechtern die Developer Experience
- Empfohlen wird eine Struktur aus „einfacher API für den konkreten Use Case“ und „geschichteter API, mit der sich auch komplexe Fälle umsetzen lassen“
Parser-Entwicklung
- Rekursiver Abstieg wird in der akademischen Welt unterschätzt, ist aber für produktiven Code die geeignetste und am leichtesten verständliche Methode
- Nach den meisten Erfahrungen in der Parser-Entwicklung sind mit Tools generierte Parser in ihren Ergebnissen zu komplex und bei der Problemlösung eher ein Nachteil
- Als bestes empfohlenes Buch wird „Crafting Interpreters“ genannt, weil es viele praxisnahe Ratschläge enthält
Frontend und Trends
- Modernes Frontend (React, SPA, GraphQL usw.) ruft eher noch mehr Komplexitätsdämonen herbei und ist oft unnötig
- Grug selbst bevorzugt einfache Tools wie htmx und hyperscript, um Komplexität zu reduzieren
- Im Frontend gibt es zwar ständig neue Versuche, doch man sollte beachten, dass vieles nur Wiederholung bestehender Ideen ist
Psychologische Faktoren, Impostor-Syndrom
- Die meisten Entwickler haben oft das Gefühl, „nicht zu wissen, was sie tun“, und sollten sich von FOLD (Fear Of Looking Dumb) lösen
- Wenn ein Senior-Entwickler offen sagt: „Das ist auch für mich schwierig, das ist zu komplex“, können auch Junior-Entwickler den Druck loslassen
- Das Impostor-Syndrom ist ein verbreitetes Gefühl, und es wird dazu ermutigt, weiter zu lernen und daran zu wachsen
Fazit
- In der Programmierung muss Komplexität immer kritisch beobachtet werden, und das Bewahren von Einfachheit ist der Schlüssel zu erfolgreicher Entwicklung
- Erfahrung, der effektive Einsatz von Tools, Bescheidenheit und Respekt vor tatsächlich funktionierendem Code führen langfristig zu effizienterer und wertvollerer Entwicklung
- „Komplexität sehr, sehr schlecht“ — dieser Satz sollte immer im Gedächtnis bleiben
1 Kommentare
Hacker-News-Kommentare
print-Statements debuggen. Selbst wenn ich versuche, meinen Workflow Kollegen zu zeigen, kommt kaum Reaktion. Ich stimme zu, dass der beste Ausgangspunkt, um ein System zu verstehen, direkt der Debugger ist. Bei Tests an einer interessanten Codezeile anzuhalten und sich den Stack anzusehen, ist viel einfacher, als den Code nur im Kopf nachzuverfolgen. Wenn man lernt, einen Debugger zu benutzen, bekommt man wirklich eine kleine Superkraft. Wenn es irgendwie geht, sollte man das unbedingt einmal ausprobierenprint-Debugging die einzige realistische Option. Und wenn sogar noch das Logging-System Probleme hat oder das Programm abstürzt, bevor es Logs ausgibt, dann kann man nicht einmalprintbenutzenprintsowie selbstprüfenden Code einzubauen.printeinzufügen ist viel schneller, als sich Schritt für Schritt mit dem Debugger hineinzubewegen. Außerdem bleibtprint-Code im Programm erhalten, während eine Debugging-Sitzung verschwindet." Ich stimme dieser Sicht ebenfalls zu. Während des größten Teils der Entwicklung liefert die Schleife ausprint-Hypothese-Ausführen eine viel schnellere Problemlösung. Es geht nicht darum, den Code im Kopf zu "auszuführen"; vielmehr hat man bereits ein Funktionsmodell des Codeflusses, und wennprinteine falsche Ausgabe zeigt, erkennt man in den meisten Fällen schnell, was wirklich los ist. Passender Link: The unreasonable effectiveness of print debuggingprintf-Debugging unter Linux-artigen Systemen immer verbreitet war, lag auch daran, dass GUI-basierte Debugger in dieser Umgebung nicht vertrauenswürdig waren. Linux-GUIs sind oft instabil und daher schwer zu verlässlich zu nutzen. Bei mir begann der ernsthafte Einsatz von Debuggern erst, als (1) unter Windows die GUI gut funktionierte, aber die CLI oft kaputtging, und (2) ich mehrfach Probleme hatte, weilprint-Debugging-Code versehentlich in eine Version übernommen wurde. Danach habe ich einige Abenteuer mit CLI-Debuggern erlebt, und der Prozess aus JUnit + Debugger (IDE-basiert, etwa in Eclipse), bei dem man experimentellen Code direkt ausprobiert und zugleich als Test stehen lässt, fühlte sich fast so bequem an wie ein Python-REPL. Allerdings braucht man eine Anfangsinvestition, um den Debugger passend zur Umgebung einzurichtenAstWalker,AstItem::dispatch(AstWalker)oderAstWalker::process(AstItem)viel aussagekräftiger als Visitor. „Visitor“ im Sinn von "besucht" ist einfach zu abstrakt und bedeutungsarm. Je nach Kontext sieht das natürlich anders aus, und man kann einfach im Kommentar "visitor pattern" dazuschreiben, dann gibt es kein Erkennungsproblem. Ich hatte früher einmal die Aufgabe, zwei Objekttrees gegeneinander abzugleichen, um Daten zu vergleichen oder zu importieren, und habe dafür den NamenAbstractImporterverwendet. Das war konkreter, der Prozess und die Rolle waren klarer. Mit einem typischen Visitor Pattern war das nicht ganz identisch