Follow my new blog

Posts mit dem Label Event Sourcing werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Event Sourcing werden angezeigt. Alle Posts anzeigen

Mittwoch, 20. August 2014

Konstruktivistische Softwareentwicklung für mehr Wandelbarkeit

Code sei ein Mittel, um die Realität abzubilden. Zumindest scheint mir das ein wesentlicher Antrieb hinter der Objektorientierung zu sein. Mit Objekten sollte es endlich möglich sein, Dinge und Verhältnisse der Welt möglichst 1:1 in Code zu fassen.

Das ist eine platonische Sichtweise der Welt. Sie geht davon aus, dass es etwas unabhängig von Code gibt, das irgendwie ist. Und dieses So-sein kann man mehr oder weniger getreu in Code abbilden. Und je getreuer es abgebildet wird, desto besser ist das für die Software in irgendeiner Weise.

Ich glaube, dieses Weltbild sollten wir nun hinter uns lassen. Es hat uns zu der Software geführt, die wir haben. Die funktioniert, die skaliert - aber die ist nur schwer wandelbar.

Vom Platonismus sollten wir zum Konstruktivismus wechseln.

Wenn wir konstruktivistisch Software entwickeln, dann ist es nicht mehr wichtig, eine Realität getreu abzubilden.

Bei der platonischen Softwareentwicklung gibt es drei Bereiche, die in Deckung sind: die Realität (Domäne), unsere Wahrnehmung der Realität (Entwickler), Code. Als Entwickler einer Buchhaltungssoftware sehe ich eine Rechnung (Domäne) und lege dafür eine Klasse Rechnung und eine RDBMS-Tabelle Rechnungen an.

Bei der konstruktivistischen Softwareentwicklung hingegen, muss das, was ich als Entwickler in der Realität erkenne, nicht im Code auftauchen. Auch wenn ich eine Rechnung als Gegenstand in der Hand halte, führt das nicht Zwangsläufig zu einer Klasse oder einer einer Tabelle.

Leider muss ich hier von drei Bereichen sprechen, weil Software sich nicht selbst anpassen kann. Wir als Entwickler müssen das als Mittler tun. Deshalb haben wir bisher auch versucht, unsere Sicht der Welt in der Software zu manifestieren.

Aber es geht gar nicht um uns. Wenn wir für unser Überleben Rechnungen, Autos, Katzen, Götter, ein Ich in der Welt sehen wollen, dann ist das unsere Sache. Das ist unsere Konstruktion, die über das gelegt ist, was irgendwie ist.

Der Stoff aus dem die Konstruktionen sind, sind unsere sinnlichen Wahrnehmungen und a priori Grundkonzepte. Die haben wir uns nicht ausgesucht, sondern sie definieren uns. Die Evolution hat dazu geführt, dass wir als Organismen so sind, wie wir sind. Mit unserer Form und unseren Wahrnehmungen haben wir größere Stabilität in einer bestimmten Umwelt erreicht als mit einer anderen Form und anderen Wahrnehmungen.

So ist das mit Evolution. Es geht um größere Stabilität von Strukturen in einer (Um)Welt.

Und so ist es auch mit der Softwareevolution. Es geht darum, wie Software größere Stabilität erreicht in einer stetig im Wandel befindlichen Umwelt.

Wir haben es nun einige Jahrzehnte versucht, die Lebensfähigkeit durch getreuer Abbildung von durch uns wahrgenommener “dinglicher” Realität zu erhöhen. Das hat nicht so geklappt, wie gewünscht, würde ich sagen. Und nur das zählt.

Also sollten wir es anders versuchen. Befreien wir Software vom Abbildungszwang. Das einzige was zählt ist, dass Software als Ganzes ihren Zweck erfüllt (Funktionalität + Qualität) und sich zügig an eine gewandelte Umwelt anpassen lässt (Investitionssicherheit).

Die Struktur von Software muss also nicht zwangsläufig irgendetwas widerspiegeln, was wir als Menschen als Dinge in der Welt erkennen. Wenn wir ein Formular auf dem Tisch liegen haben, dann mag es noch hilfreich sein, das Formular in der Software als geDialog wiederzufinden. Ja, vielleicht ist das so. Vielleicht aber auch nicht. Machen wir uns da mal ganz locker.

Vor allem sollte uns ein Formular auf dem Tisch nicht dazu verleiten, unterhalb der Oberfläche im Code das Formular nochmal zu erschaffen. Und dann ein weiteres Mal auf der Festplatte - weder als Verbund von RDBMS-Tabelle noch als NoSql Dokument.

Damit will ich nicht sagen, dass das nicht so sein darf. Vielleicht ist es hier und da vorteilhaft, das Formular im Code wiederzufinden. Aber wir sollten das sich ergeben lassen und nicht im Sinne eines Weltbildes an den Anfang setzen.

Wenn wir schon mit dem Paradigma unserer Hauptwerkzeuge, den Programmiersprachen, Softwarestrukturen einem platonischen Weltbild unterwerfen, dann schränken wir die Freiheit der Evolution von Software ein. Wir machen es ihr schwer, in einer fluktuierenden Umwelt zu überleben, weil wir sie massiv fixieren.

“Abbildung der Realität” ist aber nicht, worum es geht. Einziger Markstein ist Zufriedenheit des Kunden - zu der gehört, dass Software quasi unsterblich ist, weil sie sich auf ewig anpassen lässt.

Natürlich haben wir wenig Erfahrung mit der Herstellung von Unsterblichen. Wer hat solche Erfahrung schon? ;-) In jedem Fall scheint mir jedoch ein schlechter Ausgangspunkt dafür, die Abbildung von objektbeladener Realität. Denn: Wenn sich diese Objekte in der Realität andauernd ändern, dann muss sich ja auch die Software andauernd ändern. Das umso häufiger und breiter, je tiefer die Objekte der Realität in der Software verankert sind.

Mir scheint, wir brauchen für evolvierbare Softwareentwicklung nicht mehr Objektorientierung, sondern das Gegenteil: Anti-Objektorientierung.

Java und C# und Ruby müssen wir deshalb nicht sofort auf den Müll werfen. Letztlich sind die Sprachen unschuldig. Auf den Müll muss das kontraproduktive platonische Weltbild. Denn das Weltbild steuert, wie wir die Sprachen einsetzen.

Mit einem neuen Weltbild können wir auch mit den überkommenen Werkzeugen durchaus neue, besser passende Strukturen schaffen. Konstruktivistische Softwareentwicklung ist mit C#, Java, Ruby, JavaScript, F# usw. möglich. Mal leichter, mal schwerer.

Hören wir also auf, das Innen der Software so zu strukturieren, wie wir meinen, dass die dingliche Realität aussieht. In Ihrem Gehirn finden Sie den Computer nicht, auf den Sie schauen. Es darin auch nicht den Stuhl, auf dem Sie sitzen. Oder den Raum, in dem Sie sich befinden. Weder die Anatomie des Gehirns noch die Signale zwischen den anatomischen Strukturen haben irgendeine Ähnlichkeit mit der Umwelt. Innen ist nicht wie außen.

Allerdings befähigt Sie der Aufbau Ihres Gehirns (plus Körper), als Ganzes in der Umwelt zu überleben. Nur das ist es, was zählt.

Genau das müssen wir für Software auch erkennen. Ein erster Schritt: EventSourcing und EventStores.

Ein EventStore löst realweltliche Strukturen auf. Sie finden sich in der Software nicht mehr so wieder, wie wir sie als Entwickler in der Domäne gezeigt bekommen. Aber das ist nur der Anfang.

Ein nächster Schritt: Bounded Contexts und Aggregate.

Bounded Contexts und Aggregate lösen die Vorstellung von dem einen Datenmodell, von der einen Datenstrukturrealität auf.

Und noch ein Schritt: Inkrementelle Architektur. Das ist für mich die grundlegende Orientierung von Softwarestruktur an Durchstichen und Nutzen statt an Technologien, Infrastruktur und überkommenen Patterns (z.B. MVC, Layers).

Inkremente lösen die Vorstellung auf, dass es um Dinge ginge bei der Softwareentwicklung. Nicht jedoch das Dokument ist das Wichtigste, sondern der Prozess. Der ändert sich zuerst, ihm folgt ein eventuelles Dokument. Aber der Prozess, die Tätigkeit, das Verhalten sind weniger greifbar, sind keine Dinge. Deshalb tut sich die platonische Softwareentwicklung mit ihnen schwer.

Weitere Schritte werden wir noch erkennen, denke ich. Wenn wir uns darauf einlassen, unser Weltbild bzw. das von Software umzubauen.

In der Psychologie hat der Konstruktivismus “gewonnen”. Ich denke, davon sollten wir lernen als Softwareentwickler.

Was außerhalb von Software existieren mag, ist eine Sache. Eine ganz andere ist es, wie Software intern organisiert ist. Ähnlichkeit muss es zum Außen nicht geben. Nur Überlebenstauglichkeit. Dafür braucht es vor allem… Wandelbarkeit.

Donnerstag, 1. August 2013

Was ist eine Entität?

Im Zuge meiner Beschäftigung mit CQRS und Event Sourcing habe ich mir wieder die Frage gestellt, was denn eigentlich eine DDD Entität sei?

Nun hab ich endlich für mich eine ganz einfache Definition gefunden: Eine Entität ist ein Dictionary<string,string> mit unwandelbarer Id [1]. Eine Entität ist ein Sammlung von Schlüssel-Wert-Paar als Zeichenfolgen mit unwandelbarer Id [1].

Jup. Das isses. Mehr nicht [2].

Den Inhalt einer Entität kann man wandeln, die Id aber nicht. Und der Inhalt einer Entität ist strukturiert. Gerade die Annahme, dass Keys Pfade sein können [2], macht mich zufrieden. Nicht nur “Vorname” oder “Mindestgebot” oder “Lagerplatz” sind also Keys, sondern ebenfalls “Wohnsitz/PLZ” oder “Telefonnummern/3” oder “Positionen/2/Menge” [3].

So ein Dictionary ist ein Dokument, d.h. es ist selbstgenügsam (self-contained). Alles, was dazugehört, steht drin. Was nicht drin steht, gehört zu einer anderen Entität. So einfach ist das.

Was Sie als Daten bei einem Key hinterlegen, ist Ihre Sache. Das kann so umfangreich und strukturiert sein, wie es will. Per definitionem ist das aber keine Entität, weil es eben ein Wert in einer Entität ist. Werte als string [4] und nicht als object zu definieren ist mir deshalb sehr wichtig. Dadurch ist ausgeschlossen, aus einer Entität Objekte zu referenzieren.

Entitäten dürfen allerdings Ids anderer enthalten. Solche Entitätsreferenzen sind ok, weil sie sich auf das wahrhaft Unwandelbare beziehen [5]. Beispiel:

var frau = new Dictionary<string,string>{{$$id, "10"}, {"Vorname", "Eva"}, ...};
var mann = new Dictionary<string,string>{{$$id, "0815"}, {"Vorname", "Adam"}, ...};

frau["partner"] = mann["$$id"];
mann["partner"] = frau["$$id"];

Mit der Definition “Eine Entität ist ein Dictionary<string,string>” ist es nun ganz einfach herauszufinden, ob ein zusammengesetzter Wert eine Entität ist oder nicht. Fragen Sie sich einfach, ob Sie den Wert einem oder mehreren Keys in einer anderen Entität zuweisen würden. Beispiel:

Ist der zusammengesetzte Wert

Person("Vorname": "Peter", "GebDat": "1967-05-12", "Geschlecht": "m")

eine Entität? In manchen Zusammenhängen mag das so sein:

var person = new Dictionary<string,string>{{"$$id", "7654}, {"Vorname", "Peter"}, ...};
var einwohner = new Dictionary<string,string>{{"$$id", "0192"}, {"Person", "7654"}};

In anderen wieder nicht. Dann ist er Teil einer umfassenden Entität, z.B.

var einwohner = new Dictionary<string,string>{...};
einwohner["Person"] = "{\"Vorname\": \"Peter\",
                        \"GebDat\": \"1967-05-12\",
                        \"Geschlecht\": \"m\"}";

oder

einwohner["Person/Vorname"] = "Peter";
einwohner["Person/GebDat"] = "1967-05-12";
einwohner["Person/Geschlecht"] = "m";

Ich denke, das macht die Entscheidung einfacher. Doch auch wenn die mal daneben geht, ist es nicht so schlimm. Entitäten in dieser Form kann man auch leicht refaktorisieren. “Inline entity” und “extract entity” scheinen mir genauso probat wie “inline method” und “extract method”. Also keine Angst vor BDUF!

Außerdem lassen sich Entitäten dieser Form sehr leicht verteilt über Ereignisse denken, z.B.

Zugezogen("$$id": "9384", "Nachname": "Müller", "Straße": "Isestr. 2", ...)
Umgezogen("$$id": "9384", "Straße": "Richterstr. 17", "PLZ": "22085", ...)
Geheiratet("$$id": "9384", "Ehepartner": "0815", "Nachname": "Schmied", ...)
Geheiratet("$$id": "0815", "Ehepartner": "9384", ...)

In jedem Ereignis wird die Entität angegeben und danach nur eine Liste von Keys mit neuen Werten. Der aktuelle Stand einer Entität ergibt sich dann aus dem Summe ihrer Ereignisse.

Endnoten

[1] Ob die Id ein Eintrag mit speziellem Key in so einem Dictionary ist oder separat gehalten wird, ist mir erstmal egal.

[2] Noch einfacher könnte man natürlich sagen, eine Entität sei ein strukturierter Text, z.B. ein Json- oder XML-Dokument. Wird letztlich bei NoSql auch so gemacht. Aber das ist mir grad zu wenig griffig, wenn ich an den Umgang damit im Code denke. Mit einem Dictionary fühle ich mich etwas wohler.

Ein geschachteltes Dokument wie

<Person id="1234">
  <Vorname>Peter</Vorname>
  <Anschrift>
    <PLZ>22085</PLZ>
  </Anschrift>
</Person>

sieht mir auch komplizierter aus als eine Liste von Key-Value-Paaren:

$$id:1234
Vorname:Peter
Anschrift/PLZ:22085

Das eine kann in das andere übersetzt werden. Das reicht mir.

[3] Wem da Metadaten fehlen, der kann sie sich gern dazudenken, z.B. “Vorname:string” oder “Positionen/2/Menge:int”. Am Ende ist jedenfalls jeder Wert auf eine Zeichenkette abbildbar.

Wie solche Pfade aussehen, ist hier ebenfalls nicht wichtig. Mit der Syntax, die ich hier benutze, sind Sie ja aber vertraut.

[4] Binärdarstellungen sind nur eine Optimierung. Mit textuellen Repräsentationen von Daten kommen wir weiter. Sie sind ausreichend, bis ein konkreter Use Case etwas anderes fordert.

[5] Allerdings hadere ich hier noch mit mir. Manchmal denke ich, dass Entitäten gar keine Referenzen enthalten sollten. Wenn sie zu anderen in Beziehung stehen, dann nur innerhalb eines Kontextes - und der wird dann von einem Aggregat aufgespannt.

Ein Aggregat ist dann eine Entität, deren Zweck es ist, andere zu verbinden. Es ist ein Beziehungsraum.

So ganz traue ich mich aber noch nicht, das konsequent zu denken. Ich habe es zwar schon einmal in der dotnetpro so beschrieben - doch im Moment bin ich wieder ein wenig im Zweifel… Hm… warum eigentlich? Dazu mach ich mir am besten ein andermal weitere Gedanken.

Mittwoch, 24. Juli 2013

Historische Interaktionen

Die erste Frage bei der Überführung von Anforderungen in Code sollte den Interaktionen gelten: Welche Interaktionen braucht ein Softwaresystem? Das bedeutet, welche technischen Ereignisse [1] sind domänenrelevant und müssen verarbeitet werden?

Im einfachsten Fall bedeutet das, hinter jedem Menüpunkt und jeder Schaltfläche steht eine Interaktion. Löst der Anwender sie aus, soll im Softwaresystem etwas passieren.

Für mich beginnt die Softwareentwicklung also mit einer Diskussion über die Schnittstellen des Softwaresystems zur Umwelt. Dort finden Interaktionen mit Benutzern statt - seien das Menschen, andere Software oder Maschinen. Von dort treibe ich den Softwareentwurf nach innen weiter: outside-in design. Denn alle Strukturen im Softwaresystem müssen einem Bedarf in der Umwelt dienen, der über eine Schnittstelle vermittelt wird.

Wie der Entwurf nach Fund einer Interaktion dann bis zum Code fortschreiten kann, will ich hier nicht thematisieren. Von mir aus kann man das streng mit Test-Driven Design versuchen.

Entscheidend ist vielmehr, dass Interaktionen einen idealen Ansatzpunkt für jegliches weiteres Vorgehen bieten. Jede Interaktion kann nämlich als Funktion codiert werden [2].

Beispiel Benutzeranmeldung: Die Benutzeranmeldung erfolgt über einen Dialog, in dem der Anwender seinen Benutzernamen und ein Passwort einträgt. Dann drückt er die Schaltfläche “Anmelden”, um die Authentifizierung und Autorisierung anzustoßen.

Die Schaltfläche steht für die Interaktion “Anmelden”. Und die Interaktion kann als Funktion bool Anmelden(string benutzername, string passwort) realisiert werden.

Alternativ könnte die Interaktion durch einen Menüpunkt oder einen Tastatur-Shortcut ausgelöst werden. Das technische Ereignis ist letztlich unwichtig. Wichtig ist, was daraufhin passiert, das Verhalten des Softwaresystems. Und das kann komplett in einer Funktion gekapselt werden. Immer.

Soweit der Blick mit unbewaffnetem Auge.

Interaktionen unterscheiden

Jetzt aber die Lupe zur Hand genommen. Da zeigen sich nämlich Unterschiede in den Interaktionen bzw. Interaktionsfunktionen.

image

Reinheitsgrad: Ich denke, es lohnt sich die Unterscheidung zwischen reinen und unreinen Interaktionen. Reine Interaktionen arbeiten nur auf Daten, die ihnen übergeben werden. Beispiel: Wenn ich in einem Dialog einen Text eingeben kann, der auf Knopfdruck umgekehrt wird, damit ich sehen kann, ob ein Palindrom vorliegt, dann steht dahinter eine reine Interaktionsfunktion. Der oben skizzierte Anmeldedialog hingegen stößt eine unreine Interaktionsfunktion an. Mit Benutzername und Passwort allein kann nicht authentifiziert werden; dazu ist mindestens noch ein Verzeichnis von Benutzern nötig.

Verhaftungsgrad: Unreine Interaktionen zerfallen für mich darüber hinaus noch in solche, die zeitlich verhaftet sind - und solche, die quasi nur im Moment leben.

Zeitlich verhaftete Interaktionen benötigen für ihre Arbeit ein Gedächtnis. Ihnen ist die Vergangenheit wichtig - zumindest in Form eines akkumulierten Zustands. Man könnte auch sagen, sie seien historisch determiniert. Denn ihr Verhalten hängt nicht nur vom aktuellen Input ab, sondern von vorherigem Verhalten des Softwaresystems.

Der Anmeldedialog ist dafür ein Beispiel. Ob er einen Benutzer erfolgreich authentifiziert oder nicht, hängt davon ab, welche Veränderungen vorher am Benutzerverzeichnis vorgenommen worden sind.

Anders ist das bei Interaktionen, die zwar auch nicht nur auf ihrem Input arbeiten, sondern weitere Ressourcen brauchen. Doch da ist deren Geschichte nicht wichtig. Sie wird nicht in die Verarbeitung mit einbezogen.

Das ist zum Beispiel der Fall, wenn eine Interaktion etwas druckt oder Input mit der aktuellen Systemzeit vergleicht.

Zugriffsart: Unreine Interaktionen, die nur im Moment leben, können auf ihre Ressourcen lesend oder schreibend zugreifen. Sie interessieren sich nicht für die Vergangenheit, also dürfen sie Zustand auch einfach verändern. An der Ressource ist anschließend nicht abzulesen, in welchem Zustand sie vorher war.

Aber wie ist das bei unreinen verhafteten Interaktionen?

Ich glaube, hier sollten wir umdenken. Bisher ist dort auch lesender und schreibender Zugriff erlaubt - ohne dass vorherige Zustände erhalten blieben.

Aus Effizienzgründen war das bisher so. Wir konnten nicht anders oder haben das zumindest geglaubt. Doch es hat immer schon der Natur dieser Interaktionen widersprochen.

Das haben wir schmerzvoll erfahren, wenn wir Mühe hatten undo/redo zu implementieren oder Daten mit Geltungsdauern zu verwalten oder historische Daten zu pflegen oder wir uns mit der Versionierung von Datenstrukturen herumschlagen mussten.

Wenn wir nun aber mal von eingefahrenen Mustern Abstand nehmen, dann können wir mit solchen Interaktionen natürlicher umgehen. Das bedeutet für mich, dass bei “historischen Interaktionen” nur lesender und anfügender Zugriff erlaubt sind. Es gibt keinen schreibenden im Sinne von verändernden Zugriff. Denn der würde ja die Geschichte umschreiben.

Die Geschichte, das ist die Liste der Veränderungen, die zum aktuellen Stand der Daten geführt hat. Ja, ich glaube, die sollte unangetastet bleiben. Wir können davon nur profitieren.

Fazit

Nicht jede Interaktion ist zeitlich verhaftet. Wir tun also gut daran, genauer hinzuschauen. Die Unterscheidung zwischen Interaktionsfunktionen mit und ohne Seiteneffekt finde ich jedoch zu einfach. Denn Seiteneffekte können sehr unterschiedlich sein.

Die Ausgabe auf einem Drucker ist etwas anderes, als die Fortschreibung von Unternehmensdaten in einer Datenbank. Event Sourcing scheint mir in dieser Hinsicht ein sehr probates Mittel, um Klarheit zu schaffen.

Interaktionen, die mit Zustand zu tun haben, der sich über die Lebenszeit eines Softwaresystems durch das Softwaresystem entwickelt, sollten sich dafür ein Gedächtnis bewahren. Das ist für mich der neue Default.

Und damit dieser Default nicht über Gebühr verstört, sollte klar sein, wo er gilt. Das sind nur die “historischen Interaktionen”. Alle anderen - auch wenn sie Seiteneffekte haben - können weiter wie bisher gehandhabt werden.

 

PS: Natürlich können unreine Interaktionen auch gemischter Art sein: Sie können aus Wiedergabe und Seiteneffekt oder aus Lookup und Aufzeichnung bestehen usw.

In jedem Fall sollten diese Aspekte aber in eigenen Funktionen gekapselt sein. Das Integration Operation Segregation Principle (IOSP) gilt auch hier.

Endnoten

[1] Technische Ereignisse sind Tastendrücke, Mausklicks/-bewegungen oder Notifikationen von Timern oder Geräten.

[2] Die Schnittstelle zwischen Umwelt und Domäne nehme ich von dieser Funktion aus. Die Interaktionsfunktion kümmert sich nicht darum, woher ihre Input-Daten kommen und was mit ihren Output-Daten gemacht wird. Sie kennt die Schnittstelle nicht, über die sie aufgerufen wird. Das kann ein WPF-Dialog oder ein Webservice sein.

Es ist also nicht Sache einer Interaktionsfunktion, sich Daten aus Eingabefeldern des UI zu beschaffen oder Ergebisse im UI, über das sie aufgerufen wurde, anzuzeigen.

Montag, 22. Juli 2013

Zustand ist eine armselige Optimierung

Das Denken des Softwareentwicklers kreist ständig um Daten. Wer auf Anforderungen schaut, sucht zuerst nach Daten. Wer an Infrastruktur denkt, denkt zuerst an Persistenztechnologien. Und wenn nicht immer, dann zumindest oft.

Nicht umsonst gibt es die Mittel der Objektorientierung als Weiterentwicklung von Sprachen wie C und Pascal: um Daten nicht nur zusammenzufassen (class), sondern auch noch funktional zu kapseln (interface).

Was dann in Objekten steckt, ist Zustand. Das sind nicht nur einfach Daten, sondern Daten in einer aus vielen Veränderungen hervorgegangenen Konstellation.

Hier ein simples Beispiel:

image

Mit jedem Aufruf von Sum() berechnet ein Aggregator die Summe einer Anzahl von Werten. Zu diesem Zweck führt das Objekt einen Zustand. Der ist das Ergebnis einer Reihe von Veränderungen:

image

Er stellt also einen Schnappschuss dar. Wie es zu ihm gekommen ist, liegt im Dunkeln.

Das macht für diesen Zweck auch nichts. Ich meine nicht, dass man eine solche Aggregationsfunktionalität dringend anders realisieren müsste. Dennoch kann sie helfen, deutlich zu machen, dass solcher Umgang mit Daten nur eine Option von mehreren ist. Und das deshalb ihr Entscheidungen zugrundeliegen, die ungünstig sein können, wenn wir sie nicht kennen oder leichtfertig treffen.

Wie könnte es anders gehen? Die Funktionale Programmierung (FP) würde vielleicht sagen, der Zustand in der Klasse sei unschön. Sum() ist keine reine Funktion, eben weil sie von unsichtbarem Zustand abhängt. Für einen gegebenen Input - z.B. (2) - lässt sich nicht vorhersagen, welchen Output sie erzeugt.

Als Antwort könnte die Summierung so umgebaut werden:

image

Es gibt immer noch Zustand im obigen Sinne, doch der wird irgendwo gehalten. Er ist nicht mehr Sache der nun reinen Funktion Sum(). Für einen gegebenen Input - z.B. (2, 1) - ist deren Output immer gleich - z.B. (3).

Ist dadurch etwas gewonnen? FP sagt, ja. Über reine Funktionen lässt sich besser nachdenken, die composability ist höher, die Testbarkeit besser. Und dann die Daten auch noch immutable sind… Besser geht´s nimmer ;-)

Ich möchte jedoch noch ein Stückchen weitergehen. Selbst wenn der reingereichte Zustand immutable ist, juckt mich etwas.

Warum gibt es in diesem Beispiel die Summe als Zustand? Ich glaube, der Grund ist ganz schlicht und einfach, aus Platz- und Performancegründen.

Denn es ginge ja auch anders. Der Zustand als punktuelle Konstellation hervorgegangen aus einer Linie von Veränderungen, kann immer durch diese Linie plus eine Reihe von Transformationen ersetzt werden:

image

Sum() hat jetzt überhaupt keinen Zustand mehr, weder internen noch externen. Stattdessen sieht die Funktion immer nur einen Liste von Veränderungen bestehend aus vielen historischen und einer aktuellen. Daraus berechnet sie das aktuelle Resultat - und schreibt die Liste der historischen Veränderungen fort.

Das Ergebnis ist dasselbe wie in den Varianten vorher. Nur wird jetzt mehr Platz und Zeit gebraucht.

Hat das einen Vorteil? Ja, ich denke, schon. Vorteil #1: Die Entwicklung des Ergebnisses ist immer komplett nachvollziehbar. Das hilft bei der Fehlersuche. Vorteil #2: Wenn eine Korrektur der Funktionalität nötig ist, können Ergebnisse nachträglich angepasst werden, falls das sinnvoll ist.

Diese Vorteile stechen in diesem Kleinstbeispiel nicht so ins Auge. Aber wenn Sie das Aggregat mal als persistent und etwas umfangreicher ansehen, dann werden sie relevant. Dann ist es interessant, diese Option denken zu können.

Ob die Liste der Veränderungen in die Funktion reingereicht wird oder aus einer globalen Quelle beschafft werden kann, finde ich gerade nicht so wichtig.

image

Hier geht es mir um den Wechsel der Sichtweise: vom Zustand zum Veränderungsstrom. Dass es schlicht möglich ist, auf Zustand zu verzichten. Dass seine Herstellung eine selbstverständliche und intuitive Optimierung ist - die wir als solche erkennen sollten.

Über die Optimierung in Hinsicht auf Platz und Zeit steckt darin sogar noch eine: eine Optimierung im Hinblick auf Form.

Wenn Sie Zustand denken - sei das eine Summe wie oben oder eine Datenklasse wie Person, Auktion oder Produkt -, dann denken Sie auch immer sofort an eine konkrete Struktur. Sie überlegen sehr sorgfältig, wie die aussehen soll. Davon hängen Klassen-, Datei- und Datenbankstrukturen ab. Bei solchen Strukturen darf Ihnen kein Fehler unterlaufen. Davon hängt ja viel ab. Sie zu ändern, allemal, wenn sie persistent sind, macht viel Aufwand.

Was aber, wenn es nicht mehr um Zustand geht? Dann müssen Sie nicht mehr über die eine beste Struktur für Daten nachdenken. Bei der obigen Summe ist diese Struktur trivial. Aber nehmen wir eine Software für ein Ortsamt: Wie sollte darin am besten die Klasse oder Tabelle oder das Dokument für einen Einwohner strukturiert sei?

Wie lange sollte man darüber nachdenken? Wieviele Anforderungen sollte man dafür analysieren? Nach wievielen Jahren Entwicklung und Änderungen an der Software kann man sich sicher sein?

Ich weiß es nicht. Und deshalb ist für mich jede Festlegung ohne Erfahrung und Mustererkennung eine vorzeitige Optimierung.

Die Lösung besteht für mich darin, das harte Nachdenken aufzugeben. Schluss mit Zustand! Stattdessen einfach nur Veränderungen speichern. So, wie sie in einem Inkrement gerade anfallen.

Aus diesen Veränderungen kann dann jederzeit eine Datenstruktur nach aktuellem Bedarf gegossen werden. Oben ist das zunächst nur eine Summe. Doch jederzeit könnte der auch noch ein Mittelwert zugesellt werden:

image

Hinter dem steckt nicht nur eine andere Logik, sondern auch eine andere Datenstruktur (double).

Zustand statt Veränderungen (events) zu speichern, ist mithin eine Optimierung, die unsere Optionen reduziert. Sie lässt uns langfristig verarmen - für kurzfristige Gewinne.

Wir glauben, wir hätten weder Platz noch Zeit, um Zustände bei Bedarf aus den ursprünglichen Veränderungen herzustellen. Deshalb treiben wir viel Aufwand in Bezug auf universelle in-memory wie persistente Datenstrukturen. Doch letztlich wissen wir nicht, ob wir mit dieser Befürchtung recht haben. Wir probieren ja nie die Alternative aus.

Dabei gäbe es viel zu gewinnen an Flexibilität. Und die brauchen wir dringend, wenn Software lange leben soll.

Montag, 8. Juli 2013

Freiheitsgrade nutzen

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:

image

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:

image

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.

image

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.

image

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:

image

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.

Mittwoch, 3. Juli 2013

Event Sourcing: Vorzeitige Datenstrukturoptimierung vermeiden

Datenstrukturen wohin Sie schauen. Wenn Sie Anforderungen studieren, sucht Ihr Gehirn sofort nach Zusammenhängen und Mustern für Daten. So sind Sie einfach trainiert. Die Objektorientierung scheint das nahezulegen. Der Umgang mit Datenbanken erfordert das allemal.

Ich kenne diesen Reflex jedenfalls genau. Wenn ich vor der Aufgabe stehe, einen Stack zu implementieren, frage ich mich, wie dafür die Daten strukturiert sein sollen. Speichere ich die Einträge in einem Array? Oder benutze ich besser eine Liste? Sollte die Liste einfach oder doppelt verkettet sein?

Oder wenn ich vor der Aufgabe stehe, ein Tic Tac Toe Spiel zu implementieren, dann frage ich mich, wie das Spielbrett intern abgebildet werden sollte. Ist es besser, alle Spielfelder in einem eindimensionalen Array zu halten? Oder sollte ich ein zweidimensionales Array benutzen?

Und falls dazu noch die Anforderung kommt, die Daten zu persistieren, dann mache ich mir Gedanken über das Persistenzparadigma – relational oder dokumentenorientiert oder key-value store usw. – und ein dazugehöriges Schema.

Puh… ganz schön viele Gedanken, die sich um Datenstrukturen drehen. Aber das ist ja auch verständlich, weil es um die eine Datenstruktur geht, quasi das Herzstück der Codes. Denn alle Funktionalität muss damit leben. Da greift man besser nicht daneben, sonst knirscht das Software-Getriebe später.

Dieses Vorgehen scheint alternativlos. So haben wir es schon immer gemacht. So muss man Softwareentwicklung planen.

Oder?

Nein! Ich glaube daran nicht mehr. Historisch gesehen finde ich diese Herangehensweise zwar verständlich – nur müssen wir deshalb ja nicht so weitermachen.

Für mich scheint der “data structure first” Ansatz zunehmend kontraproduktiv. Er versucht gleich zu Beginn der Entwicklung etwas zu optimieren, das sich eigentlich erst über die Zeit ergeben muss. Beim Stack und für Tic Tac Toe mag das noch nicht so offensichtlich sein. Liegen da die Datenstrukturen nicht sehr klar auf der Hand? Aber bei Ihren Anwendungen ist das sicherlich anders. Beweis dafür ist die Bewegung, die über Jahre in Ihren Schemata stattgefunden hat. Die sehen sicherlich nicht mehr so aus wie am Anfang.

Datenstrukturen sind kein Selbstzweck. Wenn Sie eine Datenstruktur planen, müssen Sie vielmehr immer genau im Blick haben, wer deren Konsument ist. Was sind dessen Bedürfnisse? Wissen Sie das aber genau? Können Sie das wissen? Ist das nicht auch eine Form von Big Design Up-Front (BDUF) und ein Fall von Premature Optimization?

Klar, bei Tic Tac Toe ist da natürlich zunächst einmal der Anwender. Der will auf seinem Bildschirm ein zweidimensionales Spielbrett sehen.

Und dann ist da die Domänenlogik. Die muss auch mit dem Spielbrett umgehen. Aber ist für sie ebenfalls eine zweidimensionale Struktur die beste? Vielleicht. Vielleicht aber auch nicht. Herausfinden werden Sie das erst, wenn Sie die Domänenlogik implementieren.

Vielleicht ist es sogar so, dass verschiedene Aspekte der Domänenlogik unterschiedliche Datenstrukturen bevorzugen würden. Braucht die Bestimmung des nächsten Spielers eine zweidimensionale oder eindimensionale Datenstruktur oder gar etwas anderes? Wie steht es mit der Prüfung, ob ein Spieler gewonnen hat? Wie wird am leichtesten festgestellt, ob ein Zug überhaupt gültig ist?

Wenn Sie nach der einen Datenstruktur zur Erfüllung aller Anforderungen an den Entwurf herangehen, befinden Sie sich schnell im Kreuzfeuer vieler “Stakeholder”, d.h. Aspekte. Die Suche nach einem Optimum ist da vergeblich, würde ich sagen. Es kann immer nur ein Kompromiss herauskommen. Die Frage ist nur, wie sehr Sie sich dabei aufreiben.

Warum also nicht Zeit sparen? Schluss mit BDSDUF = Big Data Structure Design Up-Front. Seien Sie schnell statt gründlich – insbesondere wenn die Gründlichkeit ja ohnehin kein stabiles Ergebnis liefern kann. Statt in die Glaskugel schauen Sie auf die ohnehin stattfindende Entwicklung beim Umgang mit Daten und versuchen, Muster zu entdecken. Dann kommen Sie früher oder später schon zu stabile(re)n Datenstrukturen.

Den Plural meine ich hier ernst. Statt sich auf die eine Datenstruktur zu kaprizieren, versuchen Sie mal, mehrere zuzulassen.

Events als Datengranulat

Ich will versuchen, Ihnen konkreter zu beschreiben, was ich damit meine. Als Beispiel soll Tic Tac Toe dienen. Die Verarbeitung eines Zuges könnte so aussehen:

image

Klar ist dabei, womit die Verarbeitung angestoßen wird – durch Meldung der Koordinate des Spielfeldes, auf das ein Spieler einen Stein setzen will – und was die Verarbeitung am Ende als Ergebnis liefern soll: die aktuelle Konfiguration des Spielfeldes sowie den Spielstand (ob es weitergeht oder das Spiel zuende ist durch Gewinn oder Unentschieden).

Alle anderen Daten liegen im Dunkeln. Also könnte es losgehen mit der Spekulation, wie denn “das Spiel” über diese Kette von Verarbeitungsschritten am besten repräsentiert werden sollte, was die eine beste Repräsentation sein sollte.

Was immer Sie nun dazu denken… Ich möchte Ihnen etwas anderes vorschlagen. Wie wäre es, wenn es keine spielspezifische gemeinsame Datenstruktur gäbe? Wie wäre es, wenn es keine Datenstruktur gäbe, die den aktuellen Zustand darstellte?

Stattdessen schlage ich vor, eine Liste von Zustandsänderungen zu führen. Im Falle von Tic Tac Toe ist das ganz, ganz einfach. Diese Zustandsänderungen sind die Züge. Jeder Zug verändert die Konfiguration des Spielbretts und den Spielstand. Das kann als Ereignis (Event) angesehen werden. Und diese Events schlage ich vor zu speichern.

Statt Spielbrett mit Daten für Spielfeldbelegungen…

image

…gibt es eben nur eine Liste von Events:

image

Diese Events beziehen sich zwar auf die Vorstellung eines zweidimensionalen Spielbretts, doch das existiert eben nicht statisch.

Und wie sollen die Verarbeitungsschritte nun vorgehen?

Fangen wir mal einfach an: Für den Spielerwechsel muss nichts getan werden. Es ist kein spezieller Zustand zu führen. Welcher Spieler dran ist, ergibt sich aus der Anzahl der Züge:

image

Das ist auch nur relevant für das Rendering des Spielbretts.

Wie ist es mit der Zugausführung – vorausgesetzt, der Zug ist valide? Das ist ein Einzeiler:

image

Es muss ja nur memoriert werden, welcher Zug gemacht wurde. Kein Spielstein wird auf einem Spielbrett platziert.

Und wie sieht die Validation aus? Die beschränkt sich auf die Prüfung, ob die Koordinate des Zuges schon einmal vorgekommen ist:

image

Falls ja, liegt ein Fehler vor. Nur der Einfachheit halber bricht das Spiel dann mit einer Exception ab.

Jetzt die Spielstandprüfung. Die ist aufwändiger. Aber das wäre sie auch, wenn der Spielbrettzustand vorgehalten würde:

image

Ob ein Unentschieden vorliegt oder nicht, ist schnell entschieden. Wenn alle Felder belegt sind, also 9 Züge gemacht wurden, geht nichts mehr.
Die möglichen Gewinnpositionen werden einzeln geprüft. Züge auf horizontale, vertikale und diagonale Feldreihen werden selektiert. Falls in einer Reihe nur Züge desselben Spielers gemacht wurden, liegt ein Gewinn vor.
Züge von Spieler X werden mit 1 bewertet, die von O mit –1. Eine Gewinnreihe hat dann den Wert 3 bzw. –3.
Zum Schluss ist natürlich doch ein Spielbrett der herkömmlichen Art nötig. Das zeigt das obige Flow-Diagramm ja schon. Allerdings überlasse ich das nicht dem Spielerwechsel – das wäre ein Widerspruch zum SRP -, sondern verpacke es in eine eigene Routine:
image
Bei CQRS spricht man von ReadModels auf der Query-Seite. Und genau darum geht es ja auch hier: eine Datenstruktur, die nur für lesenden Gebrauch bestimmt ist. Dass die jedes Mal neu generiert wird, macht nichts. Auch ein Cache ist eine Optimierung, die hier vorzeitig wäre.
Zur Abrundung noch die Integration dieser Operationen in einer eigenen Methode:
image

Fazit

Ich habe mir die Entscheidung für die eine alleinseligmachende Datenstruktur gespart. Alle Aspekte des Tic Tac Toe Spiels haben für sich entscheiden können, was ihnen am besten taugt. Und wie sich herausstellt, konnten alle mit einer Event Source sehr gut leben.
Das ist insofern bemerkenswert, als dass eine Event Source eine generische Datenstruktur ist. Die sieht in allen Anwendungen gleich aus. Es ist nicht mehr als eine Liste von Events. Zugegeben, die sind bei TTT sehr, sehr simpel. Aber auch wenn Events eigene Klassen sind, ändert sich das Prinzip nicht. Eine Event Source ist und bleibt eine schlichte Liste. Und aus der kann bei Bedarf jede konkretere Datenstruktur generiert werden.
Deshalb bezeichne ich die Events auch als Datengranulat. Sie sind wie kleine Kunststoffkügelchen, aus denen man bei Bedarf allerlei Nützliches formen kann.
Mit einer Event Source bin ich also schneller am Start. Und wenn ich feststelle, dass ich aus den Events die eine oder andere Datenstruktur öfter generiere, also sich ein Muster herausschält, dann kann ich mir Gedanken darüber machen, ob ich dafür einen Cache einrichte. Denn nichts anderes sind die üblichen fein ziselierten Datenstrukturen in unseren Anwendungen.
Ihre Bevorratung dient der Effizienz – und kostet uns Zeit in der Entwicklung und Flexibilität während der Evolution der Software.
Deshalb: Versuchen Sie doch einmal, solche vorzeitige Optimierung zu vermeiden. Geben Sie solchem Event Sourcing eine Chance.