Me complace anunciar el lanzamiento de la versión 1.0 del SDK de Couchbase Kotlin. La verdad es que estoy encantado. Este proyecto ha sido un trabajo de amor. Después de trabajar con Java durante décadas, tengo un nuevo lenguaje favorito.
En este artículo, diré algunas cosas buenas sobre Kotlin, y luego mostraré cómo usar el SDK de Couchbase Kotlin para conectarse a la base de datos como servicio de Capella. Finalmente, compartiré algunas decisiones de diseño que dieron forma a la API pública del SDK. Espero que te quedes para esta última parte, especialmente si estás diseñando la API de tu propia librería Kotlin.
¿Por qué Kotlin?
Kotlin cambió nuestra forma de pensar sobre la programación asíncrona en la JVM. Las coroutines y las funciones de suspensión de Kotlin son la prueba de que la programación reactiva podría ser un peldaño hacia algo mejor, algo que no requiera sacrificar código legible en el altar de la escalabilidad. Kotlin ha demostrado que hay una forma mejor de escribir código asíncrono de alto rendimiento, y no necesitamos esperar a las fibras y continuaciones del Proyecto Loom.
Capella + Kotlin
Couchbase Capella es la base de datos como servicio (DBaaS) para Couchbase Server. Es una tecnología sólida, y cuando me registré para una prueba gratuita hace unas semanas, el proceso fue completamente indoloro.
Digamos que usted tiene un cluster de prueba Capella y ha añadido su IP a la lista de permitidos, y ha creado un usuario de base de datos que puede leer el pre-instalado viaje-muestra cubo. A continuación te explicamos cómo conectarte a tu clúster utilizando el SDK de Kotlin:
1 2 3 4 5 6 |
val dirección = "--tu-cluster--.cloud.couchbase.com" val grupo = Grupo.conecte( connectionString = "couchbases://$address", nombre de usuario = "harpo", contraseña = "pez espada", ) |
Una vez que tenga un objeto Cluster, puede ejecutar una consulta N1QL:
1 2 3 4 5 |
val queryResult = grupo .consulta("SELECT * FROM `viaje-muestra` LIMIT 3") .ejecutar() queryResult.filas.paraCada { println(it) } println(queryResult.metadatos) |
O consiga una referencia a una Colección y lea un documento específico:
1 2 3 4 5 6 7 |
val colección = grupo .cubo("viaje-muestra") .defaultCollection() val obtenerResultado = colección.consiga("aerolínea_10") println(obtenerResultado) println(obtenerResultado.contentAs<Mapa<Cadena, Cualquier?>>()) |
La versión completa de este ejemplo se incluye en el archivo Documentación del SDK de Kotlinjunto con varios otros.
Decisiones de diseño de la API del SDK
El resto de este artículo está dedicado a compartir algunas notas sobre las decisiones que tomamos al diseñar la API pública del SDK Kotlin de Couchbase. En algunos casos, compararé el SDK de Kotlin con su hermano mayor, el SDK de Java.
Extensión frente a SDK independiente
El SDK de Couchbase Kotlin depende del mismo core-io como el SDK de Java, pero no depende de éste.
Alternativas rechazadas
Nos planteamos dar soporte a Kotlin proporcionando funciones de extensión para las clases del SDK de Java. Por desgracia, algunas decisiones de diseño que tomamos para el SDK de Java no se trasladaron bien a Kotlin y no se pudieron compensar solo con funciones de extensión.
También consideramos la posibilidad de proporcionar una envoltura nativa completa de la API de Kotlin que simplemente delegara en el SDK de Java, pero nos preocupaba que tener dos versiones de todas las clases (una para Kotlin y otra para Java) resultara confuso para los usuarios.
¡Suspender o reventar!
El SDK de Kotlin no proporciona una API de bloqueo; los métodos que realizan E/S de red son todos suspender funciones.
Alternativas rechazadas
Consideramos la posibilidad de añadir variantes "bloqueantes" de Cluster, Bucket, Scope, Collection, etc., pero parece algo que los usuarios pueden hacer por sí mismos con muy poco esfuerzo, simplemente envolviendo las llamadas a las funciones de suspensión con runBlocking.
Parámetros opcionales
Como Java no tiene parámetros opcionales, el SDK Java de Couchbase los emula con un "bloque de opciones" construido usando el patrón constructor.
En el siguiente ejemplo, withExpiry es un parámetro booleano opcional cuyo valor por defecto es false. Los fragmentos de código muestran un sitio de llamada en el que el desarrollador desea pasar true en su lugar.
Java:
1 2 |
GetOptions opciones = GetOptions.getOptions().withExpiry(verdadero); colección.consiga(documentId, opciones); |
El SDK de Kotlin aprovecha el soporte nativo de Kotlin para parámetros por defecto:
Kotlin:
1 |
colección.consiga(documentId, withExpiry = verdadero) |
Alternativas rechazadas
También consideramos la posibilidad de utilizar bloques de opciones específicos de método para Kotlin, que habrían tenido un aspecto similar:
1 |
colección.consiga(documentId, GetOptions(withExpiry = verdadero)) |
Se rechazó porque era engorroso para los usuarios y difícil de mantener para los desarrolladores del SDK (considere el impacto de añadir una nueva opción común a todos los métodos).
También consideramos la posibilidad de utilizar un lambda/mini-DSL para las opciones, que habría sido algo así:
1 2 3 |
colección.consiga(documentId) { withExpiry = verdadero } |
Esta era la más tentadora de las alternativas rechazadas porque habría sido excelente para la compatibilidad binaria (las firmas de los métodos no cambiarían al añadir nuevos parámetros opcionales). También "se siente como Kotlin". Fue rechazada porque:
-
- La finalización de código IDE no ofrecía el mismo nivel de orientación para los DSL que para los parámetros de método (aunque es probable que los IDE mejoren con el tiempo).
- Queríamos reservar el último parámetro lambda para otros fines.
Parámetros comunes
Algunos parámetros opcionales son comunes a muchos métodos de la API del SDK de Couchbase. Algunos ejemplos son la duración del tiempo de espera, la estrategia de reintento y el intervalo de rastreo.
En Java, estas opciones comunes son propiedades de una clase base CommonOptions a la que extienden todos los bloques de opciones específicos de los métodos; para el usuario, no se ven diferentes de otros parámetros:
Java:
1 2 3 4 |
GetOptions opciones = GetOptions.getOptions() .withExpiry(verdadero) .tiempo de espera(Duración.deSegundos(10)); colección.consiga(documentId, opciones); |
En Kotlin, adoptamos un enfoque diferente que equilibra la comodidad de los parámetros por defecto con algunas concesiones pragmáticas para la mantenibilidad y la compatibilidad binaria. Los parámetros comunes están representados por un bloque de opciones llamado CommonOptions. Los métodos aceptan un parámetro opcional cuyo valor por defecto es un CommonOptions que representa las opciones por defecto. Anular las opciones predeterminadas tiene el siguiente aspecto:
Kotlin:
1 2 3 4 5 |
colección.consiga( documentId, común = CommonOptions(tiempo de espera = 10.segundos), withExpiry = verdadero, ) |
Alternativas rechazadas
Hemos considerado tratar las opciones comunes como parámetros normales, así:
1 2 3 4 5 |
colección.consiga( documentId, tiempo de espera = 10.segundos, withExpiry = verdadero, ) |
Aunque esto sería agradable para los usuarios, se rechazó porque añadir o eliminar un parámetro común requeriría cambiar la firma de casi todos los métodos públicos de la base de código, lo que haría que mantener la compatibilidad binaria fuera una tarea ardua. Estudiamos la posibilidad de automatizar este proceso mediante la generación de código, pero la complejidad de este enfoque parecía superar su valor.
Al final, optamos por utilizar el CommonOptions como una especie de mamparo de la API para aislar los problemas de mantenimiento relacionados con las opciones comunes.
Compatibilidad binaria
Estas decisiones sobre los parámetros comunes y opcionales tienen las siguientes implicaciones para la compatibilidad binaria:
Añadir un parámetro opcional a un método rompe la compatibilidad binaria sólo para ese método. La compatibilidad puede restaurarse añadiendo un método con la firma antigua, anotado como Deprecated(level=HIDDEN). El resultado es que el impacto del mantenimiento se aísla en un único método, y los cambios en el código para mantener la compatibilidad tienen un alcance igualmente restringido.
La adición de un parámetro común rompe la compatibilidad binaria sólo para el CommonOptions clase. La compatibilidad puede restaurarse añadiendo un constructor con la firma antigua, anotado como Deprecated(level=HIDDEN). Significativamente, no tenemos que cambiar la firma de los métodos que toman CommonOptions como parámetro.
Parámetros mutuamente excluyentes
A veces, un método puede tener dos formas distintas de especificar el valor de un parámetro. Por ejemplo, varios métodos toman un caducidad que puede especificarse como Duración o un Instantánea. En la API de Java, nada te impide escribir este código:
1 2 3 4 |
UpsertOptions opciones = UpsertOptions.upsertOptions() .caducidad(Instantánea.ahora().y(Duración.deMinutos(15))) .caducidad(Duración.deMinutos(10)); colección.upsert("foo", "bar", opciones); |
Estas dos formas de especificar la caducidad son mutuamente excluyentes, pero Java le permite escribir el código de todos modos. Si hay una comprobación de validez, tiene que ocurrir en tiempo de ejecución. (En este ejemplo concreto, la segunda llamada a caducidad borra el valor establecido por la llamada anterior).
En Kotlin el método upsert tiene un único parámetro de caducidad de tipo Caducidaddonde Caducidad es una clase sellada:
1 |
colección.upsert("foo", "bar", caducidad = Caducidad.de(10 minutos)) |
o
1 2 |
val instantánea = Instantánea.ahora().y(Duración.deMinutos(15)) colección.upsert("foo", "bar", caducidad = Caducidad.de(instantánea)) |
Este patrón se aplica en toda la API; las opciones mutuamente excluyentes siempre se representan como un único parámetro que toma una instancia de una clase sellada cuyas instancias representan las diferentes formas de especificar el valor.
Resultados del streaming
Los servicios Couchbase Query, Analytics, View y Full-Text Search pueden devolver conjuntos de resultados muy grandes. Con el fin de procesar estos resultados de manera eficiente sin agotar el montón, los métodos de consulta para estos servicios devuelven sus resultados como un flujo Kotlin.
Ofrecemos dos ejecutar en este flujo. Uno de los métodos almacena las filas de resultados en memoria antes de devolver el conjunto de resultados completo (sólo se utiliza cuando se sabe que el conjunto de resultados es pequeño). El otro método permite al usuario proporcionar una lambda para aplicar a cada fila de resultados a medida que se recibe del servidor. Ambas versiones aprovechan el control de contrapresión/flujo proporcionado por la biblioteca core-io.
Alternativas rechazadas
Consideramos la posibilidad de exponer los objetos del Proyecto Reactor Flux/Mono utilizados por el core-io pero hemos decidido que después de probar las coroutines no echamos de menos Reactor en absoluto, y creemos que la mayoría de los usuarios opinarán lo mismo.
DSL frente a constructor jerárquico
Los SDKs de Couchbase tienen muchas opciones de configuración agrupadas en categorías separadas. Para los SDKs de JVM, estas opciones son propiedades del módulo ClusterEnvironment. En Java, estas opciones se configuran mediante una directiva ClusterEnvironment constructor. He aquí un ejemplo en Java que desactiva la compresión, DNS SRV y el interruptor de servicio Key/Value:
1 2 3 4 5 6 7 8 |
ClusterEnvironment env = ClusterEnvironment.constructor() .ioConfig(IoConfig .enableDnsSrv(falso) .kvCircuitBreakerConfig(CircuitBreakerConfig .habilitado(falso))) .compressionConfig(CompressionConfig .active(falso)) .construya(); |
La API de Kotlin aprovecha el soporte DSL de Kotlin, permitiendo expresar la misma configuración como:
1 2 3 4 5 6 7 |
val env = ClusterEnvironment.constructor { io { enableDnsSrv = falso kvCircuitBreaker { habilitado = falso } } compresión { active = falso } } |
La API de Kotlin también permite configurar el entorno en línea con el método connect:
1 2 3 4 5 |
val grupo = Grupo.conecte(host, nombre de usuario, contraseña) { io { enableDnsSrv = falso } } |
Los usuarios que prefieran el constructor de entornos de clúster tradicional al DSL pueden seguir utilizando el constructor si lo desean.
Alternativas rechazadas
También podríamos haber utilizado clases de datos en lugar de un DSL:
1 2 3 4 5 6 7 |
val env = ClusterEnvironment( io = IoConfig( enableDnsSrv = falso, kvCircuitBreaker = CircuitBreakerConfig(habilitado = falso), ), compresión = CompressionConfig(active = falso), ) |
Eso habría estado bien, pero el DSL es más conciso y da la sensación de que estamos jugando con los puntos fuertes de Kotlin.
Resumen
Hemos puesto mucho cuidado en el diseño de la API pública del SDK Kotlin de Couchbase. No puedo prometer que lo hayamos hecho todo bien, pero espero que el resultado se sienta como algo que respeta los modismos y las mejores prácticas de Kotlin.
El SDK de Couchbase Kotlin está finalmente listo para su uso en producción, ya sea que esté utilizando el Capella DBaaS o la gestión de su propio clúster de Couchbase Server. Todo lo que no esté anotado como volátil o no comprometido ya forma parte oficialmente de la API pública estable. Un enorme Gracias. a todos los miembros de la comunidad que nos han hecho llegar sus comentarios.
-
- Más información en Documentación de Couchbase Kotlin SDK.
- ¿Tiene alguna pregunta o comentario? Encuéntrenos en el:
- el Foro Couchbase
- el Servidor Couchbase Discordo
- el canal #couchbase en el Espacio de trabajo Slack de Kotlin.