Cómo usar el nuevo all-open plugin de Kotlin 1.0.6

Poco antes de terminar el pasado año, JetBrains lanzó su última versión de Kotlin y entre otras novedades han añadido un plugin muy interesante: All-open plugin.

Introducción

Antes de explicar qué es exactamente, me gustaría recordarte que por defecto todas las clases son finales en Kotlin a no ser que indiquemos lo contrario usando la palabra reservada open:

open class HelloWorld {  
}

¿Qué significa ésto? Que si no indicas que una clase es open no podrás heredar de ella ni sobreescribir ningún método ya que es una clase final.

Desde el punto de vista de programador ésto no debería ser un problema, de hecho yo considero una ventaja que nadie pueda heredar de mis clases para cambiar su comportamiento si no está explícitamente especificado para ello con open.

En cambio, ésto sí es en un problema cuándo necesitas que algunas de tus clases y todos sus miembros sean open explicítamente por requerimientos del framework, como puede ser, por ejemplo, cuándo usas @Component o @Bean en Spring, o cuándo estás creando un nuevo modelo para la base de datos Realm.

¿Qué es?

All-Open Plugin es un nuevo plugin introducido en Kotlin 1.0.6 que nos permite convertir clases con todos sus miembros a no finales sin tener que escribir la palabra reservada open manualmente.

¿Cómo funciona?

Imagina que tenemos un DataStore que se utiliza como punto de entrada en nuestro código para escribir y leer información sin importarnos dónde lo hace realmente. Ejemplo:

class SimpleDataStore {  
   fun read(): String {
      // read something
   }
   fun write(val data: String) {
      // write data to somewhere
   }
}

Si queremos que esta clase se convierta en no final simplemente tenemos que decirle al plugin que queremos que haga su trabajo en nuestra clase. ¿Cómo se hace? A través de anotaciones, para ello necesitamos crearnos una anotación que usaremos para anotar las clases que queremos que se openicen (puede tener cualquier nombre, yo la he llamado AllOpen por simplicidad):

package com.arturogutierrez.example

annotation class AllOpen  

Ahora, necesitamos anotar nuestro DataStore con @AllOpen:

@AllOpen
class SimpleDataStore {  
   fun read(): String {
      // read something
   }
   fun write(val data: String) {
      // write data to somewhere
   }
}

Desde el punto de vista de código hemos terminado, sólo nos queda activar el plugin en nuestro proceso de build. Para ello, necesitamos añadir la dependencia en nuestro build script. Abre el fichero build.gradle de tu proyecto y añade el plugin a dependencies:

buildscript {  
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-allopen:1.0.6"
  }
}

Ahora abre tu fichero build.gradle de tu aplicación, depende de la configuración de tu proyecto puede ser el mismo o no, en el caso de Android solemos tener un build.gradle para el proyecto y otro por módulo por lo que ahora tienes que abrir el perteneciente al módulo, y añade el nuevo plugin con su configuración:

apply plugin: 'kotlin-allopen'

allOpen {  
    annotation("com.arturogutierrez.example.AllOpen")
}

Cómo habrás visto no sólo tenemos que añadir el plugin sino que además tenemos que indicarle que anotación va a usar para buscar las clases que queremos que se conviertan en no finales.

Si quieres usar varias anotaciones en vez de una sólo no hay problema, el plugin también incluye la propiedad annotations (nótese el plural) dónde puedes pasar una lista de anotaciones:

allOpen {  
    annotations("com.arturogutierrez.example.AllOpen", "com.arturogutierrez.example.AllOpenAgain")
}

¡Eso es todo!, ahora cada vez que compiles las clases serán abiertas automáticamente en tiempo de compilación sin que tengas que hacer nada más 🙂.

Ejemplo Real

Si has trabajado con Realm para Android sabrás que cuándo creas un modelo éste no puede ser final para que Realm pueda hacer su magia en tiempo de compilación. Ésto en Java no es un problema porque por defecto todo es no final, en cambio, en Kotlin es al revés, todo es final a no ser que digas que no lo es explícitamente con open.
En el caso de Realm es tedioso (y además feo) tener que poner todo open manualmente por lo que ésto es un caso de uso perfecto para el plugin.

Voy a poner un caso real de mi aplicación Openticator que estoy desarrollando. Tengo un modelo de Realm que describe la cuenta de un usuario de ésta manera:

open class AccountRealm : RealmObject() {  
  @PrimaryKey
  open var accountId: String = ""
  open var name: String = ""
  open var type: String = ""
  open var secret: String = ""
  open var issuer: String = ""
  open var order: Int = 0
  open var category: CategoryRealm? = null
}

Como ves, todo es open para que Realm funcione correctamente. ¿Qué he hecho entonces? Crearme una anotación que he llamado RealmModel para anotar mis modelos con ella y decirle al plugin que use la anotación para convertirlas en open:

@RealmModel
class AccountRealm : RealmObject() {  
  @PrimaryKey
  var accountId: String = ""
  var name: String = ""
  var type: String = ""
  var secret: String = ""
  var issuer: String = ""
  var order: Int = 0
  var category: CategoryRealm? = null
}

Podemos decir que este caso de uso es el caso de uso perfecto para usar este nuevo plugin. Si estás interesado en ver exactamente los cambios que he hecho en la aplicación para usar el plugin puedes ver consultar Pull Request en GitHub.

Limitaciones

Una vez que añades el plugin a tu bulid script no puedes activar o desactivarlo por configuración, lo que sería muy interesante para testing. Por ejemplo, sería muy interesante crearse una anotación @OpenForTesting y aplicar el plugin sólo para testing, sin modificar las clases fuera del ámbito de testeo.

La razón de querer hacer ésto es que no se pueden mockear clases finales (de una forma sencilla), entonces, si quieres mockear una dependencia de la clase que estás testeando y esa dependecia es final probablemente termines haciéndola open o extrayendo una interfaz (cuándo sólo tienes una implementación y no tiene sentido extraer una interfaz para una sola clase en la mayoría de los casos).

Es cierto que ahora con Mockito 2 se pueden mockear clases finales pero el soporte es experimental y, además, es incompatible con el resto de test frameworks que modifiquen bytecode cómo puede ser Robolectric.

Eso es todo, a partir de aquí sólo queda que lo pruebes, experimentes, que busques casos de uso dónde más te cuadre y nos cuentes la experiencia 😊.