Optionals erklärt

Much of SpongeAPI makes use of Java’s Optional system on object accessors, but if you’ve never used Optional before this might seem like a bit of a peculiar way of doing things. You might be tempted to ask: „why do I need to perform an extra step when fetching something from an API object?“

This section gives a brief summary of Optional and explains how - and perhaps more importantly why - it’s used throughout SpongeAPI.

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();
}

In this example, the plugin author is aware that the method can return null and has a constant available with a default instance of Foo which can be used instead. Of course, the plugin could just short-circuit the call entirely, or it could attempt to fetch Foo from somewhere else. The key message is that handling nulls even in simple cases can lead to spaghetti code quite quickly, and moreover relies on the plugin author to explicitly visit the method’s contract to check whether a null check is necessary in the first place.

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.

Thus, we can see that allowing implicit nullable contracts leaves us with a selection of pretty awful solutions to choose from:

  • 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

As mentioned above, APIs for Minecraft are in a difficult situation. Ultimately, they need to provide a platform with a reasonable amount of implied stability atop a platform (the game) with absolutely no amount of implied stability. Thus, any API for Minecraft needs to be designed with full awareness that any aspect of the game is liable to change at any time for any reason in any way imaginable; up to and including being removed altogether!

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.

So, what does this mean?

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 lambdas 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.