Seiteneffekte und Reihenfolge

In diesem Post will ich den Zusammenhang herausarbeiten zwischen der Verwendung von Seiteneffekten und dem Einfluss auf die Einfachheit von APIs. Am Ende werde ich noch ein paar Tipps geben, die mir helfen das Problem handhabbarer zu machen.

Die korrekte Reihenfolge einzuhalten ist schwierig

Die Reihenfolge von was? Und wie kann sie korrekt sein? Und warum sollte ich sie einhalten wollen? Und selbst wenn das alles klar ist, warum sollte es schwierig sein?

Mir geht es um die Reihenfolgen von API Aufrufen. Mit API meine ich aber nicht nur die, die wir als 3rd Party Komponenten in ein Projekt einbinden, sondern auch die Klassen, die man selber schreibt und man selber nutzt oder den Kollegen zur Verfügung stellt.

Eine API besteht in C# aus mindestens einer Klasse oder einem Interface und den Methoden, die in diesem Typen definiert sind. Wenn die Methoden voneinander unabhängig sind, kann ich diese in einer beliebigen Reihenfolge aufrufen und alles ist gut. Auch wenn es solche APIs gibt, gibt es ebenso diese, wo manche Methoden erst dann aufgerufen werden dürfen, wenn vorher eine andere aufgerufen wurde. Auf diese Zusammenhänge wird häufig in der Dokumentation hingewiesen, wie z.B. man muss erst Open() ausführen, bevor man Execute(string command) aufrufen darf. Wenn man sich nicht daran hält, wird im besten Fall sofort eine Exception geworfen, im schlimmsten Fall läuft es weiter und man wundert sich später, wie man in diesen Zustand gelangt ist.

Bevor ich weitermache, erst mal ein kleines Beispiel, dass das Problem zeigt.

Beispiel1 - Seiteneffekte und Reihenfolge, generic.de AG

Beispiel 1 – Oberfläche

 

Im Beispiel wird ein Chart gezeichnet über die Funktion y = x. Beide Knöpfe fügen einen Datenpunkt hinzu. Der Unterschied liegt in der Reihenfolge der Aufrufe der zugehörigen Methoden. Eine der beiden Reihenfolgen ist falsch. In diesem Minibeispiel lässt sich das womöglich noch auf einen Blick erkennen, aber das ist nicht immer so.

Programmiersprachen bieten keine Unterstützung

Das Problem ist, dass man diese ungültige Reihenfolge nicht unbedingt sofort sieht, weil der Code nicht zusammensteht, sondern möglicherweise verteilt ist. Und man den Fehler erst erkennt, wenn er zur Ausführungszeit auftritt. Kann man das nicht schon früher erkennen?

Leider nicht wirklich. Der Grund ist, dass die Programmiersprache keine Möglichkeiten bietet so einen Zusammenhang einfach formal auszudrücken. Daher bleibt nur der informelle Weg in Form von Dokumentation.

Am nächstbesten kann man eine Reihenfolge erzwingen, indem man die Reihenfolge in Rückgabetypen und Parametertypen kodiert. Code sagt mehr, als tausend Worte, daher gleich das angepasste Beispiel.

Diesmal musste ich die Signaturen der Methoden mit einfügen, da es ja diesmal nicht nur void und empty-args Methoden sind. Zudem sind die Typen ja der Knackpunkt hier.

Nach diesen Änderungen kann ich Add gar nicht mehr aufrufen, ohne vorher Inc aufzurufen. Da ich ansonsten gar keinen passenden Typ habe.

Ich will nun aber darauf zu sprechen kommen, was die Ursache ist, warum die Reihenfolge überhaupt relevant geworden ist. Denn dann können wir statt das Symptom zu kurieren, die Wurzel anpacken.

Warum ist die Reihenfolge überhaupt relevant?

Die Reihenfolge wurde relevant, weil wir einen Zustand verändern. Und nicht alle möglichen Zustandsübergänge sind immer auch gültig zu jedem Zeitpunkt. Das Problem ist alt und die Theorie dazu ebenfalls. Ich meine „Endliche Zustandsautomaten“ oder auf Englisch: „Finite State Machines“.

Zustandsautomaten von Hand korrekt zu programmieren, wird schon schwierig, wenn es mehr als zwei Zustände sind. Eine API, um diesen Automaten zu benutzen, muss diesen Umstand widerspiegeln, da die Komplexität nicht verschwindet.

Aber wir waren ja auf der Suche nach der Ursache. Und Zustandsveränderungen sind eine Art von Seiteneffekt, da sie in der Signatur nicht auftauchen, da sie versteckt sind.

Ist ein Leben ohne Seiteneffekte möglich?

Seiteneffekte sind also die Wurzel des Übels. Also verzichten wir einfach auf alle Seiteneffekte und müssen uns nie wieder Gedanken, um die Reihenfolge machen. Prinzipiell ja, aber leider ist Software ohne Seiteneffekte auch unbrauchbar, da sie nichts macht. Wir werden also nicht ohne Seiteneffekte leben können. Aber wir können ihren Gebrauch einschränken und uns damit das Leben (beim Entwickeln von Software) erleichtern.

Damit meine ich, dass wir darauf achten sollten, dass der größte Teil unserer APIs keine Seiteneffekte hat. Das macht sie zum einen für uns einfacher zu entwickeln, da wir die Korrektheit einfacher sicherstellen können, weil Unit Tests für reine Methoden – also ohne Seiteneffekte – wesentlich kürzer ausfallen. Und zum anderen auch für die Kollegen einfacher zu benutzen, da keine impliziten Zusammenhänge im Kopf zu behalten sind, wann welche Methode aufgerufen werden darf.

Schlussworte und Tipps

In diesem Artikel habe ich versucht herauszuarbeiten, warum Seiteneffekte die Ursache sind, dass wir APIs haben, wo die Reihenfolge der Methodenaufrufe relevant ist. Da wir nicht ohne sie auskommen, sind hier nun die angesprochenen Tipps, um den Umgang mit Seiteneffekten besser in den Griff zu kriegen:

  • Vermeide void als Rückgabetyp, solange es geht
  • Sei misstrauisch bei Methoden ohne Argumente (empty-args)
  • Das static Schlüsselwort an Methoden ist ein Indikator, dass die Methode (vermutlich) keine Seiteneffekte hat

Wie immer gilt bei solch kurzen und prägnanten Aussagen, dass man immer Kopf behalten muss, aus welchem Kontext sie stammen, denn nur wenn der Kontext stimmt haben sie ihre Gültigkeit.

Konsequent angewendet, erhält man eine Software, wo nur die letzte Schicht, die mit der Umgebung interagiert, Methoden mit Seiteneffekte hat. Der Rest sind reine Funktionen, wo die Reihenfolge keine Rolle spielt.

Wer noch mehr dazu lesen möchte, den möchte an der Stelle auf den Artikel von Mark Seemann verweisen. Er kommt dort über einen anderen Einstieg zum gleichen Ergebnis.

Den gesamten Quellcode zu den Beispielen findet ihr in Github.

Danke für’s Lesen.