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