Follow my new blog

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.

Kommentare:

Anonym hat gesagt…

Interessante Überlegung. Klingt ja schon fast philosophisch. Nur der Himmel könnte hierbei das Limit sein. Ich verstehe Zustand hier als Aggregat und benutzerdefinierte Datentypen. Ich werde das mal ausprobieren.

ThomasZeman hat gesagt…

In der Tat interessant. Wie lange zurück allerdings speichere ich Veränderungen bzw. Ereignisse? Angenommen unendlich, dann wird es auch unendlich lange dauern den aktuellen Zustand (hier Summe) zu bestimmen.
Die Sache erinnert mich btw. auch ein bisschen an "full backup" vs. "inkrementelle backups".

Ralf Westphal - One Man Think Tank hat gesagt…

@Thomas: Wenn das Abspielen vergangener Ereignisse zu lange dauert, dann und erst dann solltest du optimieren. Dann nämlich weißt du, dass sich der Aufwand dafür lohnt. Tust du es vorher, schaffst du Verhältnisse/Strukturen, von denen du nicht weißt, ob sie wirklich nötig sind.

Inkrementelles Backup oder eine Versionsverwaltung à la git sind natürlich sinnige Analogien. Beide spiegeln nicht die Dateisystemstruktur in ihren Repositories, sondern legen die Differenzen "irgendwie" ab. Wenn wir durch einen Repo-Browser darauf schauen, dann bekommen wir nur einen View zu sehen.

Ich denke, genauso sollten wir (immer öfter) mit unseren Anwendungsdaten umgehen. Wir sparen uns einfach sehr viel gedanklichen Aufwand, ideale Strukturen zu suchen. Besser ist es, die nach Bedarf in vielfältiger Weise aus dem "Datengranulat" der Veränderungen/Ereignisse zu generieren.

Wenn das dann mal zu langsam ist... dann können wir sie cachen. Da kommt dann z.B. das Read Model von CQRS ins Spiel. CQRS ist insofern also auch schon eine Optimierung.