Opcionales Explicadas
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.
Vamos a empezar con un poco de historia y mirar como los accesores - particularmente «getters» - típicamente funcionan al no hacer uso de “”Optional””.
1. Contratos implícitos que se pueden anular y porque no sirven
Supongamos que tenemos un objeto API simple “”Entity”” con un método “” getFoo()”” que devuelve la entidad” “Foo””.
En los antiguos tiempos de antaño, nuestra extensión podría buscar y utilizar el “”Foo”” de la entidad usando el “”getter”” como este:
public void someEventHandler(Entity someEntity) {
Foo entityFoo = someEntity.getFoo();
entityFoo.bar();
}
El problema surge porque, cuando se está diseñando la API - tenemos que confiar un contrato implícito en el método de “” getFoo”” con respecto a si el método puede (o no) devolver “”null””. Este contrato implícito puede ser definido de dos maneras:
En el javadoc- esto es malo porque se basa en que el autor del complemento lea el método javadoc, y el contrato puede que no esté claro para el autor del complemento
Usar anotaciones anulables - esto no es ideal porque, en general, estas anotaciones requieren que una herramienta sea de alguna utilidad, por ejemplo, se basa en el IDE o el compilador para manejar las anotaciones.
Vamos a asumir que el método de “”getFoo()”” puede - como parte de su contrato - devolver null. De repente, esto significa que nuestro código anterior es inseguro ya que puede causar en una “”NullPointerException”” si “”entityFoo”” es nula.
public void someEventHandler(Entity someEntity) {
Foo entityFoo = someEntity.getFoo();
entityFoo.bar();
}
Asumamos que nuestro autor de complementos es conocedor de la naturaleza anulable de nuestro método getFoo
y decide arreglar el problema con la comprobación nula. Asumiendo que ellos hayan definido una constante local Foo
, el código resultante se ve así:
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.
Sin embargo, esa no es la única desventaja. Vamos a considerar que el API a largo plazo y asumir que en ese momento el autor escribe una extensión, ellos revisan el javadoc del método y observan que el método garantiza nunca devolver un null (ya que cada Entity tiene un Foo disponible). Excelente! No se requiere ningún complejo cheque null!
Sin embargo, ahora supongamos en una versión posterior del juego, los desarrolladores eliminan o descartan el concepto de “”Foo””. Los autores de API en consecuencia actualizan la API y establecen que de ahora en adelante el método “” getFoo()”” puede devolver “” null”” y escribir esto en el javadoc del método. Ahora hay un problema: incluso autores diligentes de extensiones que revisaron el método del contrato cuando escribieron su código por primera vez, están inconscientemente manejando incorrectamente el método: sin un chequeo de null cualquier código que use “”Foo” que venga de “”getFoo”” va a levantar un NPE.
Thus, we can see that allowing implicit nullable contracts leaves us with a selection of pretty awful solutions to choose from:
Autores de las extensiones pueden asumir que ** todos** los métodos pueden devolver nulos y en consecuencia codificar defensivamente, sin embargo ya hemos visto que esto lleva a un código spaguetti muy rápidamente.
Los autores de la extensión, pueden definir un contrato implícito que se pueda anular en cualquier método de API, en un intento de hacer que la gestión de los null sea problema del autor de la extensión, lo cual solo exacerba el enfoque anterior.
Los autores del API pueden asegurar que nunca será alterado cualquier contrato implícito que se pueda anular y haya sido definido por ellos. Esto significa que en la eventualidad que necesiten gestionar la eliminación de una característica del juego base entonces deben:
Usa una excepción - difícilmente elegante pero seguramente más fácil de diagnosticar que un NPE suelto que se puede disparar en otros lugares en el código y ser difícil de rastrear
Devolver un objeto «falso» o valor inválido - esto significa que consumir (extensión) código seguirá funcionado, pero crea un carga en constante aumento en los desarrolladores de API a partir de este momento, puesto que cada característica obsoleta requerirá la creación de aún más objetos falsos. Esto pronto puede ocasionar una situación donde una gran parte del API está llena con objetos basura cuyo único propósito es darle soporte a partes del API que ya no están en servicio.
Debe quedar bastante claro por ahora que hay algunos dolores de cabeza considerables relacionados con contratos implícitos que se pueden anular, que se pueden hacer más agudos cuando el API en cuestión es una capa sobre una extremedamente inestable base de producto. Afortunadamente, hay una mejor manera:
2. Opcional y el Contrato Explícito que se puede Anular
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!
Esta volatilidad es lo que conduce al problema con el método de los contratos que aceptan valores que se pueden anular descrito arriba.
“Optional” soluciona los problemas anteriores al reemplazar contratos implícitos con otros * explícitos*. El API nunca anuncia, * «aquí tienes el objeto, kthxbai» *, en cambio presenta los accesores con un *»esta es una caja que puede o no contener el objeto que usted pidió, ymmv» *.
Mediante la codificación de la posibilidad de devolver “”nulo “” dentro de un contrato explícito, reemplazamos el concepto de comprobación de null con el concepto más matizado de puede no existir. También estipulamos este contrato desde el día uno.
So, what does this mean?
En pocas palabras, los autores de los plugins ya no tendrán que preocuparse por recibir una respuesta inválida al ejecutar una orden. En su lugar, la posibilidad de que un objeto particular no esté disponible, será codificada en la misma trama de creación del plugin. Esto ofrece la misma seguridad que hacer pruebas constantemente para evitar esas fallas, pero con el beneficio de poder hacerlo con un código mucho más prolijo y legible.
Para ver por qué, veamos el ejemplo anterior, convertido para usar un método getFoo
que devuelve Optional<Foo>
en su lugar:
public void someEventHandler(Entity someEntity) {
Optional<Foo> entityFoo = someEntity.getFoo();
if (entityFoo.isPresent()) {
entityFoo.get().bar();
}
}
Usted puede darse cuenta que este ejemplo se parece mucho a una comprobación de null estándar, sin embargo el uso de “”Optional”” realmente lleva un poco más de información en la misma cantidad de código. Por ejemplo, no es necesario de alguien leyendo el código anterior para revisar el método del contrato, es claro que el método no puede devolver un valor, y el manejo de la ausencia de valor es clara y explícita.
¿Y qué? Nuestro contrato explícito en este caso resulta en básicamente la misma cantidad de código que un cheque nulo, aunque uno este contractualmente obligado por el comprador. «Whoop de do», dices, «so what?»
Bueno, el encuadre “Optional” nos permite tomar algunos de los aspectos más incómodos del testeo de respuestas inválidas y hacerlos más prolijos: considere el siguiente código:
public void someEventHandler(Entity someEntity) {
Foo entityFoo = someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO);
entityFoo.bar();
}
Espere! Acabamos de reemplazar las tediosas pruebas y funciones por defecto que ejemplificamos más arriba con una simple línea de codificación? Sí, eso hicimos. De hecho, para casos sencillos, podemos solucionarlo con la función:
public void someEventHandler(Entity someEntity) {
someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO).bar();
}
Esto es perfectamente seguro siempre que MyPlugin.DEFAULT_FOO
esté siempre disponible.
Considere el siguiente ejemplo con dos entidades, usando un contrato que acepta valores Null implícito, queremos usar Foo
de la primera entidad, o si no está disponible use Foo
de la segunda entity
, y recurramos a nuestro valor predeterminado si ninguno está disponible:
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();
}
Usando``Optional`` nosotros podemos codificar esto mucho más limpiamente como:
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
Ejemplos de Uso page.
Nota
Otra explicación sobre el fundamento de evitar referencias nulas puede encontrarse en “Guava: Usando y Evitando los Null Explicado < https://github.com/google/guava/wiki/UsingAndAvoidingNullExplained/ >”_. Tenga en cuenta que la clase de la guava “”Optional”” mencionada en el artículo vinculado es diferente del “” java.util.Optional”” de java y por lo tanto tendrán nombres de métodos diferentes de los utilizados aquí.