
Compartilhar:
Sina é um ex-membro da equipe da Vonage. Ele atuou como Java Developer Advocate na Vonage. Com formação acadêmica, ele tem uma curiosidade geral por tudo o que se relaciona a carros, computadores, programação, tecnologia e natureza humana. Em seu tempo livre, ele gosta de caminhar ou jogar videogames competitivos.
Apresentamos o SDK do servidor Kotlin da Vonage
Tempo de leitura: 16 minutos
Introdução
Quando o Kotlin surgiu, no início da década de 2010, poucos poderiam imaginar que ele se tornaria uma das 20 principais linguagens de programação que é hoje, superando outras linguagens consagradas da JVM, como Groovy e Scala. Desde que se tornou a linguagem padrão para o desenvolvimento de aplicativos Android, o ecossistema só tem se expandido com recursos avançados e uma conferência anual dedicada.
Apesar da cadência atualizada de lançamentos e da introdução de novos recursos para se manter relevante, o Kotlin continua sendo uma linguagem popular além das Applications móveis. Para o desenvolvimento do lado do servidor, o Kotlin é uma excelente escolha por várias razões: um rico ecossistema de bibliotecas existentes (algo de que todas as linguagens baseadas na JVM se beneficiam), mesmo a versão 2.0 mais recente ainda é compatível com o bytecode do Java 8, que já tem uma década, foi projetado de forma elegante desde o início, sem recursos legados, e, de modo geral, torna a programação um prazer, com o mínimo de código repetitivo e sintaxe e semântica simplificadas, poderosas e intuitivas.
Tenho o prazer de anunciar que, após trabalhar nisso em tempo integral por um trimestre, o SDK do Vonage para Kotlin já está oficialmente disponível e com suporte a partir da v1.0.0! Ele foi publicado no Maven Central, onde você também encontrará instruções para usá-lo em seu sistema de compilação. Com este SDK, você pode usar todas as APIs da Vonage em fase de lançamento geral (GA) em Kotlin para fazer chamadas de voz, enviar e receber SMS e criar aplicativos de vídeo. Nesta postagem do blog, explicarei como e por que este SDK foi criado.
Contexto
Reconhecendo a crescente prevalência do Kotlin no lado do servidor e o desânimo geral que muitos desenvolvedores de Kotlin sentem quando precisam trabalhar com Java, pensamos em disponibilizar trechos de código em Kotlin para o Java Server SDK. Afinal, uma das vantagens do Kotlin é sua compatibilidade perfeita com o Java. No entanto, decidimos dar um passo além. Queríamos oferecer uma experiência verdadeiramente de primeira classe para os desenvolvedores de Kotlin, mas sem o custo e o trabalho de manutenção envolvidos no desenvolvimento de um SDK totalmente do zero. Nosso objetivo era criar uma prova de conceito em um período relativamente curto, que eventualmente levasse a um SDK completo, mantendo o uso do Java SDK como base para sua implementação e modelo de dados. O resto está... bem, no histórico de commits!
Motivação
A pergunta que naturalmente me veio à mente foi: o que há de errado com o SDK do Java? Afinal, venho mantendo-o mais ou menos em tempo integral desde o segundo trimestre de 2022, e o próprio Kotlin faz grande parte do trabalho pesado para garantir a compatibilidade. A resposta está na diferença entre os recursos de linguagem do Kotlin e do Java. A reputação do Java de ser prolixo, combinada com a relutância das empresas em migrar do Java 8 (ou, pelo menos, em fazê-lo a um ritmo glacial) e o fato de que a compatibilidade entre as duas linguagens ocorre no nível do bytecode, significa que o que pode ser idiomático em Java não necessariamente se traduz bem para o Kotlin. Além disso, o impressionante arsenal de recursos poderosos da linguagem Kotlin só pode ser aproveitado por código escrito em Kotlin.
Nullabilidade
Uma das características que diferenciam o Kotlin do Java é a distinção entre valores nulos e não nulos. Isso não é alcançado por meio de uma estrutura monádica como java.util.Optional; trata-se de uma característica nativa da linguagem e de sua sintaxe. Portanto, Any (o equivalente do Kotlin a Object) e Any? são dois tipos distintamente diferentes: o primeiro é não nulo e o segundo é nulo. Como o Java não possui essa distinção (exceto para tipos primitivos), o Kotlin possui a noção de “tipos de plataforma”, que são indicados por um ponto de exclamação (por exemplo, Any!). Isso contorna o recurso de nulosidade do Kotlin por uma questão de compatibilidade. A criação de um SDK do Kotlin oferece a oportunidade de ser explícito sobre o que é e o que não é nulo, tanto nos parâmetros de função quanto nos tipos de retorno.
Parâmetros nomeados, padrão e opcionais
A principal vantagem da nulabilidade explícita talvez seja mais bem percebida nos argumentos de função. Usar o `Optional` em Java não é exatamente elegante, mas, em Kotlin, os parâmetros opcionais podem ser tornados explícitos no código, declarando-os como nulos e definindo-os como `null` por padrão. Isso permite que os usuários especifiquem apenas o que realmente querem e precisam. Além disso, o Kotlin possui parâmetros de função nomeados, o que significa que os argumentos não precisam ser fornecidos com base em sua posição; eles podem ser declarados explicitamente, permitindo que o usuário ignore certos parâmetros opcionais e deixe claro qual parâmetro está fornecendo, sem precisar depender de sugestões de código de um IDE — o que é particularmente útil quando há vários parâmetros do mesmo tipo. Além disso, a capacidade de definir valores padrão, mesmo para parâmetros não nulos, tem a vantagem de documentar explicitamente o padrão e significa que apenas um método precisa ser declarado para cada função, em vez de sobrecargas com um número variável de argumentos para cada combinação, como é o caso no Java.
Por exemplo, uma operação comum na maioria das nossas APIs é listar recursos — normalmente, esse é o recurso central de uma API — usuários, chamadas, Applications, transmissões etc. A assinatura do método listRender na Video API ilustra bem o ponto: valores padrão para o count e offset são fornecidos. Como ambos são do mesmo tipo, seria impossível fornecer quatro variantes diferentes desse método em Java. Em Kotlin, basta um único método para cobrir todos os quatro casos: sem parâmetros, offset apenas, contagem apenas e ambos os parâmetros. Eles seriam chamados da seguinte forma:
video.listRenders() // Default params
video.listRenders(count = 25) // Count only
video.listRenders(offset = 10) // Offset only
video.listRenders(25, 10) // Both params Lambdas para desenvolvedores
Outro ponto negativo do SDK do Java é sua forte dependência de construtores. Eu já já escrevi anteriormente sobre o padrão Buildere de como ele só existe porque a linguagem não possui parâmetros de construtor nomeados e padrão (opcionais). Em alguns casos, o Kotlin nos permite eliminar totalmente o uso de construtores, recorrendo apenas a parâmetros nomeados/opcionais. Um bom exemplo disso é a implementação da Number Insight API. Observe como, nos bastidores, o construtor do SDK do Java é usado, mas a expressividade do Kotlin nos permite distinguir parâmetros obrigatórios e opcionais usando apenas a assinatura do método.
No entanto, reimplementar tudo usando esse padrão exigiria mais trabalho (e duplicação) do que o previsto no escopo deste projeto e aumentaria a carga de manutenção. Em vez disso, o padrão builder estabelecido no SDK do Java pode ser reutilizado no Kotlin de maneira bastante elegante usando “lambdas finais”; um recurso comumente utilizado na criação de linguagens específicas de domínio (DSLs). Para ilustrar, vamos começar com alguns exemplos básicos. Na implementação da Messages API, há métodos utilitários para cada combinação válida de tipo de mensagem e canal. Cada função recebe como entrada uma expressão lambda com a classe `Builder` apropriada como seu receptor. Isso significa que, da perspectiva do usuário, enviar um SMS ficaria assim:
vonage.messages.send(smsText {
from("Kotlin SDK")
to(System.getenv("TO_NUMBER"))
text("Hello, World!")
})Os métodos de, até e texto são todos provenientes do SmsTextRequest.Builder do SDK do Java e sua superclasse MessageRequest.Builder, incluindo a documentação e os parâmetros. No entanto, da perspectiva de um desenvolvedor Kotlin, todo o processo de obter o construtor e chamar o método build() fica oculto. Combinado com os parâmetros padrão, isso significa que, se todos os parâmetros de um construtor forem opcionais, ele pode ser totalmente omitido. Por exemplo, veja a seguir a implementação do createSession na Video API:
fun createSession(properties: CreateSessionRequest.Builder.() -> Unit = {}): CreateSessionResponse =
client.createSession(CreateSessionRequest.builder().apply(properties).build())Para os parâmetros padrão, pode ser chamado da seguinte forma:
val session = vonage.video.createSession()Para fornecer parâmetros opcionais, pode-se usar a sintaxe lambda final, desta forma:
val session = vonage.video.createSession {
mediaMode(MediaMode.RELAYED)
archiveMode(ArchiveMode.MANUAL)
}Também é possível usar lambdas finais para fornecer valores opcionais. Talvez os exemplos mais complexos estejam na implementação da Voice API, onde a complexidade de definir NCCOs no SDK do Java se beneficia ao máximo dessa abordagem. Para ilustrar, segue um exemplo de como criar uma chamada de voz para um número de telefone com duas ações — Falar e Connect — no SDK do Java, juntamente com algumas outras configurações, escrito em Kotlin:
val callEvent = javaClient.voiceClient.createCall(Call.builder()
.to(PhoneEndpoint("448001234567", "1p2#5"))
.fromRandomNumber(true)
.advancedMachineDetection(AdvancedMachineDetection.builder().build())
.ncco(
TalkAction.builder("Hello, this is a text-to-speech call.")
.language(TextToSpeechLanguage.UNITED_KINGDOM_ENGLISH)
.premium(true)
.build(),
ConnectAction.builder()
.endpoint(SipEndpoint.builder("sip:me@example.org").build())
.ringbackTone("http://example.com/ringback.mp3")
.build()
)
.ringingTimer(30)
.eventUrl("https://example.com/webhooks/events")
.build()
)Embora seja relativamente legível com a indentação adequada, não é tão elegante quanto uma abordagem mais idiomática. Aqui está o mesmo código escrito usando o SDK do Kotlin:
val callEvent = vonage.voice.createCall {
toPstn("448001234567", "1p2#5")
fromRandomNumber(true)
advancedMachineDetection()
ncco(
talkAction("Hello, this is a text-to-speech call.") {
language(TextToSpeechLanguage.UNITED_KINGDOM_ENGLISH)
premium(true)
},
connectToSip("sip:me@example.org") {
ringbackTone("http://example.com/ringback.mp3")
}
)
ringingTimer(30)
eventUrl("https://example.com/webhooks/events")
}Observe como a sintaxe da DSL flui de maneira mais natural. Em particular, o função toPstn libera o usuário da necessidade de chamar a função to diretamente e observe como, na ausência de qualquer configuração para a Detecção Avançada de Máquinas, nenhum construtor ou lambda é necessário. No entanto, para o talkAction, o parâmetro obrigatório — o texto a ser falado — faz parte do construtor, enquanto os parâmetros opcionais estão na lambda. Da mesma forma, para o connectToSip , a URI é obrigatória, com uma lambda opcional para configuração adicional. Em ambos os casos, a lambda pode ser omitida, desta forma:
val callEvent = vonage.voice.createCall {
toPstn("448001234567", "1p2#5")
fromRandomNumber(true)
advancedMachineDetection()
ncco(
talkAction("Hello, this is a text-to-speech call.")
connectToSip("sip:me@example.org")
)
ringingTimer(30)
eventUrl("https://example.com/webhooks/events")
}Você pode ver como isso é implementado nas funções connectTo* da Voice API; que combinam dois construtores em uma única chamada de função. Os parâmetros das funções referem-se ao endpoint ao qual se está conectando, e o lambda final serve para configurar o ação em si.
Termos de referência baseados em recursos
Além de aproveitar os recursos interessantes do Kotlin para reduzir o código repetitivo, outra diferença entre o SDK do Kotlin e o do Java é que o SDK do Kotlin adota uma abordagem baseada em recursos para realizar chamadas de API. Na maioria das APIs da Vonage, há pelo menos um recurso que pode ser consultado, criado, atualizado e excluído. Para cada um desses recursos, existe uma classe correspondente que fornece acesso a esses endpoints. O que todos esses recursos têm em comum é um identificador único. Em vez de precisar armazenar esse identificador em cache em outro lugar para fazer chamadas de API repetidamente a um recurso específico, o SDK cuida disso, de modo que o ID não precise ser fornecido todas as vezes. Esses recursos podem até mesmo ser aninhados, como exemplificado pela implementação da Video API. Veja um exemplo:
val existingSession = vonage.video.session(sessionId)
val streams = existingSession.listStreams()
existingSession.connection(connectionId).sendDtmf("1234")Isso fará muito mais sentido ao usar um IDE com autocompletar. Nesse sentido, as chamadas de API são agrupadas por tipo de recurso. Especialmente em APIs complexas, como a de Video, onde há recursos e IDs aninhados, isso torna a tarefa de fornecer um ID menos propensa a erros, já que o parâmetro necessário é fornecido por padrão. Isso permite que as assinaturas de métodos para recursos aninhados sejam simplificadas, para que você possa se concentrar em fornecer parâmetros significativos separadamente da identificação do recurso. Em contrapartida, o SDK do Java é “plano/sem contexto”, no sentido de que geralmente há um mapeamento um-para-um entre os endpoints na especificação da API e os endpoints no SDK. Essa abordagem reforça ainda mais a separação entre parâmetros obrigatórios e opcionais e significa que é mais provável que uma solicitação esteja correta por definição.
Dê uma olhada na especificação da API para silenciar um stream de vídeo como exemplo. Fica claro que as coordenadas do endpoint exigem o ID da sessão e o ID da transmissão. Na implementação do SDK do Java, é possível confundir, por engano, o ID da sessão e o ID da transmissão ao passá-los na ordem errada. Em contrapartida, a implementação do SDK do Kotlin não requer nenhum parâmetro, já que nenhum dado está sendo enviado. Da mesma forma, para adicionar um Stream a um Archive, a implementação do SDK do Kotlin possui um único método no qual fica claro que o ID do stream é o parâmetro que deve ser passado, dado o contexto. Em contrapartida, a implementação do SDK do Java possui dois métodos (devido aos parâmetros opcionais) e depende da nomenclatura dos métodos e da documentação para descrever como funciona e o que faz.
Talvez seja uma questão de preferência pessoal; não se pode afirmar que uma das abordagens seja objetivamente melhor que a outra. No entanto, a consistência com que o SDK do Kotlin segue essa abordagem em todas as APIs, sempre que possível, é um aspecto diferenciador significativo, especialmente quando se leva em conta a forte dependência do SDK do Java em relação aos construtores, mesmo para parâmetros obrigatórios.
Extensões
Outro recurso interessante do Kotlin é funções de extensão; a capacidade de definir métodos em classes existentes sem precisar estendê-las. Isso é especialmente útil para aprimorar os construtores definidos no SDK do Java, tornando-os mais idiomáticos no Kotlin. Veja, por exemplo, a API de Aplicativos. Cada aplicação da Vonage pode ter múltiplas capacidades, uma de cada tipo. Cada capacidade possui um ou mais webhooks, e cada webhook tem um tipo, como answer_url, status_url, event_url etc. No entanto, dependendo do tipo de Capacidade, apenas determinados Webhooks são aplicáveis. Por exemplo, a Capacidade “Verify” possui apenas status_url, enquanto a capacidade “Voice” possui answer_url, fallback_answer_url e event_url. No momento da redação deste artigo, a implementação do SDK Java para isso é bastante desajeitada. Para ilustrar, segue um exemplo de como atualizar o nome de uma aplicação existente, remover o recurso “Mensagens” e atualizar os webhooks para “Verify” e “Voice”. Observe que, para atualizar um aplicativo, ele precisa ser recuperado primeiro, pois qualquer configuração fornecida substituirá o aplicativo existente. Aqui está o código escrito usando o SDK Java:
val ac = javaClient.applicationClient
val existing = ac.getApplication(appId)
val updated = ac.updateApplication(Application.builder(existing)
.name("My Updated Application")
.addCapability(Verify.builder()
.addWebhook(Webhook.Type.STATUS, Webhook.builder()
.address("https://example.org/webhooks/verify/status")
.method(HttpMethod.POST)
.build()
)
.build()
)
.addCapability(Voice.builder()
.addWebhook(Webhook.Type.ANSWER, Webhook.builder()
.address("https://example.org/webhooks/voice/answer")
.method(HttpMethod.POST)
.build()
)
.addWebhook(Webhook.Type.EVENT, Webhook.builder()
.address("https://example.org/webhooks/voice/event")
.method(HttpMethod.GET)
.build()
)
.build()
)
.removeCapability(Capability.Type.MESSAGES)
.build()
)São muitos construtores! É fácil se confundir, e não há nada no design que impeça você de especificar o tipo errado de webhook para uma funcionalidade. Compare isso com o SDK do Kotlin:
val ac = vonage.application
val application = ac.application(appId)
application.update {
name("My Updated Application")
verify {
status {
url("https://example.org/webhooks/verify/status")
method(HttpMethod.POST)
}
}
voice {
answer {
url("https://example.org/webhooks/voice/answer")
method(HttpMethod.GET)
}
event {
url("https://example.org/webhooks/voice/event")
method(HttpMethod.POST)
}
}
removeCapability(Capability.Type.MESSAGES)
}Observe que nem precisamos acessar o aplicativo usando o endpoint — o SDK cuida disso para você. Além disso, as funções de extensão definidas em cada Capability restringem os webhooks que podem ser definidos, de modo que tudo fica correto por definição. Isso ficará mais claro quando visualizado em um IDE com autocompletar, mas, basicamente, quando você estiver dentro, por exemplo, da bloco , as únicas opções que aparecerão são responder, resposta de fallback e event, pois esses são os únicos definidos no construtor como uma extensão. Não há nenhuma menção a “build” em lugar algum, e você nem precisa especificar o tipo de webhook diretamente: basta declarar o elemento apropriado.
São pequenos detalhes como esse que podem melhorar a experiência e a produtividade dos desenvolvedores. É claro que, como na Vonage não geramos automaticamente nenhum dos nossos SDKs, isso também poderia ser aplicado em Java. Na verdade, durante o processo de desenvolvimento do SDK em Kotlin, elaborei uma lista de itens a serem alterados no SDK em Java, tanto grandes quanto pequenos. No momento em que escrevo este artigo, essa lista poderia ser convertida em 55 tickets do JIRA! No entanto, é um detalhe interessante que melhorias fáceis possam ser feitas com funções de extensão. É claro que nem todas as melhorias do SDK do Kotlin podem — nem devem — ser adaptadas para o SDK do Java.
Documentação
O SDK do Kotlin é documentado usando o KDocs. Cada função e classe possui documentação redigida manualmente sobre parâmetros, tipo de retorno, exceções e uma descrição de sua função. Além disso, essas documentações são publicadas no moderno formato HTML do Dokka, mantendo o estilo da documentação da API do Kotlin, em contraste com o formato Javadocs, que parece relativamente ultrapassado. Você pode consultar a documentação usando qualquer serviço capaz de renderizar documentos a partir de arquivos JAR, como o Javadoc.io. É claro que a documentação também está disponível diretamente no seu IDE.
Exemplos de código
Você encontrará trechos de código para todas as APIs compatíveis em nosso Portal do Desenvolvedor. Basta acessar o produto de seu interesse e, na seção “Crie sua solução”, você encontrará os trechos de código, nos quais o Kotlin será uma das linguagens disponíveis. Você também pode conferi-los diretamente e executá-los usando o repositório de trechos de código Kotlin da Vonage no GitHub. Por exemplo, aqui está um trecho de texto-para-voz “Hello World” usando a Voice API no SDK do Kotlin. Cada trecho de código, conforme exibido na página da documentação, também possui um link para o código-fonte correspondente no GitHub. Para executar os exemplos, basta fazer o check-out do repositório e definir as variáveis de ambiente; em seguida, execute-os via Gradle ou seu IDE. As instruções para isso podem ser encontradas no arquivo READMEdo repositório.
Testes
O SDK possui 99% de cobertura de código no momento da redação deste artigo. Todas as funções são testadas, com parâmetros obrigatórios e opcionais. Até mesmo os elementos que residem no SDK do Java são testados quanto à integridade. A abordagem adotada é de testes de ponta a ponta/integração, uma vez que o SDK do Java já testa o modelo de dados e a validação de parâmetros. Decidi usar o WireMock para isso; uma estrutura para testes de API.
Eu criei uma pequena biblioteca de funções de alto nível para facilitar a verificação dos corpos e cabeçalhos das solicitações e respostas. O resultado é que cada teste se parece com uma especificação; exatamente como todos os bons testes deveriam ser. A estrutura de um teste consiste em simular a URL do endpoint esperada, o método HTTP, o corpo da solicitação (JSON ou parâmetros de consulta), os cabeçalhos (autenticação, tipo de conteúdo, agente do usuário), os parâmetros de resposta esperados (se houver, no formato JSON) e o código de status HTTP. Isso basicamente reflete a especificação da API no código. Para tornar a escrita de testes menos prolixa e menos propensa a erros, os corpos de solicitação e resposta não são escritos diretamente como JSON, mas como um Map<String, Any>. Isso pode ser serializado usando o Jackson; portanto, basta fornecer os nomes corretos das propriedades e os valores esperados.
Aqui está um exemplo que testa o envio de uma nova solicitação a um usuário na Verify API. Você pode ver todos os elementos em ação aqui: a abordagem declarativa para definir a especificação e como ela captura todos os elementos da especificação OpenAPI. Na prática, a maioria dos testes no SDK usa métodos de nível ainda mais alto, declarados no mesmo arquivo, para reduzir ainda mais a duplicação e a complexidade. Além disso, todos os parâmetros são testados; por isso, muitas vezes cada endpoint terá dois métodos de teste: um para todos os parâmetros e outro apenas para os parâmetros obrigatórios. Os testes mais complexos são os da Voice API; mesmo assim, a abordagem declarativa torna esses testes muito mais legíveis e fáceis de manter do que os testes comparativamente prolixos que usam JSON diretamente no SDK Java. Por exemplo, compare os testes da Video API no SDK do Java e SDK do Kotlin; tenho certeza de que você vai concordar!
A vantagem de ter testes escritos desde o início de forma independente da implementação subjacente é que, no futuro, qualquer pessoa poderá reescrever o SDK em Kotlin puro, sem precisar usar o SDK em Java. Os testes descrevem a especificação; portanto, apenas os usos reais do SDK precisariam ser alterados, e não os mocks. A única coisa que o SDK em Kotlin não testa é a validação de parâmetros; ou seja, se determinadas combinações de parâmetros são válidas ou se há parâmetros obrigatórios nos construtores. A validação do modelo de dados é testada no SDK em Java e, propositalmente, não foi duplicada no SDK em Kotlin. Alguns podem até argumentar que o backend da API deveria fazer a validação e retornar uma resposta de erro, e não o SDK. Para evitar a duplicação da lógica de validação e o aumento excessivo dos testes e do tempo de desenvolvimento, isso foi intencionalmente deixado fora do escopo, pois se trata de uma questão ortogonal.
Próximos passos
Embora o SDK agora seja oficialmente compatível com implementações de todas as APIs da Vonage em estado de Disponibilidade Geral, isso é apenas o começo. Além de acompanhar os novos recursos e APIs em sincronia com o SDK Java, há tutoriais a serem escritos, integrações a serem desenvolvidas e diversas melhorias a serem feitas com base no feedback dos usuários. Por falar nisso, agradecemos muito as contribuições e sugestões da comunidade, mesmo que isso signifique mudanças significativas. Experimente o SDK e conte-nos o que você achou!
Encerrando
Por enquanto é só isso. Se você encontrar algum problema ou tiver sugestões de melhorias, fique à vontade para abrir um ticket no GitHubou entre em contato conosco no X (antigo Twitter) ou dar uma passada no nosso Slack da Comunidade. Espero que você tenha uma experiência agradável ao usar o SDK do Kotlin e aguardo ansiosamente seus comentários.
Compartilhar:
Sina é um ex-membro da equipe da Vonage. Ele atuou como Java Developer Advocate na Vonage. Com formação acadêmica, ele tem uma curiosidade geral por tudo o que se relaciona a carros, computadores, programação, tecnologia e natureza humana. Em seu tempo livre, ele gosta de caminhar ou jogar videogames competitivos.