- Ausgehend von einem Zitat von Linus Torvalds beginnt gutes Design damit, Datenstrukturen und Beziehungen stabil festzulegen, bevor Code geschrieben wird
- Ein gut entworfenes Datenmodell vereinfacht die Anwendungslogik auf natürliche Weise und macht Software zuverlässiger und leichter verständlich
- Wenn man das Datenmodell auf später verschiebt, wächst der spätere Arbeitsaufwand; wer die Struktur früh sauber festlegt, erleichtert Migrationen und den Ausbau komplexer Systeme
- In einem Projekt wurde statt komplexer Algorithmus-Optimierung durch Neuorganisation der Daten die gesamte Problemkategorie beseitigt und eine 500-Zeilen-Funktion durch eine 50-Zeilen-Funktion plus Datenstruktur ersetzt
- In der Praxis sollte man strengere Typen auf Interfaces und Datenbanken anwenden und vor Code-Details zuerst Datenflüsse und Komponenteninteraktionen entwerfen
Datenstrukturen bestimmen das Code-Design
- Linus Torvalds sieht Git als einfaches Design mit stabilen und dokumentierten Datenstrukturen und betont den Ansatz, Code um die Daten herum anzuordnen
- Das Zitat „Schlechte Programmierer sorgen sich um den Code, gute Programmierer sorgen sich um Datenstrukturen und ihre Beziehungen“ ist der Kernsatz
- Einer der Gründe für den Erfolg von Git liegt darin, dass der Code datenorientiert entworfen wurde
- Gute Datenstrukturen erleichtern Code-Design und Wartung und erhöhen die Zuverlässigkeit der Software, die Verständlichkeit des Systems und die Lesbarkeit des Codes
- Anwendungslogik folgt häufig dem Datenmodell
- Wer erst später über das Datenmodell nachdenkt, erhöht den anschließenden Arbeitsaufwand
- Ein gut entworfenes Datenmodell erleichtert spätere Migrationen und den Ausbau komplexer Systeme
- In einem Praxisbeispiel erzielte Neuorganisation der Daten eine größere Wirkung als das weitere Verfeinern eines komplexen Algorithmus
- Durch Änderung der Datenstruktur wurde eine ganze Problemkategorie beseitigt
- Eine 500-Zeilen-Funktion wurde durch eine 50-Zeilen-Funktion und eine gut entworfene Datenstruktur ersetzt
- Der neue Code wurde schneller sowie leichter zu verstehen und zu warten
- Allerdings verlagerte sich der Aufwand in die darunterliegende Schicht, weil bestehende Daten neu organisiert werden mussten
Komplexität sollte eher in die Daten verlagert werden
- Die „Rule of Representation“ aus The Art of Unix Programming erklärt, man solle Wissen in Daten ablegen, um Programmlogik einfach und robust zu machen
- Prozedurale Logik ist für Menschen schwer zu prüfen, komplexe Datenstrukturen lassen sich dagegen leichter modellieren und durchdenken
- Ein Diagramm eines Pointer-Baums mit 50 Knoten kann ausdrucksstärker und erklärender sein als ein Ablaufdiagramm eines 50-Zeilen-Programms
- Wenn eine Transformationstabelle als Array-Initialisierung dargestellt wird, ist das transparenter und klarer, als denselben Inhalt mit einer
switch-Anweisung zu schreiben - Wenn man entscheiden muss, ob Komplexität im Code oder in Datenstrukturen liegen soll, ist es besser, sie in die Datenstrukturen zu verlagern
In der Praxis wird zuerst der Datenfluss entworfen
- Der direkteste praktische Ansatz ist, bei den Daten anzufangen
- Strengere Typen für Interfaces oder Datenbanken können Code-Komplexität reduzieren
- Es braucht mehr Zeit, um im Voraus länger über Datenstrukturen nachzudenken
- Das bedeutet nicht, dass Code unwichtig ist; alle Elemente sind gemeinsam wichtig
- Bevor man in Code-Details einsteigt, ist ein übergeordneter Ansatz hilfreich, der zuerst betrachtet, wie Daten fließen und wie Komponenten miteinander interagieren
- Als Beispiel für Anforderungen an Senior Engineers (L5) gehört bei FAANG üblicherweise das Schreiben von High-Level-Design-Dokumenten für komplexere Systeme dazu
- Dazu zählen auch das Vorantreiben der Teamplanung und der Aufbau guter Roadmaps für mittelgroße bis große Funktionen
- Die Fähigkeit, zuerst Datenflüsse und Komponenteninteraktionen zu entwerfen, steht mit größerem Engineering-Einfluss auf höherem Niveau in Verbindung
1 Kommentare
Meinungen auf Hacker News
Dieser Substack-Beitrag wirkt, als hätte er einfach mehrere Zitate aus diesem Stack-Exchange-Beitrag übernommen: https://softwareengineering.stackexchange.com/questions/1631...
„Zeig mir deine Flussdiagramme [Code] und verbirg deine Tabellen [Schemas], dann werde ich weiter rätseln. Zeig mir deine Tabellen [Schemas], und ich werde deine Flussdiagramme [Code] normalerweise nicht brauchen; sie werden offensichtlich sein.“ — Fred Brooks, „The Mythical Man Month“, Kapitel 9
„Lies The Mythical Man Month. Ich erinnere mich, dass ich dachte, ein so altes Buch könne zur heutigen Softwareentwicklung nichts Relevantes mehr sagen, aber ich lag falsch.“
Dann beginnen Anwendungsentwickler, die Datenbank zu missbrauchen, weil sie schneller sind und mehr zu tun haben.
Datenstrukturen und Typen sind nicht dasselbe. Eine Datenstruktur besteht aus Bitmustern und Verweisen auf andere Bitmuster, also aus Pointern oder Beziehungen.
Typen schränken solche Bitmuster in der Art ein, wie sie in Programmiersprachen verwendet werden, können darüber hinaus aber viele Sprachfeatures ausdrücken. Durch unnötige Abstraktion komplexe Typhierarchien zu bauen, bedeutet nicht, dass man sich „um Datenstrukturen kümmert“, und ist ein Fehlermuster, in das auch kluge Engineers häufig geraten.
Typen sind Bits auf der Festplatte.
Gemeint war hier wohl eher, dass man tiefer über das Problem nachdenken und keine Struktur wählen sollte, die einem später im Weg steht. Man sehe sich zum Beispiel an, wie weit sich Unix-Pipes verbreitet haben und auf wie viele Bereiche und Anwendungsfälle sie ausgeweitet wurden. Sie sind eine hervorragende Art, sich vorzustellen, wie man Systeme baut, die die Grenzen von Menschen und Maschinen respektieren.
Auch Ken Thompson und andere brauchten eine ganze Weile, bis sie erkannten, dass etwas wie Pipes in Unix sinnvoll ist. Diese Einsicht kam nicht leicht; sie erforderte Beharrlichkeit und Folgearbeit, um die richtigen Bausteine des Systems zu finden.
Linus fasst immer gut zusammen, was andere nur vage im Kopf haben. Was der Text beschreibt, ähnelt auch DDD, das zu einer verlorenen Kunst geworden ist.
Mit „verloren“ meine ich, dass die meisten Entwickler, denen man heute begegnet, mehr daran interessiert sind, Algorithmen und JSON hin- und herzuschieben, als die Domain zu verstehen, mit der sie arbeiten, und Entitäten sowie deren Interaktionen zu modellieren. In modernen AWS-basierten Designs zeigt sich das als Bündel schlecht begründeter DynamoDB-GSIs, anämische Objekte und „Service“-Schichten wie Skripte, in denen Hack auf Hack gestapelt wird. Vielleicht gab es die implizite Annahme, dass der Domain-Kontext innerhalb von Service-Grenzen hinreichend gut definiert sein würde, aber ich halte das nicht für eine gute Annahme.
Ich weiß nicht, wo unsere Branche die Strenge im Design verloren hat. Ob in der Ausbildung, in den Interview-Pipelines, durch gesenkte Standards oder durch all das zusammen.
Schlimmer noch: Design begeht die große Sünde, sich nicht leicht automatisieren zu lassen. Deshalb folgen die Leute unkritisch dem Design, das ihnen Tools aufzwingen, und fühlen sich unwohl bei dem Gedanken, tiefer über das nachdenken zu müssen, was sie tun. Alle wollen dieses Denken an „Experten“ auslagern.
Ein weiteres Problem ist, dass es nicht richtig gelehrt wird, man es sich über Jahre selbst aneignen muss und es als weniger real als Code gilt und daher als weniger wichtig behandelt wird. Aber dieser Glaube begrenzt letztlich das Niveau dessen, was man bauen kann, auf das eines fortgeschrittenen Anfängers. Programmierer entscheiden sich kollektiv dafür, die Messlatte möglichst niedrig zu halten, und bei diesem Thema scheint es fast eine krabbenartige Mentalität zu geben, sich gegenseitig herunterzuziehen.
Außerdem entsteht viel Code-Duplizierung, die syntaktisch nicht identisch ist und daher nicht wie Duplikation aussieht, funktional aber dasselbe tut.
1 https://martinfowler.com/bliki/AnemicDomainModel.html
2 https://wiki.c2.com/?PrimitiveObsession
Ich denke, das liegt daran, dass die Folgen schlechten Codes weniger sichtbar sind. Eine schlechte Brücke stürzt ein, schlechter Code wird nur refaktorisiert oder durch noch mehr Code ersetzt. Eine Textdatei, die das Management nicht versteht, wird einfach durch eine andere Textdatei ersetzt, die das Management ebenfalls nicht versteht.
Und sobald etwas funktioniert, herrscht Funkstille. Nichts ist so dauerhaft wie ein provisorischer Hack, der perfekt funktioniert. Aber 1000 provisorische Hacks ergeben kein gut entwickeltes System. Reife in der Softwareentwicklung bedeutet meiner Ansicht nach, sich stärker auf Daten und Beziehungen zu konzentrieren als auf das Schreiben von Code. Man muss das in Code übersetzen können, aber nicht funktionierenden Code in ein Datenmodell verwandeln, sondern Daten und Beziehungen in Code.
Nur weil eine Domain-Entität die gesamte Logik enthalten kann, heißt das nicht, dass sie das auch sollte. Wenn man zum Beispiel prüfen muss, ob ein Benutzername bereits existiert, wie soll das in einer Domain-Entität geschehen, die nicht von der Datenzugriffsschicht „abhängen darf“? Häufig wird dann so etwas wie ein „Domain Service“ empfohlen, aber damit verteilt sich Business-Logik auf mehrere Stellen, was sich für mich wie das Gegenteil der DDD-Idee anfühlt.
Als Philosophie mag ich DDD ziemlich, aber die Muster des „taktischen DDD“ lehne ich extrem ab. Ich denke, zu viele Menschen setzen Domain-Driven Design mit Domain-Driven Implementation gleich. Wo es passt, versuche ich reichhaltige Domains zu bauen, aber das passt nicht zu jedem Projekt, und ich will mich nicht in Begrifflichkeiten verlieren. Ob ein „Name“-Typ ein Value Object oder ein Aggregate Root ist, interessiert mich nicht. Vor allem sind Bounded Contexts wichtiger. Ich erkenne auch an, dass DDD manchmal die Komplexität einer Anwendung erhöhen kann, ohne viel zu bringen. Ich würde es keinesfalls als Allheilmittel bezeichnen.
Ich werde DDD weiterhin verwenden, aber ich werde das Gefühl nicht los, dass DDD ein Versuch ist, zu vermitteln: „Seht ihr, objektorientierte Programmierung ist doch gar nicht so schlecht, oder?“ Und ich bin mir nicht einmal sicher, ob es dieses Ziel erreicht.
Ich finde das interessant, weil ich vor meinem Einstieg in professionelles Engineering täglich Daten- und Statistikanalysen mit Statistiksystemen wie Matlab, R und frühem Python gemacht habe.
Deshalb beruhte meine Engineering-Perspektive immer auf zwei Dingen: der Verwaltung von funktionalem Zustand und von Daten-Workflows.
Nach zehn Jahren beruflicher Softwareentwicklung habe ich festgestellt, dass die meisten „wissenschaftlichen“ Ingenieure wie Minsky oder Shannon die Welt des Computings über Zustandsverwaltung, Datentransformation und das Management von Rechen-Overhead beschrieben haben. Alle Größen und Pioniere der Software nahmen Daten und Zustand sehr ernst; in der frühen Computertechnik war das im Grunde alles, und man erwartete, dass sich dieses Muster fortsetzen würde.
Dagegen gibt es bei den grundlegenden Annahmen im Engineering-Systemdesign, die angeblich immer wahr sind und denen daher alle folgen, keinerlei Konsistenz; wenn überhaupt, sind sie meist eher Moden. In der meisten Betriebssoftware bestimmen Business-Zeitpläne die Engineering-Prioritäten und die Struktur sehr viel stärker als Robustheit, Antifragilität oder Zustandsverwaltung.
Berufsorganisationen wie Gilden oder Gewerkschaften werden von Softwareentwicklern nahezu universell abgelehnt. Da es keine Nachteile hat, IEEE nicht ernst zu nehmen, nimmt es tatsächlich niemand ernst. In der Folge gibt es keine Mechanismen, die die Praxis erzwingen oder selbst regulieren, wie im Bauingenieurwesen oder in der Biomedizintechnik, und selbst dort werden sie nur gerade so genutzt.
Insgesamt ist der aktuelle Zustand der Softwareentwicklung vollständig von ihren sehr hochgesinnten und philosophischen Wurzeln getrennt und wird faktisch von Unternehmen geprägt, die Systeme priorisieren, mit denen Menschen mit Geld noch mehr Geld verdienen. Daher hat das, was „gut“ ist, kaum etwas mit dem zu tun, wofür Anreize gesetzt werden.
„Wenn du mir deine Flussdiagramme [Code] zeigst und deine Tabellen [Datenstrukturen] verbirgst, werde ich weiter ratlos sein. Zeigst du mir deine Tabellen, brauche ich in der Regel keine Flussdiagramme. Sie werden offensichtlich sein.“ — Fred Brooks
Eine 1:1-Abbildung auf die zugrunde liegenden Tabellen ist extrem einschränkend und führt meiner Ansicht nach zu Modellen, die die Ausdrucksstärke moderner Sprachen nicht nutzen
Das ist im Kern die Perspektive der funktionalen Programmierung und der Kategorientheorie.
Es gibt ein bestimmtes Datenobjekt, und dessen Struktur gibt Einschränkungen dafür vor, wie es transformiert werden kann. Die gesamte Programmlogik dreht sich dann um Transformationen, die diese Struktur erhalten.
Die Transformationen werden einfacher und leichter nachvollziehbar, und am Ende bleibt ein Graph, in dem die Transformationen die Kanten und die Strukturen die Knoten sind. Das ist im Allgemeinen leichter zu durchdenken als ein beliebiges imperatives Programm
Die Perspektive der funktionalen Programmierung geht eher dahin, dass man Objekte nicht verändern sollte und Änderungen vermeiden sollte; das ist von dieser Diskussion getrennt. Im Kern der Kategorientheorie geht es um Beziehungsmuster, die in verschiedenen Bereichen der Mathematik gemeinsam auftreten, und auch das hat mit dem hier Diskutierten überhaupt nichts zu tun. Vielleicht war Typentheorie gemeint, aber auch die ist nicht relevant
Meine frühere Schlussfolgerung lautet: Alles, was wir im Code tun, wird mit hoher Wahrscheinlichkeit viel kürzer überdauern als eine gute Entscheidung über Daten.
https://www.swyx.io/data-outlasts-code-but
Dieses Prinzip gilt auch auf Business-Ebene. Ich habe ständig mit Business-Analysten zu tun, die sich auf Prozesse (Code) fixieren, ohne zuerst Zeit darauf zu verwenden, die Entitäten und ihre Beziehungen (Daten) zu verstehen.
Wenn es dann darum geht, etwas zu bauen, können sie den Entwicklern nicht vermitteln, wie das Datenmodell aussehen sollte. Die Prozesse werden implementiert, und das Datenmodell wird nicht sorgfältig entworfen, sondern ad hoc zusammengestückelt