Optionals erklärt

Große Teile der Sponge API machen Gebrauch von Javas Optional-System für den Zugriff auf Objekte. Wenn du zuvor das Optional-System noch nie benutzt hast, könnte dir selbiges vielleicht ein wenig seltsam vorkommen. Vielleicht fragst du dich: „Inwiefern ist es sinnvoll, einen zusätzlichen Schritt machen zu müssen, um Daten aus einer API abzufragen?“

Hier wird ein kurzer Überblick darüber gegeben, wie Optional in der Sponge API benutzt wird - und natürlich auch warum überhaupt, was vermutlich noch wichtiger ist.

Wir beginnen mit einer kleinen Geschichte und schauen, wie Zugriffsmethoden - besonders Getter-Methoden - in der Regel arbeiten, wenn sie nicht Optional benutzen.

1. Werte, welche NULL enthalten könnten - und warum sie nerven

Gehen wir einmal davon aus, wir hätten ein einfaches API-Objekt mit dem Namen Entity, welches eine Methode namens getFoo() hat. Diese Methode übergibt eine Foo-Instanz.

../../_images/optionals1.png

Früher hätte man vielleicht solch eine einfache Methode benutzt, um an sein Sandwich zu kommen:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

Das Problem entsteht dann, wenn es sich um Methoden handelt, welche nicht zwingend eine Foo-Instanz übergeben, sondern manchmal auch einfach nur null. Dass die entsprechende Methode eventuell null übergeben könnte, kann man auf verschiedene Weisen deklarieren:

  • In den Javadoc - Das ist deshalb eine schlechte Idee, weil es dann notwendig ist, dass der Plugin-Entwickler die Javadoc ließt. Dies ist aber nicht immer der Fall.

  • Die Nutzung von Nullable-Annotiations - Das ist nicht ideal, da Annotations in der Regel ein Programm benötigen, von dem sie verarbeitet werden, beispielsweise eine IDE oder ein Compiler.

../../_images/optionals2.png

Nun nehmen wir an, unsere getFoo()-Methode wäre eine dieser Methoden, welche null übergeben könnten. Das würde bedeuten, dass unser Code von oben unsicher ist und eine NullPointerException entstehen könnte, sollte entityFoo null sein.

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

Angenommen, unser Plugin-Entwickler weiß, dass die Methode null übergeben könnte. Er entscheidet sich dafür, das Problem zu umgehen, indem er nach der API-Abfrage überprüft, ob entityFoo eventuell null ist. Ist dies der Fall, dann ersetzt er den null-Wert durch eine Foo-Instanz, welche er in einer Konstanten speichert. Das Resultat würde so aussehen:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

Natürlich ist es ganz einfach, auf diese Weise zu überprüfen, ob es sich um null handelt. Was aber klar werden sollte ist, dass die richtige Behandlung von null im Code selbst bei simpelsten Anwendungen schnell zu sehr viel komplizierterem Code führen kann. Außerdem muss man sich dabei darauf verlassen, dass der Plugin-Entwickler die Dokumentation der Methode durchschaut, um zu prüfen, ob eine null-Überprüfung notwendig ist.

Allerdings ist dies nicht der einzige Nachteil. Betrachten wir die API mal über einen längeren Zeitraum. Lass uns annehmen, dass der Autor des Plugins sich die Javadoc der Methode anschaut und sieht, dass diese garantiert unter keinen Umständen null zurückliefert ( da jede Entity immer auch ein Foo hat). Spitze! Keine aufgeblasenen null-Überprüfungen notwendig!

Aber nun lass uns annehmen, dass in einer späteren Version des Spieles die Entwickler des Spieles das Konzept von Foo entfernen oder überholen. Die Ersteller der API passen ihre API entsprechend an und sagen, dass von nun an die Methode getFoo() null zurückgeben kann und schreiben das auch so ins Javadoc. Nun darin liegt das Problem: Selbst wenn gewissenhafte Plugin Autoren die Methodenspezifikation überprüft haben, als sie den Code zum ersten Mal geschrieben haben, werden sie die Methode nun falsch behandeln: Ohne null Überprüfungen an Ort und Stelle wird jeder Code der versucht das von getFoo zurückgegebene Foo zu verwenden eine NPE verursachen.

Daraus folgt, dass uns implizit Null-erlaubende Spezifikationen vor die Wahl zwischen verschiedenen Übeln stellt:

  • Die Autoren von Plugins können annehmen, dass alle Methoden null zurückgeben können und programmieren entsprechend defensiv, aber wir haben bereits gesehen, dass dies sehr schnell zu hässlichen Spaghetti Code führt.

  • Die Ersteller der API könnten definieren, dass jede Methode null zurückgeben kann, um das Problem mit der Null-Behandlung auf den Entwickler des Plugins abzuschieben, was das vorherige Problem nur verschlimmert.

  • Die Autoren der API können garantieren, dass keine Methode ihre Spezifikation bzgl. Null in Zukunft mehr ändert. Das bedeutet, dass im Falle der Entfernung eines Features vom Basis-Spiel sie eine der folgenden Möglichkeiten haben:

  • Eine Exception werfen - was nicht wirklich elegant ist, aber immer noch deutlich einfacher zu diagnostizieren ist als eine eine freigelassene NPE irgendwo im Code, die nur sehr schwer zurückverfolgt werden kann

  • Ein „Fake“-Objekt oder eine ungültigen Wert zurückgeben - Das bedeutet, dass der weiterverarbeitende (Plugin) Code weiterhin funktionieren wird, aber es verursacht eine immer größer werdende Belastung für die API-Entwickler, die immer mehr Fake-Objekte erstellen müssen. Daraus könnte früher oder später eine Situation entstehen, dass große Teile der API mit Müll-Objekten gefüllt sind, die nur den Zweck erfüllen, die Teile der API zu unterstützen, die nicht länger verwendet werden können.

Nun sollte klar sein, dass es mit impliziten Null-erlaubenden Spezifikationen ein größeres Klumpen an Kopfschmerzen verbunden sind, die nur noch schlimmer werden, wenn die betroffene API selbst nur eine Schicht über eine ohnehin sehr instabiles Basis-Produkt ist. Glücklicherweise gibt es eine bessere Lösung:

2. Optional und die explizit Null-erlaubende Spezifikation

Wie zuvor schon erwähnt, sind die APIs für Minecraft in einer schwierigen Situation. Am Ende sollen sie eine Plattform bieten, die ein angemessenes Maß an Stabilität bietet, während sie selbst auf einer Plattform (dem Spiel) aufbauen, dass absolut und kein bisschen Stabilität garantiert. Deshalb müssen alle APIs für Minecraft in dem vollen Bewusstsein entwickelt werden, dass sich jeder Aspekt des Spieles zu jeder Zeit, aus einem beliebigen Grund und in unvorstellbarer Art und Weise; bis hin zur kompletten Entfernung; verändern kann!

Diese Dynamik führt zu den oben beschriebenen Problemen mit Null-erlaubenden Methoden Spezifikationen.

Optional löst das zuvor genannte Problem, indem es die implizite Spezifikation durch eine explizite ersetzt. Die API wirbt niemals damit, „Hier ist dein Objekt, Danke und Tschüss“, sondern es präsentiert den Zugriff mit „Hier ist eine Kiste, die das Objekt enthält, nach dem du gefragt hast, oder halt auch nicht“.

../../_images/optionals3.png

Durch das Verpacken der Möglichkeit null zurückzugeben in eine explizite Absprache, können wir das Konzept des Null-Überprüfens durch das leicht abgewandelte Konzept des Könnte nicht vorhanden sein ersetzten. Wir vereinbaren diese Absprache deshalb auch vom ersten Tag an.

Also was bedeutet das?

Um es auf den Punkt zu bringen, die Plugin Autoren müssen sich nicht länger darum kümmern, dass null zurückgegeben wird. Stattdessen wird die ganze Möglichkeit, dass ein spezifisches Objekt nicht vorhanden ist, durch die Struktur des Plugin Codes selbst ausgedrückt. Dies hat das selbe Maß ein eingebauter Sicherheit, wie das ständige Überprüfen auf Null und hat zudem den Vorteil, dass der Code dadurch deutlich eleganter und einfacher zu lesen wird.

Um dies zu zeigen, lasst uns einen Blick auf das Beispiel von oben werfen, allerdings mit der Änderung, dass die getFoo Methode jetzt ein Optional<Foo> zurückgibt:

public void someEventHandler(Entity someEntity) {
    Optional<Foo> entityFoo = someEntity.getFoo();
    if (entityFoo.isPresent()) {
        entityFoo.get().bar();
    }
}

You may note that this example looks very much like a standard null-check, however the use of Optional actually carries a little more information in the same amount of code. For example, it is not necessary for someone reading the above code to check the method contract, it is clear that the method may not return a value, and the handling of the value’s absence is explicit and clear.

So what? Our explicit contract in this case results in basically the same amount of code as a null check - albeit one that is contractually enforced by the getter. „Whoop de do,“ you say, „so what?“

Well the Optional boxing allows us to take some of the traditionally more awkward aspects of null-checking and make them more elegant: consider the following code:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO);
    entityFoo.bar();
}

Hold the phone! Did we just replace the tedious null-check-and-default-assignment from the example above with a single line of code? Yes indeed we did. In fact, for simple use cases we can even dispense with the assignment:

public void someEventHandler(Entity someEntity) {
    someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO).bar();
}

This is perfectly safe provided that MyPlugin.DEFAULT_FOO is always available.

Consider the following example with two entities, using an implicit nullable contract we want to use Foo from the first entity, or if not available use Foo from the second entity, and fall back on our default if neither is available:

public void someEventHandler(Entity someEntity, Entity entity2) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = entity2.getFoo();
    }
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

Using Optional we can encode this much much more cleanly as:

public void someEventHandler(Entity someEntity, Entity entity2) {
    someEntity.getFoo().orElse(entity2.getFoo().orElse(MyPlugin.DEFAULT_FOO)).bar();
}

This is merely the tip of the Optional iceberg. In java 8 Optional also supports the Consumer and Supplier interfaces, allowing lambas to be used for absent failover. Usage examples for those can be found on the Beispiele für die Verwendung page.

Bemerkung

Another explanation on the rationale behind avoiding null references can be found on Guava: Using And Avoiding Null Explained. Beware that the guava Optional class mentioned in the linked article is different from java’s java.util.Optional and therefore will have method names different from those used here.