Wir engen uns beim Umgang mit Daten selbst ständig ein. Das passiert, weil wir uns der Freiheitsgrade und der Unsicherheit, in der wir uns bewegen, nicht recht bewusst sind.
Bei der Funktionalität ist es interessanterweise umgekehrt. Da wollen wir uns keine Tür durch vorschnelle Entscheidung verschließen. Der Grund ist allerdings derselbe: hohe Unsicherheit.
Das Ergebnis ist Software mit fixen Datenstrukturen und durch Allgemeingültigkeit verrauschten Code. Und der Preis dafür ist hoch. Die fixen Datenstrukturen widersetzen sich natürlich die notwendig häufigen Änderungen – denn es ist ja unzweifelhaft, dass wir in hoher Unsicherheit programmieren. Und der Code, der nach allen Seiten offen sein will – und deshalb eher nicht ganz dicht ist ;-) - ist schwer lesbar und noch schwerer veränderbar. Intentionen sind nicht klar zu erkennen und verschmiert über die Codebasis.
Ich sehe aus dieser Situation nur einen Ausweg: Wir müssen auf der einen Seite allgemeiner und auf der anderen spezifischer werden. Schlicht etwas mehr Lockerheit täte uns gut. Im Raum der Freiheitsgrade, die wir haben, sollten wir uns bewusster verorten.
Freiheitsgrad #1: Intention
Bei der Funktionalität bewegen wir uns solange wie es geht im Allgemeinen. Das Resultat sind die allgegenwärtigen CRUD-Benutzerschnittstellen. Weil wir nicht vorhersehen können oder wollen, was Anwender mit Daten tun möchten, lassen wir einfach alles zu. Statt eines Restaurants bieten wir ihnen einen Supermarkt. Selbst ist der Anwender. Soll er die rohen Datenstrukturen doch selbst formen.
Das macht drei Probleme: Erstens zwingt uns das zu frühzeitigen Entscheidungen über Datenstrukturen, weil wir sie ja dem Anwender zur Befüllung vorgeben müssen. Zweitens bindet das uns wie Anwender an diese Datenstrukturen, da sie nun Teil des Kontraktes zwischen Software und Anwender sind. Und drittens senkt das letztlich die Effizienz der Bedienung, weil Anwender selbst ihren Weg zur Bewältigung einer Aufgabe durch Datenstrukturen finden müssen.
Auf der Achse “Funktionale Konkretheit” legen wir uns häufig, viel zu häufig auf der linken Seite fest:
Stattdessen sollten wir uns weiter nach rechts orientieren. Die Benutzerschnittstellen sollten spezifischer, fokussierter sein in ihren Angeboten. Statt einem Dialog für 10 Zwecke lieber 10 Dialoge mit einem Zweck. Wir brauchen mehr Benutzerschnittstellen, die konkrete Angebote für die Intentionen der Anwender machen.
Dafür müssen wir die Domäne natürlich genauer kennen. Das klingt nach mehr Aufwand up-front. Aber ich glaube, das Gegenteil ist der Fall – zumindest, wenn wir uns von dem Glaubenssatz verabschieden, dass wir nicht so kleinteilig liefern können, weil spätere Erweiterungen dann schwierig werden.
Die Vorteile von intentionalen oder aufgabenorientierten Benutzerschnittstellen scheinen mir einfach zu groß, als dass wir sie ignorieren sollten. Anwender werden effizienter und wir befreien uns von der Bürde, einem Kontrakt treu zu bleiben, der die Anwender ohnehin nicht interessieren sollte. Ganz zu schweigen davon, dass dann unser Code klarer würde.
Was dann aber tun mit den Daten? Die würden dadurch ja befreit. Wir würden sie ja immer nur durch kleine Gucklöcher präsentieren. Ein Big Picture von Datenstrukturen wäre für keinen Anwender sichtbar. Wir müssten uns also nicht mehr up-front für ein solches Big Picture entscheiden.
Freiheitsgrad #2: Schema
Derzeit starren wir auf Datenbankstrukturen und in-memory Domänenobjektmodelle wie das Kaninchen auf die Schlange. Es ist Heulen und Zähne klappern. Wir haben Angst, uns falsch zu entscheiden bei Strafe, von Ineffizienz verschlungen zu werden.
Das ist ein grauenhafter Zustand, den wir aber gelernt haben, als unausweichlich, ja normal anzusehen. Es kann nicht anders sein. So funktioniert Softwareentwicklung eben. Am Anfang muss man sich halt für Datenstrukturen entscheiden. Und zwar am besten für so wenige wie möglich. “One size fits all” ist sogar das Beste.
Aber auch hier engen wir uns künstlich und unbewusst ein. Wir sollten lernen, ein Kontinuum zu sehen:
Wenn wir nach außen hin kein Big Picture von Datenstrukturen mehr liefern müssen, dann können wir uns auch intern davon befreien. Die traditionelle Objektorientierung mit ihrem Fokus auf Daten ist eine Verirrung. Sie ist verständlich, war vielleicht angesichts knapper Hardwareressourcen auch unumgänglich – deshalb müssen wir sie ja aber nicht unnötig fortsetzen.
Auf den Schultern der relationalen Datenstruktureffizienzdenke der 1970er haben wir geglaubt, wir müssten möglichst früh und möglichst spezifische Datenmodelle entwerfen. Das waren wir dem knappen Speicher und der Konsistenz schuldig. Und auch der Funktionalität, die damit scheinbar einfach eine Heimat findet.
Aber was ist das Ergebnis? Entwickler, die an sich zweifeln, weil sie es nicht hinkriegen, Funktionalität zügig zu verorten. Code und Daten, die sich notwendigen Veränderungen widersetzen.
Wie anders könnte aber die Welt aussehen, wenn wir nicht mehr versuchen würden, Daten in “one size fits all” Schemata zu pressen? Geben wir diese Optimierung auf. Sie ist angesichts der Unsicherheit der Anforderungen vorzeitig. Wir haben keine Ahnung, was der Kunde wirklich will und wohin das alles führt? Dann sollten wir genau das auch mit unseren Daten ausdrücken.
Für mich bedeutet das, weniger up-front in Schemata zu denken und stattdessen Daten kleinstteilig in Bezug auf konkrete Funktionalität zu denken. Das Ergebnis sind Events, also Datendifferenzen. Die können hunderte Formen/Schemata haben – und ergeben nur zusammen ein Big Picture. Das existiert jedoch nicht vorab, sondern entsteht im doppelten Sinn erst über die Zeit.
Freiheitsgrad #3: Bevorratung
Kann man denn aber ohne Schema, d.h. ohne “das eine” Schema Software schreiben? Natürlich. Operationen können auch auf Events arbeiten: das ist dann (Complex) Event Processing ((C)EP).
Einen kleinen Eindruck habe ich davon in meinem vorherigen Blogartikel über Tic Tac Toe gegeben. Alle Domänenfunktionen kommen dort ohne eine spielspezifische Datenstruktur aus. Ein zweidimensionales Spielbrett wird ausschließlich für den Anwender hergestellt.
Größere Schemata – ob in-memory oder persistent ist egal – sind deshalb aber nicht “böse”. Sie haben ihren Zweck. Besonders, wenn man genau weiß, wie sie für einen konkreten Zweck aussehen sollten. Dann erhöhen sie Verständlichkeit und Effizienz.
Doch die Frage ist, wann und wie lange sollten Daten in solchen größeren Strukturen vorliegen? Wann ist Zustandsherstellung und –haltung angezeigt im Gegensatz zur Arbeit auf Zustandsänderungen (Events).
Ich denke, das muss im Einzelfall entschieden werden. Womit ich wieder bei den Intentionen bin. Je intentionaler die Benutzerschnittstelle, desto größer unsere Freiheit, uns immer wieder für die beste Art der Bevorratung von Zuständen in fixen Schemata zu entscheiden.
Mein Gefühl ist, wie halten Daten zu lange in zu großen Strukturen. Wir stecken in einem Mangeldenken fest. Wie Eichhörnchen bevorraten wir zu viel. Dabei ist die Welt doch schon längst im JIT-Zeitalter angekommen. Waren werden Just-in-Time geliefert und sogar Maschinencode wird JIT in kleinen Happen erzeugt. Warum gehen wir nicht so mit unseren Daten um? Wir haben die Prozessorressourcen, um kleine Datenstrukturen individuell für den aktuellen Anwendungskontext JIT zu füllen.
Schluss mit dem einen Objekt für Kunde, Rechnung, Spiel, Vertrag, Gerät, Fragebogen, Auktion oder was immer die Domäne sein mag. Schluss mit dem einen Schema! Schluss mit der dauerhaften Speicherung in dem einen Schema! Schluss mit der zwanghaften Kombination von Daten und Funktionalität [1].
Stattdessen: Mehr Logik direkt auf Events und auf fokussierte, JIT befüllte Datenstrukturen auslegen. OR-Mapping adé – zumindest für Datenroundtrips. Technologisch einfacher werden, um schneller und flexibler zu sein.
Freiheitsgrad #4: Zweck
Wo kleinere Datenstrukturen JIT befüllt werden, da stellt sich natürlich auch die Frage nach dem Zweck. Folgen Datenstrukturen dem Prinzip, das wir für Funktionalität so hoch halten? Haben Datenstrukturen auch immer nur eine Single Responsibility?
Nein, ich glaube, wir überlasten sie. Wir denken vor allem in Allzwecksdatenstrukturen. Ist ja auch klar: je weniger es gibt, desto mehr Zwecke müssen die erfüllen.
Dass solche Wollmilchsäue schnell unwartbar werden, ist kaum verwunderlich. Wenn wir es ernst mit Clean Code meinen, dann müssen wir das Tabu ins Visier nehmen, dass an den großen Allzweckschemata nicht zu rütteln sei. Ob ein Datenbankschema oder ein OO-Domänenmodell, ist egal. Zustand, d.h. akkumulierte Veränderungen, sollte in verschiedenen Formen vorliegen, die auf konkrete Zwecke zugeschnitten sind. Die Unterscheidung zwischen Lesen und Schreiben wie bei CQRS ist da nur ein Anfang. Am Ende kann, darf, sollte es viele verschiedene flüchtige und persistente Datenstrukturen geben.
Und alle sollten sich aus einer Quelle jederzeit re-generieren lassen. Soviel zum Thema Konsistenz. Die Datenwahrheit lebt wie in Zeiten des “Relationnismus” an nur einem Ort. Das scheint mir derzeit ein Event Store oder – das Bild gefällt mir eigentlich besser – eine Black Box.
Fazit
Sie sehen, ich bin derzeit beseelt vom Thema Event Sourcing ;-) Aber es ist einfach so, dass darin für mich vieles zusammenläuft, was bisher getrennt nach einer Lösung gesucht hat. Und allemal sehe ich darin eine Antwort auf die ewig große Frage der Softwareentwicklung: Wie umgehen mit der Unsicherheit und Flüssigkeit der Anforderungen?
Bei all den nicht-funktionalen Anforderungen der Kunden scheint mir das die größte und gleichzeitig die unbewussteste: “Ich möchte mich so wenig wie möglich festlegen müssen.”
In Handwerker- oder auch Ingenieursmanier laufen wir dagegen jedoch Sturm. Seit Jahrzehnten. Es widerspricht allem, was wir in Tausenden Jahren gelernt haben. Sagen wir es denn nicht auch unseren Kindern, “Du musst dich entscheiden lernen!”?
Was aber, wenn man das aus vielfältigen Gründen nicht kann? Dann ist es doch widersinnig, es immer wieder zu fordern und sein Tun darauf auszulegen. Dann sind fixe Strukturen für Daten und Funktionalität kontraproduktiv.
Also sollten wir unsere Freiheitsgrade ausreizen. Sonst unterscheiden wir uns nicht von Fundamentalisten, die ständig auf dem Selben beharren, weil es nur so sein kann weil es immer so war und nur so sein darf.
Lernen wir, über den Tellerrand des bisher Kanonischen hinaus zu blicken. Das braucht natürlich Experiment und Übung. Da werden wir auch scheitern und mal zu weit gehen. Macht aber nichts.Wir können nur gewinnen. Denn eines ist ja klar: So wie es ist, kann es nicht bleiben. Wir ersticken an Inflexibilität, d.h. an Unfreiheit zur Veränderung.
Machen wir uns also auf den Weg. Ich würde sie gern hier sehen:
Endnoten
[1] Damit meine ich nicht, dass wir Objekte aufgeben sollten. Dass wir Daten und Funktionalität kombinieren können, soll erhalten bleiben. Ich bin also kein jünger extremer Funktionaler Programmierung. Aber wir sollten uns genauer als bisher überlegen, welche Funktionalität wir mit Daten zusammenfassen.
Funktionaltität, die der Konsistenz einer Datenstruktur dient (Stichwort: Abstrakter Datentyp (ADT)), gehört natürlich zu den Daten. Aber da gibt es für mich eine Hierarchie.
Ein Objekt, dass Name, Anschrift und Telefonnummer einer Person zusammenfasst, ist eine Datenstruktur, die nur wenig Funktionalität verträgt. Zum Beispiel könnte es sicherstellen, dass der Name nie leer ist. Genauso wie ein Stack sicherstellt, dass Pop() das zuletzt mit Push() eingelegte Element entnimmt.
Ein Objekt hingegen, dass ein solches Personenobjekt enthält (!), kann Funktionalität auf einer höheren Ebene haben. Zum Beispiel kann es zuständig dafür sein, dass keine zwei Personen mit derselben Telefonnummer existieren.
Wann jedoch ein Objekt andere enthalten sollte, also einen Zustand haben sollte, auf dem es arbeitet, und wann es andere erhalten sollte, also auf einem Fluss wechselnder Objekte arbeiten sollte, das ist eine andere Frage.
5 Kommentare:
Vielen Dank für diesen (wieder einmal sehr gelungenen) Post.
Ich denke auch dass es Zeit ist, dass "Anwendungsentwickler" und "Datenbänkler" voneinander lernen - Stichwort Fokus auf Funktionalität vs. Fokus auf Datenstrukturen und -verarbeitung (z.b. bei Beladungs- und Transformationsprozessen, ETL).
Aber:
Je nachdem wie nah der Kunde an "seinen" Datenstrukturen dran ist, wird es schwierig ihn davon zu lösen, und ihm den genannten Mehrwert von spezifischen Dialogen zu verkaufen.
Viele Grüße
Johannes
Das sehe ich genauso.
Dazu muss man die Daten oefter einmal "umformen", aber dadurch hat man alle Freiheiten, z.B. beim Speichern im Repository.
Im allerschlechtesten Fall nimmt man ein GUI-Werkzeug, das direkt auf den DB-Tabellen aufsetzt. Da erzielt man schnell Erfolge. Aber wehe, wenn die DB-Tabellen geaendert werden (muessen)! Das ist bei grossen Systemen nicht mehr beherrschbar...
"...Ein Objekt, dass Name, Anschrift und Telefonnummer einer Person zusammenfasst, ist eine Datenstruktur, die nur wenig Funktionalität verträgt. Zum Beispiel könnte es sicherstellen, dass der Name nie leer ist..."
Fällt dies nicht in den Zuständigkeitsbereich einer Validierung und stellt somit einen eigenen Belang dar (Seperation of concerns)?
Datenstrukturen, finde ich, sollten "as stupid as possible" sein.
Je mehr ich drüber nachdenke, desto schwerer tue ich mich auch mit den uns durch Programmiersprachen vorgegebenen Strukturen. Z.B.:
Usercontrols besitzen Positionsangaben (Top, Left,...). Wieso muss ein Usercontrol solche Informationen tragen. Das bedeutet doch, dass es sich seiner Umgebnung bewusst ist. Wenn ich ein Usercontrol im Code erzeuge, also nicht direkt auf ne Form 'raufbatsche', schon dann machen solche Properties (also Daten) überhaupt keinen Sinn mehr. Wenn ich eine Vase auf einen Tisch in einen Raum stelle, warum soll dann die Vase wissen, wo sie sich befindet? Wo sich die Vase befindet und welche anderen Gegenstände, das sind doch Informationen, die ein Raum besitzen muss.
Genauso die Verortung von Funktionalitäten auf Datenobjekten...
Wenn ich auf meiner Pizza neben Tomaten und Schinken zusätzlich auch Champignons haben möchte, dann sag ich doch nicht der Pizza, dass sie Champignons hinzufügen soll, coded à la Pizza.Belaege.Add("Champignons"); oder Pizza.BelagHinzufügen("..");
Und weil ich die Pizza immer gerne so esse sag ich dann auch noch Pizza.Save(); oder was.
Erstaunlich - eine Pizza kann sich selber speichern... Respekt!
Wenn ich nun Champignons auf der Pizza haben will, dann bin ich es doch, der sie auf die Pizza packt. Eine Pizza, die so schlau ist, sich selber Champignons hinzuzufügen und ggf. noch zu sagen "nö, geht nicht, sind schon Pfifferlinge drauf"... nein, so eine intelligente Pizza würd' ich nicht essen wollen :-)
Sry, das war jetzt ein wenig abgeschweift, aber irgendwie musste das mal raus.
Gruß,
Mirko Schneider
@Mirko: Was für Funktionalität eine Person-Klasse haben kann, ist natürlich zu bedenken. Ich sage ja auch nicht, dass sie die von mir genannte besitzen muss. Aber für mich wäre die noch plausibel.
Ein Stack hat Methoden, um eine gewisse Semantik und Konsistenz sicherzustellen. Das ist ein ADT.
Was das für eine Person bedeutet, liegt an der Domäne. Wenn ich sage, sie stellt sicher, dass der Name nicht leer ist, dann finde ich das sehr strukturell. Es wird ja nicht festgestellt, ob der Name nur einmal vergeben wurde oder mit einem Großbuchstaben beginnt. Sondern es wird nur sichergestellt, dass eine Person immer irgendeinen Namen hat, so wie ein Stack sicherstellt, dass Neues über Altem liegt. Fertig.
Datenklassen können Methoden haben. Aber natürlich: Vorsicht! Don´t get carried away! :-)
@Ralf:
Datenklassen sind da, um Daten zu halten. Das ist ihr Zweck und kein anderer.
Die Strukturelle Integritäts- bzw. strukturelle Prüfung ("Name ist nicht leer") ist ein Prozess.
Daten fließen in Prozesse hinein; die Prozesse erzeugen Datenoutput. Z.B.
(Personendaten) -> Strukturelle_Integritaet_prüfen -> (Personendaten, Prüfungsergebnis) -> ....
"Datenklassen können Methoden haben", sagst Du.
Technisch gesehen ohne Zweifel ja, aber ich denke es verletzt das SRP.
Gruß,
Mirko
Kommentar veröffentlichen
Hinweis: Nur ein Mitglied dieses Blogs kann Kommentare posten.