Tenho o prazer de anunciar o lançamento do GA do Couchbase Kotlin SDK versão 1.0. Na verdade, estou muito feliz. Esse projeto tem sido um trabalho de amor. Depois de trabalhar com Java por décadas, tenho uma nova linguagem favorita.
Neste artigo, falarei algumas coisas boas sobre o Kotlin e, em seguida, mostrarei como usar o SDK do Couchbase Kotlin para se conectar ao banco de dados como serviço Capella. Por fim, compartilharei algumas decisões de design que moldaram a API pública do SDK. Espero que você fique atento a essa última parte, especialmente se estiver projetando a API de sua própria biblioteca Kotlin.
Por que Kotlin?
O Kotlin mudou a forma como pensamos sobre a programação assíncrona na JVM. As corrotinas e as funções de suspensão do Kotlin são evidências de que a programação reativa pode ser um trampolim para algo melhor, algo que não exige o sacrifício de um código legível no altar da escalabilidade. O Kotlin mostrou que há uma maneira melhor de escrever código assíncrono de alto desempenho, e não precisamos esperar pelas fibras e continuações do Project Loom.
Capella + Kotlin
Couchbase Capella é o banco de dados como serviço (DBaaS) para o Couchbase Server. É uma tecnologia sólida e, quando me inscrevi para uma avaliação gratuita há algumas semanas, o processo foi totalmente indolor.
Digamos que você tenha um cluster de avaliação do Capella e tenha adicionado seu IP à lista de permissões, além de ter criado um usuário de banco de dados que pode ler o arquivo amostra de viagem bucket. Veja como se conectar ao seu cluster usando o SDK do Kotlin:
1 2 3 4 5 6 |
val endereço = "--seu-cluster--.cloud.couchbase.com" val agrupamento = Aglomerado.conectar( connectionString = "couchbases://$address", nome de usuário = "harpo", senha = "peixe-espada", ) |
Quando você tiver um objeto Cluster, poderá executar uma consulta N1QL:
1 2 3 4 5 |
val resultado da consulta = agrupamento .consulta("SELECT * FROM `travel-sample` LIMIT 3") .executar() resultado da consulta.linhas.forEach { println(ele) } println(resultado da consulta.metadados) |
Ou obtenha uma referência a uma coleção e leia um documento específico:
1 2 3 4 5 6 7 |
val coleção = agrupamento .balde("amostra de viagem") .defaultCollection() val getResult = coleção.obter("airline_10") println(getResult) println(getResult.contentAs<Mapa<Cordas, Qualquer?>>()) |
A versão completa desse exemplo está incluída no arquivo Documentação do Kotlin SDKe vários outros.
Decisões de design da API do SDK
O restante deste artigo é dedicado a compartilhar algumas notas sobre as decisões que tomamos ao projetar a API pública do Couchbase Kotlin SDK. Em alguns casos, estarei comparando o Kotlin SDK com seu irmão mais velho, o Java SDK.
Extensão versus SDK independente
O SDK do Couchbase Kotlin depende do mesmo núcleo-io como o Java SDK, mas não depende do Java SDK.
Alternativas rejeitadas
Consideramos a possibilidade de oferecer suporte ao Kotlin fornecendo funções de extensão para classes do Java SDK. Infelizmente, algumas decisões de design que tomamos para o Java SDK não foram bem traduzidas para o Kotlin e não puderam ser compensadas apenas com funções de extensão.
Também consideramos a possibilidade de fornecer um wrapper completo da API nativa do Kotlin que simplesmente delegasse ao SDK do Java, mas estávamos preocupados com o fato de que ter duas versões de todas as classes (uma para o Kotlin e outra para o Java) seria confuso para os usuários.
Suspender ou falir!
O SDK do Kotlin não oferece uma API de bloqueio; os métodos que fazem E/S de rede são todos suspender funções.
Alternativas rejeitadas
Consideramos a possibilidade de adicionar variantes "bloqueadoras" de Cluster, Bucket, Scope, Collection etc., mas isso parece ser algo que os próprios usuários podem fazer com muito pouco esforço, bastando envolver as chamadas para as funções de suspensão com runBlocking.
Parâmetros opcionais
Como o Java não tem parâmetros opcionais, o Couchbase Java SDK os emula com um "bloco de opções" construído usando o padrão builder.
No exemplo a seguir, com expiração é um parâmetro booleano opcional cujo padrão é false. Os trechos de código mostram um site de chamada em que o desenvolvedor deseja passar true em vez disso.
Java:
1 2 |
GetOptions opções = GetOptions.getOptions().com expiração(verdadeiro); coleção.obter(documentId, opções); |
O SDK do Kotlin aproveita o suporte nativo do Kotlin para parâmetros padrão:
Kotlin:
1 |
coleção.obter(documentId, com expiração = verdadeiro) |
Alternativas rejeitadas
Também consideramos a possibilidade de usar blocos de opções específicos de métodos para o Kotlin, que teriam algo parecido com:
1 |
coleção.obter(documentId, GetOptions(com expiração = verdadeiro)) |
Isso foi rejeitado porque era complicado para os usuários e difícil de manter para os desenvolvedores do SDK (considere o impacto de adicionar uma nova opção comum a todos os métodos).
Também consideramos a possibilidade de usar um lambda/mini-DSL para opções, o que seria algo parecido com:
1 2 3 |
coleção.obter(documentId) { com expiração = verdadeiro } |
Essa foi a mais tentadora das alternativas rejeitadas porque teria sido excelente para a compatibilidade binária (as assinaturas de métodos não mudariam à medida que novos parâmetros opcionais fossem adicionados). Ela também "parece Kotlin". Ela foi rejeitada porque:
-
- O recurso de autocompletar código do IDE não forneceu o mesmo nível de orientação para DSLs e para parâmetros de método (embora os IDEs provavelmente melhorem com o tempo).
- Queríamos reservar o parâmetro lambda final para outros fins.
Parâmetros comuns
Alguns parâmetros opcionais são comuns a muitos métodos na API do SDK do Couchbase. Os exemplos incluem a duração do tempo limite, a estratégia de nova tentativa e a extensão do rastreamento.
Em Java, essas opções comuns são propriedades de uma classe base CommonOptions que todos os blocos de opções específicas de métodos estendem; para o usuário, elas não são diferentes de outros parâmetros:
Java:
1 2 3 4 |
GetOptions opções = GetOptions.getOptions() .com expiração(verdadeiro) .tempo limite(Duração.ofSeconds(10)); coleção.obter(documentId, opções); |
Em Kotlin, adotamos uma abordagem diferente que equilibra a conveniência dos parâmetros padrão com algumas concessões pragmáticas para manutenção e compatibilidade binária. Os parâmetros comuns são representados por um bloco de opções chamado CommonOptions. Os métodos aceitam um parâmetro opcional cujo valor padrão é um CommonOptions que representa as opções padrão. A substituição dos padrões tem a seguinte aparência:
Kotlin:
1 2 3 4 5 |
coleção.obter( documentId, comum = CommonOptions(tempo limite = 10.segundos), com expiração = verdadeiro, ) |
Alternativas rejeitadas
Consideramos a possibilidade de tratar as opções comuns como parâmetros normais, assim:
1 2 3 4 5 |
coleção.obter( documentId, tempo limite = 10.segundos, com expiração = verdadeiro, ) |
Embora isso certamente fosse agradável para os usuários, foi rejeitado porque adicionar ou remover um parâmetro comum exigiria a alteração da assinatura de quase todos os métodos públicos na base de código e tornaria a manutenção da compatibilidade binária uma tarefa árdua. Analisamos a possibilidade de automatizar esse processo usando a geração de código, mas a complexidade dessa abordagem parecia superar o valor.
No final, optamos por usar o CommonOptions como um tipo de anteparo de API para isolar problemas de manutenção relacionados a opções comuns.
Compatibilidade binária
Essas decisões sobre parâmetros comuns e opcionais têm as seguintes implicações para a compatibilidade binária:
A adição de um parâmetro opcional a um método quebra a compatibilidade binária somente para esse método. A compatibilidade pode ser restaurada adicionando-se um método com a assinatura antiga, anotada como Depreciado(level=HIDDEN). O resultado é que o impacto da manutenção é isolado em um único método, e as alterações no código para manter a compatibilidade também têm escopo restrito.
A adição de um parâmetro comum quebra a compatibilidade binária somente para o CommonOptions classe. A compatibilidade pode ser restaurada com a adição de um construtor com a assinatura antiga, anotada como Depreciado(level=HIDDEN). É significativo o fato de que não precisamos alterar a assinatura dos métodos que recebem CommonOptions como um parâmetro.
Parâmetros mutuamente exclusivos
Às vezes, um método pode ter duas formas diferentes de especificar um valor de parâmetro. Por exemplo, vários métodos recebem um expiração que pode ser especificado como um Duração ou um Instantâneo. Na API Java, não há nada que o impeça de escrever esse código:
1 2 3 4 |
UpsertOptions opções = UpsertOptions.upsertOptions() .expiração(Instantâneo.agora().mais(Duração.deMinutos(15))) .expiração(Duração.deMinutos(10)); coleção.upsert("foo", "bar", opções); |
Essas duas formas de especificar a expiração são mutuamente exclusivas, mas o Java permite que você escreva o código de qualquer forma. Se houver uma verificação de validade, ela deverá ocorrer em tempo de execução. (Neste exemplo específico, a segunda chamada para expiração abocanha o valor definido pela chamada anterior).
Em Kotlin, o método upsert tem um único parâmetro de expiração do tipo Expiração, onde Expiração é uma classe selada:
1 |
coleção.upsert("foo", "bar", expiração = Expiração.de(10.minutos)) |
ou
1 2 |
val instantâneo = Instantâneo.agora().mais(Duração.deMinutos(15)) coleção.upsert("foo", "bar", expiração = Expiração.de(instantâneo)) |
Esse padrão é aplicado em toda a API; as opções mutuamente exclusivas são sempre representadas como um único parâmetro que recebe uma instância de uma classe selada cujas instâncias representam as diferentes formas de especificar o valor.
Resultados de streaming
Os serviços Couchbase Query, Analytics, View e Full-Text Search podem retornar conjuntos de resultados muito grandes. Para processar esses resultados com eficiência sem esgotar a pilha, os métodos de consulta desses serviços retornam seus resultados como um fluxo Kotlin.
Fornecemos dois executar métodos de extensão nesse fluxo. Um método armazena as linhas de resultados na memória antes de retornar todo o conjunto de resultados (a ser usado somente quando se sabe que o conjunto de resultados é pequeno). O outro método permite que o usuário forneça um lambda para aplicar a cada linha de resultado à medida que ela é recebida do servidor. Ambas as versões aproveitam o controle de contrapressão/fluxo fornecido pela biblioteca core-io.
Alternativas rejeitadas
Consideramos a possibilidade de expor os objetos Flux/Mono do Project Reactor usados pelo núcleo-io mas decidimos que, depois de experimentar as corrotinas, não sentimos nenhuma falta do Reactor, e acreditamos que a maioria dos usuários terá a mesma opinião.
DSL vs. construtor hierárquico
Os SDKs do Couchbase têm muitas opções de configuração agrupadas em categorias separadas. Para os SDKs da JVM, essas opções são propriedades da variável ClusterEnvironment. Em Java, essas opções são configuradas usando um ClusterEnvironment construtor. Aqui está um exemplo em Java que desativa a compactação, o DNS SRV e o disjuntor do serviço Key/Value:
1 2 3 4 5 6 7 8 |
ClusterEnvironment env = ClusterEnvironment.construtor() .ioConfig(IoConfig .enableDnsSrv(falso) .kvCircuitBreakerConfig(CircuitBreakerConfig .habilitado(falso))) .compressionConfig(CompressionConfig .ativar(falso)) .construir(); |
A API do Kotlin aproveita o suporte a DSL do Kotlin, permitindo que a mesma configuração seja expressa como:
1 2 3 4 5 6 7 |
val env = ClusterEnvironment.construtor { io { enableDnsSrv = falso kvCircuitBreaker { habilitado = falso } } compressão { ativar = falso } } |
A API do Kotlin também permite configurar o ambiente em linha com o método de conexão:
1 2 3 4 5 |
val agrupamento = Aglomerado.conectar(hospedeiro, nome de usuário, senha) { io { enableDnsSrv = falso } } |
Os usuários que preferem o construtor de ambiente de cluster tradicional em vez do DSL podem continuar a usar o construtor, se desejarem.
Alternativas rejeitadas
Poderíamos muito bem ter usado classes de dados em vez de uma DSL:
1 2 3 4 5 6 7 |
val env = ClusterEnvironment( io = IoConfig( enableDnsSrv = falso, kvCircuitBreaker = CircuitBreakerConfig(habilitado = falso), ), compressão = CompressionConfig(ativar = falso), ) |
Isso teria sido bom, mas a DSL é mais concisa e parece mais que estamos jogando com a força do Kotlin.
Resumo
Pensamos muito no projeto da API pública do Couchbase Kotlin SDK. Não posso prometer que fizemos tudo certo, mas espero que o resultado seja algo que respeite os idiomas e as práticas recomendadas do Kotlin.
O SDK do Couchbase Kotlin está finalmente pronto para ser usado na produção, quer você esteja usando o Capella DBaaS ou gerenciando seu próprio cluster do Couchbase Server. Tudo o que não estiver anotado como volátil ou não comprometido agora faz parte oficialmente da API pública estável. Uma enorme Obrigado! agradece a todos da comunidade que compartilharam seus comentários ao longo do caminho.
-
- Saiba mais em Documentação do SDK do Couchbase Kotlin.
- Tem alguma pergunta ou comentário? Encontre-nos em:
- o Fórum do Couchbase
- o Servidor Discord do Couchbaseou
- o canal #couchbase no Espaço de trabalho do Kotlin Slack.