Opcionales Explicadas

Gran parte de la API de Sponge hace uso del sistema “Opcional” de Java sobre los accesores del objeto, pero si nunca ha usado “”Opcional “” antes de esto podría parecerle una manera peculiar de hacer las cosas. Usted puede estar tentado a preguntar: «¿por qué necesito realizar un paso adicional al recuperar algo de un objeto API?»

Esta sección proporciona un breve resumen de “” opcional “” y explica cómo - y quizás aun más importante por qué - es utilizado por medio de la API de Sponge.

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

../../_images/optionals1.png

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.

../../_images/optionals2.png

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

En este ejemplo, el autor de la extensión es consciente de que el método puede devolver null y tiene una constante con una instancia predeterminada de “”Foo”” que se puede utilizar en su lugar. Por supuesto la extensión podría ocasionar un cortocircuito a la llamada o podría intentar buscar un “Foo”. El mensaje clave es que gestionar nulls incluso en casos sencillos puede conducir un código spaghetti muy rápidamente y además depende en que el autor de la extensión deba visitar expresamente el método del contrato para verificar si algún cheque nulo es necesario en primer lugar.

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.

Por lo tanto, podemos ver que permitir contratos implícitos y que se puedan anular nos deja con una selección de soluciones bastante malas para elegir:

  • 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

Como se mencionó anteriormente, APIs para Minecraft están en una situación difícil. En última instancia, deben proporcionar una plataforma con una cantidad razonable de estabilidad implícita encima de una plataforma (el juego) con absolutamente ninguna cantidad de estabilidad implícita. Por lo tanto, cualquier API para Minecraft necesita ser diseñada con completa conciencia de que cualquier aspecto del juego puede cambiar en cualquier momento y por cualquier motivo de cualquier manera imaginable; hasta e incluyendo ser eliminado por completo!

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» *.

../../_images/optionals3.png

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.

Entonces ¿qué significa esto?

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

Esto es sólo la punta del iceberg “”Optional””. En java 8 “”Optional”” también soporta las interfaces “”Consumidor”” y “”Proveedor””, permitiendo usar lambas para la conmutación por error *ausente *. Ejemplos de uso para estos pueden encontrarse en la página de “uso” de :doc:.

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