Software-Architektur lernen
(matklad.github.io)- Softwaredesign lernt man tiefer nicht in Vorlesungen, sondern in realen Projekten, wenn man Verantwortung trägt und Probleme zur eigenen Angelegenheit werden
- Conway’s Law ist die Sichtweise, dass Software die soziale Struktur der Organisation widerspiegelt, die sie erstellt; auch der Unterschied zwischen wissenschaftlichem Code und Industriecode kann aus Anreizstrukturen entstehen
- rust-analyzer macht es hochwirksamen Mitwirkenden leicht, sich zu konzentrieren, durch schnelle Builds, Unterstützung für stable, keine C-Abhängigkeiten und Tests, die in wenigen Sekunden laufen
- rust-analyzer schützte unabhängige Funktionen mit
catch_unwindund senkte die Hürden für PRs, wandte aber auf die zentrale spine deutlich strengere Qualitätsmaßstäbe an - Experimentelle Strukturen können zur langfristigen Realität werden; auch rust-analyzer führte von einem LSP-Architektur-Prototyp zur Pflege eines weiteren Compilers
Softwaredesign lernt man am besten in der Praxis
- Softwaredesign lernt man besser, wenn man in realen Projekten Verantwortung übernimmt und Probleme direkt selbst löst, als in formalen Lehrveranstaltungen
- Nicht in Uni-Kursen zu Design und Projektarbeiten, in denen die Rolle des „Architekten“ übernommen wurde, sondern im zweiten realen Projekt, IntelliJ Rust, begann das Lernen richtig, als Designprobleme zur eigenen Verantwortung wurden
- Bei IntelliJ Rust gab es einige Fehler, aber keine katastrophalen, und in diesem Prozess ließ sich viel lernen
- Software Engineering hat auch eine einfache Seite: Neugierige Menschen können sich darin einarbeiten, indem sie von Prinzipien her denken und viele Texte lesen
Anreizstrukturen und Conway’s Law
- Conway’s Law ist die Sichtweise, dass Software die soziale Struktur der Organisation wiederholt, die sie baut
- Der Unterschied zwischen Industriesoftware und wissenschaftlichem Code kann weniger aus Wissen über den Bau von Software selbst entstehen als aus den Anreizstrukturen, die Menschen dazu bringen, Software zu schreiben
- Eine Situation wie „eine Promotion, bei der innerhalb von drei Monaten ein Paper eingereicht werden muss“ kann ein entscheidender Faktor für die Form wissenschaftlichen Codes sein
- Auf Anreizstrukturen kann man im Wesentlichen auf zwei Arten reagieren
-
Die Anreize eines Projekts gestalten oder verschieben
- Gelegenheiten, die Anreizstruktur eines Projekts zu entwerfen oder zu verändern, sind selten, aber wenn sie entstehen, ist ihre Wirkung groß
- Der Kern von TIGER_STYLE ist nicht die Liste von Regeln an sich, sondern der soziale Kontext, der diese Regeln zu guten Entscheidungen macht
-
Wenn man sie nicht ändern kann, sich an die Zwänge anpassen
- Anreizstrukturen sind fast nie so gegeben, wie man sie sich wünscht; wenn man sie nicht ändern kann, muss man sich an sie anpassen
- Auch in Industrieprojekten gibt es fast nie „Zeit, es richtig zu machen“; man muss innerhalb der gegebenen Einschränkungen das Bestmögliche erreichen
Wie rust-analyzer Struktur und Mitwirkende aufeinander abgestimmt hat
- rust-analyzer ist ein Projekt mit sowohl Tiefe als auch Breite
- Auf der tiefen Seite zieht es durch seinen Charakter als Compiler herausragende und engagierte Mitwirkende an
- Auf der breiten Seite hat eine klassische IDE viele spezialisierte Funktionen für konkrete Zwecke, sodass Lernende von Rust oder Wochenend-Mitwirkende, die sich nicht dauerhaft beteiligen können, gut ein oder zwei Stunden investieren können, um ihr eigenes Problem zu beheben
- Dass rust-analyzer keinen
rustc-Build verlangt, auf stable gebaut werden kann, keine C-Abhängigkeiten hat und die gesamte Testsuite in wenigen Sekunden durchläuft, war eine bewusste Entscheidung, um hochwirksame Mitwirkende anzuziehen - Das Build-System sollte so ausgefeilt sein, dass Menschen sich um nichts anderes kümmern müssen und sich auf die Arbeit am Borrow Checker konzentrieren können
- Um Wochenend-Mitwirkende anzuziehen, wurde das Innere von rust-analyzer in mehrere unabhängige Funktionen aufgeteilt, und jede Funktion wird zur Laufzeit mit
catch_unwindgeschützt - Die Hürde für Feature-PRs wurde auf „der Happy Path funktioniert und es gibt Tests“ gesenkt, und selbst wenn der betreffende Code abstürzt, wurde das als akzeptabel betrachtet
- Dafür waren allerdings zwei Bedingungen nötig
- Qualitätsprobleme mussten innerhalb einer einzelnen Funktion isoliert bleiben und durften nicht auf andere Teile übergreifen
- Laufzeitabstürze durften für Benutzer nicht sichtbar sein; dazu arbeiten rust-analyzer-Funktionen mit unveränderlichen Snapshots und dürfen keine Daten verunreinigen
- Für die zentrale spine, die die Funktionen trägt, galten dagegen deutlich strengere Qualitätsanforderungen
Das Risiko, dass experimentelle Strukturen zur langfristigen Realität werden
- Wenn man sich eher an Anreizstrukturen anpasst als sie zu verändern, sollte man vorsichtig sein: Die Zukunft ist ungewiss und wird oft auf die unangenehmste Weise Realität
- Die ursprüngliche Motivation von rust-analyzer war, zu vermeiden, innerhalb von IntelliJ Rust noch einen parallelen Compiler zu schreiben, und als Prototyp eine bessere Architektur für LSP zu validieren, um die daraus gewonnenen Erkenntnisse wieder in
rustczurückfließen zu lassen - Deshalb war der Code, einschließlich des Kerns, stark experimentell
- Im Ergebnis musste dann doch ein weiterer Compiler gepflegt werden
- Ähnlich begann auch das uutils-Projekt als wichtiges Ziel für Menschen, die Rust lernen wollten, und wurde schließlich zur coreutils-Implementierung von Ubuntu
Empfehlenswerte Materialien und Bücher
- Es gibt kein einzelnes Buch mit der richtigen Antwort, und Praxiserfahrung scheint unverzichtbar zu sein
- Boundaries by Gary Bernhardt
- Ein Material mit soliden konkreten Ratschlägen, das weitere Erkundungen auf höherer Ebene angestoßen hat
- How to Test
- Die Bedeutung von Tests war sofort klar, aber es dauerte lange, anzuerkennen, dass viel häufig zitierter Testrat wenig praktisch ist, und zu konzeptualisieren, was tatsächlich funktioniert
- ∅MQ guide und Texte von Pieter Hintjens
- Diese Materialien führten an Denkweisen im Stil von Conway’s Law heran
- Die Architektur für Feature-Entwicklung in rust-analyzer ist eine Form der Anwendung von optimistic merging
- Reflections on a decade of coding by Jamii
- Behandelt sehr metahafte Themen und ist so hervorragend, dass es als erster Eintrag in einer Linkliste geführt wird
- Ted Kaminski Blog
- In der Form von Notizen für ein nicht existierendes Buch kommt er einer konsistenten Theorie der Softwareentwicklung am nächsten
- Software Engineering at Google und Ousterhouts The Philosophy of Software Design
- Oft empfohlene gute Bücher; besonders Software Engineering at Google half dabei, wichtige Bezeichnungen rund um Unit Tests und Integrationstests zu ordnen
- Persönlich waren sie jedoch keine bahnbrechenden Bücher
1 Kommentare
Hacker-News-Kommentare
Als Spickzettel zusammengefasst: Gutes Design sollte von einer einzigen Idee durchdrungen sein und auf minimale Überraschungen abzielen.
Wenn das System es zulässt, werden die Leute es am Ende auch so benutzen, und eine Lösung, die mit „wenn einfach alle nur X machen würden“ beginnt, ist keine Lösung.
Man sollte den Teil, der Daten transformiert, vom Teil trennen, der sie verwendet; Datenmodelle leben länger als Code, und enge Kopplung ist die Wurzel vieler Probleme.
Versionsverwaltung ist unvermeidlich, Zustand muss explizit sichtbar sein, und für jede Information sollte es eine einzige Quelle der Wahrheit geben.
Man sollte mehr Zeit auf Benennungen verwenden; wenn Tests schwierig sind, ist das Design falsch, und nicht dokumentierte Entscheidungen wird man später bereuen.
Kommunikation kostet, also sollte sie gerechtfertigt sein, bevor man sie bezahlt, und die Arbeit von Ingenieuren besteht darin, Probleme mit Heuristiken unter unvollständigen Informationen zu lösen.
Auch der Text selbst wirkte aus Sicht der Softwarearchitektur nicht besonders konsistent.
Die 4+1-Architekturansicht ist, wenn man UML ausklammert, ein gutes konzeptionelles Gerüst, um über das große Ganze nachzudenken, und die Reihe Pattern-Oriented Software Architecture ordnet viele Architekturen gut ein, zu denen Menschen gelangen.
Grady Booch hatte auch einmal ein Software-Architecture-Handbook aufgebaut, das heute aber weitgehend stillzustehen scheint; damals wurden auf der Mailingliste große Systemarchitekturen von Firmen oder großen Open-Source-Projekten dokumentiert.
Wenn man sich in solche Materialien vertieft, sieht man, dass Architekturen mit unterschiedlichen Schwerpunkten wie Skalierung, Sicherheit, Performance, Interoperabilität oder Fail-Safe-Eigenschaften entstehen und dass jedes Ziel mit realen Trade-offs verbunden ist.
Wenn etwas nach diesem Maßstab besser ist, dann ist es auch dann gutes Design, wenn es zunächst wie schlechtes Design aussieht.
Interfaces sollten leicht richtig und schwer falsch zu benutzen sein, und man sollte mitdenken, wie jemand ohne Projektwissen sie verwenden würde.
Korrekter Code sollte leicht zu schreiben sein, fragwürdiger Code sollte auffallen, und Bugs sollten möglichst weit nach links verschoben werden.
Es ist besser, eine ganze Bug-Klasse zu beseitigen, als einen einzelnen Bug zu beheben, und weil Interfaces schwerer zu ändern sind als Implementierungen, ist eine hässliche Implementierung in Ordnung, wenn das Interface richtig ist.
Kommentare und Dokumentation sollten erklären, warum der Code diese Form hat; wenn es scheinbar einfachere Wege gibt, die wegen bestimmter Einschränkungen nicht funktionieren, sollte das festgehalten werden.
Aus Datensicht sollte man Wiederholungen vermeiden; speichert man dieselbe Tatsache an mehreren Orten, driftet sie am Ende auseinander und erzeugt Bugs.
Es kostet, den ausgetretenen Pfad zu verlassen; manchmal lohnt sich das, aber man sollte diese Kosten nicht unterschätzen.
Oft ist langweilige Technik, die schlechter aussieht, die bessere Technik, und statt „ist es das wert?“ sollte man eher fragen: „Ist es das wert im Vergleich dazu, an etwas anderem zu arbeiten?“ und den Erwartungswert betrachten.
Selbst wenn man sich für klüger als andere hält, gibt es Probleme, bei denen Intelligenz allein nicht genügt, und manche Probleme entdeckt man tatsächlich erst, wenn sie explodieren; deshalb sollte man aus den Fehlern anderer lernen.
Reibung ist ein stiller Killer.
Planung ist gut, aber manchmal muss man Dinge einfach ausprobieren, und alles kostet Geld.
Wer ohne Kostenbetrachtung entwirft, wird später zu schwierigen Entscheidungen gezwungen.
Selbst in einem Solo-Projekt dienen die Einschränkungen der Engine als Leitplanke dafür, „wie man ein neues seltsames Feature hinzufügt, das beim Testen auftaucht“.
Man muss dann nicht jedes Mal eine riesige Dokumentation mit sich herumtragen wie „so baust du einen neuen Soundeffekt, der in einem bestimmten Zustandsübergang mehrfach abgespielt wird“.
Systemdesigner müssen die Branche verstehen; sie müssen deren Terminologie und Modellierungsgewohnheiten nicht vollständig übernehmen, aber sie sollten die Gründe und Perspektiven kennen, aus denen auf die Datensätze geschaut wird.
Wir haben die Komplexität des Gesundheitsmarkts an einigen Stellen bewusst vereinfacht, um unnötige Überdefinitionen zu entfernen und ein stärker integriertes Modell zu liefern, aber solche Änderungen konnten wir nur deshalb mit Überzeugung vornehmen, weil wir den Problemraum gut genug verstanden haben.
Besonders Namen sterben fast nie.
Manchmal schon, aber Umbenennungen erfordern extremen Aufwand; deshalb lohnt es sich sehr, viel Zeit darauf zu verwenden, dass Domain-Experten Namensvorschläge gründlich prüfen und bestätigen, dass nichts Wesentliches fehlt.
Einige Konzepte kann man durchdrücken, aber die Business-Seite, etwa Vertrieb und Marketing, verlangt immer wieder Branchenbegriffe und drängt darauf, das Modell an die aktuelle Sicht der Branche anzupassen.
Wenn man sich entscheidet, diesen Fluss zu durchbrechen, sollte dieser Bruch klar beabsichtigt sein und einen eindeutigen Zweck haben.
Die wichtigste Eigenschaft von Software ist Wartbarkeit.
Die Frage ist nicht nur, was die Entwicklung kostet; die Betriebskosten haben einen viel größeren Einfluss, weil dazu nicht nur Infrastruktur gehört, sondern auch wachsende Feature-Wünsche, Code-Refactoring und die Pflege von Versionen von Third-Party-Software.
Empfehlungslisten sind oft gut, etwa Ousterhouts A Philosophy of Software Design, aber insgesamt geht es eher um allgemeine Softwareentwicklung als um Softwarearchitektur selbst.
Wer Architektur verstehen will, sollte zu Klassikern wie Shaw/Garlans Software Architecture: Perspectives on an Emerging Discipline und zu Texten von Mary Shaw greifen.
Auch neuere Papers wie Myths and Mythconceptions: What Does It Mean to Be a Programming Language, Anyhow? oder Revisiting Abstractions for Software Architecture and Tools to Support Them, die untersuchen, warum sich das Feld der Softwarearchitektur nicht wie erwartet entwickelt hat, sind gut.
Praktisch lohnt sich ein Blick darauf, warum Unix Pipes and Filters und REST erfolgreich waren und wo und warum sie scheitern; auch die hexagonale Architektur ist zentral.
Persönlich sehe ich auch eine Verbindung zwischen Softwarearchitektur und Metaobjektprotokollen, etwa in Beyond Procedure Calls as Component Glue: Connectors Deserve Metaclass Status, wo daraus eine neue Grundlage für Programmiersprachen und Programmierung abgeleitet werden soll.
Es ist eine Antwort auf Mary Shaws Procedure Calls Are the Assembly Language of Software Interconnection: Connectors Deserve First-Class Status und fragt: Wenn Prozeduraufrufe Assemblersprache sind, wie würde dann eine Hochsprache aussehen?
Vielleicht hat Softwarearchitektur doch noch eine hellere und praktischere Zukunft.
Man hat ein paar Datenstrukturen und Typen sowie eine kleine Menge elementarer Funktionen und kombiniert sie dann.
Eine Sache, die ich an Lisp mag, ist, dass komplexere Typen, besonders solche aus FFI, immer opak bleiben.
Ich würde gern eine CLOS-Implementierung sehen, bei der man durch das Definieren einer Struktur in einer C-ähnlichen Sprache automatisch ein Standardset von Funktionen erhält.
Architektur lernt man am besten nicht dadurch, dass man ein hinreichend großes Projekt baut, sondern dadurch, dass man es wartet.
Und das sollte man bei mindestens zwei oder drei Projekten tun.
Wenn ein Projekt zu klein ist, funktioniert praktisch jede Architektur; und ob ein Projekt „groß“ ist, sollte man eher an der Zahl der beteiligten Personen, besser noch Teams, messen als an der Zahl der Codezeilen.
Man braucht mindestens zwei unterschiedliche Projekte, um vergleichen zu können, und ich habe Menschen gesehen, die jahrzehntelang in einem einzigen Projekt festsaßen und keine modernen Lösungsansätze kannten.
In der Praxis ist es aber oft so, dass diejenigen, die ein Projekt gebaut haben, zu Architekten befördert werden, während diejenigen, die es gewartet haben, es selten werden.
Bei Google ist das besonders ausgeprägt: Befördert wird man dort vor allem durch neue Launches, nicht durch Wartung, und es ist karriereförderlicher, möglichst kurz nach einem Launch weiterzuziehen.
Paradoxerweise könnten daher externe Auftragnehmer, die auf bestehende Projekte angesetzt werden, die intern niemand anfassen will, in der besten Position sein, Architekten zu werden.
Sie müssen die Architektur erhalten und sehen mehrere Projekte, sodass Vergleiche möglich sind.
Wenn sie allerdings nach Stunden abrechnen, besteht das Risiko, dass sie die Architektur übermäßig verkomplizieren, um mehr Zeit abrechnen zu können.
In diesem Zusammenhang kann ich Architecture of Open Source Applications wirklich empfehlen.
Es ist eine Buchreihe, in der jedes Kapitel von einem Maintainer des jeweiligen Projekts geschrieben wurde, sodass man Architektur anhand von Beispielen lernen kann.
Man erfährt nicht nur, was die Architektur ist, sondern auch, unter welchen Einschränkungen sie entstanden ist, oft inklusive Historie und sich wandelnder Projektvision.
Wegen der Grenzen eines Sammelbands mit vielen Autoren sind nicht alle Kapitel gleich gut oder gleich interessant, und alles ist inzwischen alt, aber es ist trotzdem lesenswert.
http://aosabook.org/
Ich würde gern mehr Zeit darauf verwenden, ein besseres mentales Modell des Projekts zu entwickeln, an dem ich arbeite, aber sobald ich Programmiersprachen, bestimmte Architekturentscheidungen oder Stellen, die so komplex geworden sind, dass sie jede investierte Zeit nicht mehr wert scheinen, zu hassen beginne, sinkt meine Motivation stark.
Das ist je nach Projekt unterschiedlich, aber als „Full-Stack-Entwickler“ zu arbeiten, nimmt mir gefühlt den Spaß am Programmieren.
Ich verbringe ohnehin schon 40 Stunden pro Woche damit, auf das langweiligste vorstellbare Projekt zu starren.
Es gibt kein einziges makelloses Projekt.
Und wenn die Programmiersprache wirklich ein so großes Problem ist, ist es womöglich besser, das Schiff zu wechseln.
Ich finde zwar, dass jeder mit mehreren Sprachen umgehen können sollte, aber am Ende ist das deine Entscheidung.
Man sollte prüfen, ob die Entscheidungen, in die man die meiste Zeit steckt, wirklich die wichtigsten sind.
Gut entworfene Datenstrukturen haben auf Performance und Wartbarkeit weit mehr Einfluss als Frameworks, Sprachen oder Plattformen.
Persönlich muss ich, weil ich jeden Tag mit ADHS arbeite, mich ständig weiter anschieben, damit überhaupt Fortschritt entsteht; das heißt, ich treffe weniger wichtige Entscheidungen gezielt schnell, damit ich mich auf den verbleibenden Problemraum konzentrieren kann, der sorgfältiges Denken erfordert.
Begriffe wie „Clean Code“ oder „schöner Code“ helfen Juniors nicht besonders dabei, Best Practices der Softwarearchitektur zu lernen.
Wenn ein Junior fragt, warum man ein ORM verwendet, und ein Senior nur mit „weil es sauberer ist“ antwortet, bleiben am Ende nur Fragezeichen.
Besser ist es, eine klare Liste von Zielen zu definieren.
Wartbarkeit, Performance und Skalierbarkeit, Effizienz, Resilienz, Observability, Testbarkeit und tatsächlich getesteter Code, Sicherheit sowie gute Lesbarkeit für neue Entwickler stehen dabei in einem Spannungsverhältnis.
Je mehr solcher Kriterien man hat, desto leichter trifft man in Zweifelsfällen bessere Entscheidungen, und sie sind auch für Menschen außerhalb des Entwicklerteams verständlich, sodass man sich mit Kunden darauf einigen kann, wofür bezahlt wird.
Projekte verbringen den Großteil ihres Lebens im Wartungsmodus, was ein gutes Zeichen dafür ist, dass das Projekt erfolgreich war, also kann man auch Wartbarkeit definieren.
Ein guter Ausgangspunkt ist die Fähigkeit, neue Features einzubauen, ohne die Architektur zu brechen, idealerweise nicht einmal die Signatur einer einzelnen Methode.
Mit Abstraktion muss man sehr vorsichtig sein.
Die Aussage „Abstraktion versteckt oft, wie einfach das ist, was man eigentlich will“ trifft zu, und ORMs sind ein typisches Beispiel dafür.
In den meisten Fällen sollten Daten als Bürger erster Klasse behandelt werden, und auch die Zusammenarbeit mit DBAs wird dadurch besser.
Es ist ebenfalls eine Herausforderung, nicht in vorzeitige Optimierung zu verfallen und trotzdem auch außerhalb des Happy Path zu denken; das verhindert, dass man blindlings in die Umsetzung einer vermeintlich guten Idee rennt, ohne die Vor- und Nachteile gesehen zu haben.
Wenn ich mir vorstelle, dass die Person, die nach mir meinen Code anfassen muss, einen wirklich schlechten Tag hat, hilft mir das dabei, den Code angenehmer lesbar zu machen.
Dazu gehören Kommentare an den richtigen Stellen, lokale Variablen, die man auch weglassen könnte, und gute Variablennamen.
Frameworks sollte man mit Bedacht wählen: ein guter Diener, aber ein schlechter Herr.
Die Formulierung aus dem Text „Sei kein Frameworker, sondern ein Ingenieur“ trifft es gut.
Ich habe nichts gegen starke Meinungen an sich; bei Libraries oder Tools kann das sogar eine hervorragende Eigenschaft sein.
Solche Libraries oder Tools bringen wahrscheinlich viel Domain-Expertise für ihren Bereich mit.
Bei Frameworks kippt starke Meinungsfreude jedoch leicht in ein „wer einen Hammer hat, sieht überall Nägel“ um.
Das ist nicht immer so und auch nicht immer problematisch, aber wenn man die Orthodoxie einer Domäne breit anwendet, besteht die Gefahr, dass ihre Begründung domänenspezifisch war und im größeren Kontext zusammenbricht.
Deshalb bevorzuge ich bei Frameworks Modularität: dass man andere ORMs oder Persistenz-Integrationsschichten einstecken kann und Router, Validatoren oder andere Komponenten austauschen kann, die nicht zum Problem passen.
Besonders wertvoll ist es, wenn innerhalb des Framework-Ökosystems Alternativen aus mehreren Paradigmen existieren.
Dann kann man ein nahezu passendes Standardwerkzeug finden, dessen Schwächen klar sind und das sich bei Bedarf ergänzen lässt.
Für Wartbarkeit ist es enorm wichtig, gegen das NIH-Syndrom anzukämpfen.
Das ist ein ständiges Risiko, das Ressourcen in erstaunlicher Geschwindigkeit verschlingt.
Gleichzeitig kann es bei manchen Komponenten große Vorteile haben, sie selbst feinzujustieren oder vollständig zu besitzen; die schwierige Frage ist, zu erkennen, was auf welche Seite fällt.
Ich kann mich sehr mit dem Gedanken identifizieren, Wartbarkeit ganz oben auf die Liste zu setzen.
Aus Sicht des Systems Engineering ähnelt Softwarearchitektur der Planung von Sanitärleitungen.
Sie ist sehr wichtig, aber Menschen wohnen nicht in den Leitungen, sondern in dem Haus, das diese Leitungen hat.
Wenn die Leitungen ohne Rücksicht auf den Rest des Hauses geplant wurden, kann eine Korrektur extrem teuer werden.
Guter Beitrag.
„Softwarearchitektur lernen“ bedeutet zu verstehen, dass es nicht die eine richtige Antwort gibt.
Das ist Kunst und Wissenschaft zugleich.
Als Lektüre empfehle ich Simplify IT - The art and science towards simpler IT solution.
https://nocomplexity.com/documents/reports/SimplifyIT.pdf
Der Vortrag von Gary Bernhardt ist wirklich etwas Besonderes.
Er enthält viele Ideen, die zu anderen interessanten Themen weiterführen.