Follow my new blog

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.

3 Kommentare:

Mike Bild hat gesagt…

Rein/Unrein, Resource/Historie, Reinheitsgrad, Verhaftungsgrad, Zugriffsart... mal machen - mal nicht. Warum so kompliziert? Warum sofort Kompromisse?

Gerade Early-Stage ist es IMHO doch schwer zu entscheiden. Heute ist es nur ein Klick (System-Interaction) morgen ist es ein Business-Value wie "most user interested product item -> recommendation to other users"

Wer wieder einmal die "Performanz-Angst-Mütze" trägt... Einfach mehrere "kleine" Event-Sources / Stores anlegen. Partitioning / Sharding und Joining sind auch bei Event-Streams möglich und sinnvoll ;).

Und hier auch mal die Frage - Wie viele Events werden den in einer Source erzeugt? 100 - 1000 - 10000? Wie lange dauert eine In-Memory Iteration durch 10.000 Events? Wie schnell ist der Projektion- / MapReduce- Algorithmus? Macht eine "Vorberechnung" mit Ergebnis in einen anderen Event-Stream Sinn?

Ja Event-Streams / Sources / Stores sind der "default" in Early Stage! Nach besserem Verständnis der Domäne machen Verbesserungen und Optimierung hin zu "Struktur" - in ein relationales oder auch OO Modell - absolut Sinn.

Ralf Westphal - One Man Think Tank hat gesagt…

Wenn ein Klick keinen Business Value hat, dann sollte er nicht behandelt werden.

Aber nicht hinter jedem Klick hängt gleich alles. Darum geht es mir.

Implementierung ist möglich ohne Klassen.
Implementierung ist auch möglich ohne universelles Datenschema.

Aber Implementierung ist nicht möglich, wenn ich nicht weiß, was getan werden soll.

Was aber getan werden soll, das ist durchaus unterschiedlich. Darum geht es mir. Ein bisschen Blick dafür entwickeln, dass eben nicht "alles" gleich mit Event Sourcing zu tun hat bzw. haben muss.

Und mir geht es auch darum, den mystischen "Seiteneffekt" zu entzaubern. Da ist eben nicht alles gleich. Auf einen Drucker ausgeben - ja, das ist ein Seiteneffekt. Aber aus einer Lookup-Tabelle einlesen, das ist keiner. Nur weil also Daten über Input hinaus im Spiel sind, gibt es nicht unbedingt Seiteneffekte.

Und dann ist Zustand eben auch nochmal etwas anderes. Also die Daten, die sich über die Zeit wandeln.

Ein differenzierter Blick ist von Vorteil. Was nix zu tun hat mit BDUF. Auch Event Sourcing ist kein Ersatz für Nachdenken.

Und warum du aus meinem Artikel Performance-Angst herausliest, verstehe ich nicht. Ich habe die bestimmt nicht.

Wenn ich mir nun noch ein bisschen Mühe gebe, dann höre ich bei dir aber ein ganz anderes Argument heraus. Das lautet: "Man muss nicht zwischen Inputs/Interaktionen unterscheiden. Events gibt es immer."

Da wäre ich dann auch dabei. Nur darum ging es mir hier nicht. Eins nach dem anderen. Erstmal den Zustand, der heute in Datenbanken verklumpt liegt, in ein Datengranulat verwandeln.

Und im zweiten Schritt, orthogonal dazu, dann überlegen, ob nicht alles, was überhaupt "auf ein Softwaresystem einprasselt" gleich in einen Event Store kann.

Dann wären Schnittstellen von der Logik durch einen Event Store generell entkoppelt. Dann gäbe es nicht nur eine Liste interner Events/Zustandsänderungen/Differenzen, sondern auch eine Liste externer.

Letztlich bräuchte es sogar keine andere, da alle internen Events aus externen hervorgehen. Insofern ist der übliche Event Store sogar schon eine Optimierung :-), wenn man aus ihm Aggregate "aufbläst".

But I´m getting carried away... ;-)

Mike Bild hat gesagt…

Klaro - differenziert betrachtet ist nicht jede System-Umwelt-Interaktion "würdig" für die Aufzeichnung in einen Event-Store. Sicher sollte hier und da unterschieden werden. Einen Event in einen "internen" Event-Aggregator zu "schicken" macht Sinn. Hier entscheide ich Event-Store oder nicht. Note: In Node.JS gibt es einen schönen Ansatz des Event-Emitter für "externe" also I/O Events.

"Performance-Angst" hatte ich frei als häufige Entscheidung "... nicht alles gleich mit..." Event-Sourcing interpretiert. Die Performanz-Keule ist halt ein gern verwendetes Argument. :-/ Richtig - Du hast in deinem Artikel nichts davon erwähnt. Gut so - Danke!

Wie auch immer ...

"Man muss nicht zwischen Inputs/Interaktionen unterscheiden. Events gibt es immer."

Ja genau! Vor allem dort wo wir "externe" Kommunikation (I/O), heißt UI, DB, Files, ... finden. Das ist IMHO für "don't block - be ever responsive" besonders wichtig. "Blocking & I/O" das ist der Engpass! Dagegen ist 10.000 In-Memory Events zu verarbeiten meist weniger ein Problem. Klar - auch das ist immer abhängig vom Use-Case. Häufig - so objektiv wie möglich betrachtet - jedoch auch nicht ;).

"... internen Events aus externen hervorgehen ... " & "... Event Store sogar schon eine Optimierung :-), wenn man aus ihm Aggregate "aufbläst"..."

Ja - der Event-Store ist eine Optimierung. Mit dem Zweck, möglichen Zustand nach bestimmten Regeln zu projizieren. Da möchte jemand "getBLABLA" machen ;). Event-Streams per se kennen das "Damals" / die Historie nicht. Ja das machen erst Event-Stores möglich. Und wenn ich mehrere Event-Stores für interne, externe, Teufel noch was wichtige Events benötige gibt es auch dafür die Option. Ich arbeite z.B. prinzipiell mit mehreren Event-Stores. Und häufig erzeugen meine Projektionen wieder neue Events für andere Event-Stores. So ist ein inkrementelles "aufbauen" (SEDA) von Zustand mit wenig Latenz möglich. Okay - vielleicht auch zu viel an dieser Stelle ;).

Events Schritt für Schritt erläutert - okay da komm ich mit ;).