
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.
Como um SDK pode agregar valor às APIs REST
Tempo de leitura: 12 minutos
A API do Vonage Messages v1 foi recentemente promovida ao status de Disponibilidade Geral. Após esse anúncio, a Equipe de Ferramentas de Relações com Desenvolvedores tem trabalhado intensamente para implementar o suporte a ela em nossos SDKs de servidor.
Coletivamente, decidimos que cada SDK deveria implementar a API da maneira que fizesse mais sentido, levando em conta a linguagem, a comunidade e a abordagem geral utilizada para dar suporte a outras APIs no SDK, a fim de garantir a consistência. Nosso objetivo é garantir que cada SDK pareça idiomático, em vez de adotar uma abordagem genérica em que a implementação pareça ter sido gerada automaticamente. A Messages API v1 é um bom exemplo para ilustrar isso, daí a razão deste artigo.
Naturalmente, como defensor dos desenvolvedores Java da equipe, vou me concentrar na implementação do SDK do Java e descrever a lógica por trás de seu projeto. Mas, primeiro, vamos examinar a especificação da Messages API, que é o que precisamos implementar.
A API possui um único endpoint, que consiste em enviar uma mensagem por meio de uma solicitação POST. A complexidade decorre do esquema das mensagens. Há dois conceitos principais aqui: canais e tipos de mensagem. O primeiro se refere ao serviço utilizado para enviar a mensagem (SMS, MMS, WhatsApp, Viber, Facebook Messenger). O segundo descreve o formato da mensagem (texto, imagem, áudio, Video, arquivo etc.).
Cada canal suporta um subconjunto de tipos de mensagens. Por exemplo, não é possível enviar um Video por SMS. Alguns canais, como o WhatsApp, suportam modelos e tipos de mensagens personalizadas, o que permite enviar mensagens mais complexas ou compartilhar sua localização.
Além disso, cada canal impõe restrições específicas ao conteúdo das mensagens. Por exemplo, o limite de tamanho das mensagens de texto varia de acordo com o canal, assim como os tipos de arquivos suportados para mensagens multimídia — por exemplo, o WhatsApp suporta uma ampla variedade de formatos de áudio, enquanto o Messenger suporta apenas MP3. Para mensagens multimídia, alguns canais permitem uma descrição de texto opcional (conhecida como legenda na especificação da API) para acompanhar o arquivo, enquanto outros não.
Isso pode até variar de acordo com os canais — por exemplo, é possível incluir uma legenda em um MMS, exceto se for um vCard (que, aliás, é exclusivo do canal MMS). Do ponto de vista do desenvolvedor, o desafio é, então, encontrar uma maneira adequadamente estruturada e eficiente de atender a esses requisitos, minimizando ao mesmo tempo a confusão para os usuários da API.
Como desenvolvedor Java, parece natural começar pelo modelo de dados. Inevitavelmente, acabamos criando um tipo abstrato para representar uma mensagem, que é aceito pelo sendMessage método MessagesClient. Como a resposta que recebemos ao enviar uma mensagem é a mesma, independentemente do tipo de mensagem e do canal, não precisamos nos preocupar com a mensagem em si; basta encaminhá-la como uma carga JSON, e o que recebemos de volta é um identificador único da mensagem enviada. Com isso em mente, nosso tipo abstrato MessageRequest separa os campos comuns a todas as mensagens — a saber: o tipo, o canal, o remetente, o destinatário e uma string opcional de referência do cliente. Como os canais e os tipos de mensagem são predefinidos, nós os representamos como enumerações.
Há uma combinação de parâmetros opcionais e obrigatórios (por exemplo, a referência do cliente é opcional, mas o remetente e o destinatário são obrigatórios). Existem também outros parâmetros, dependendo das combinações de canal e tipo de mensagem. Como o Java não possui argumentos nomeados, usamos o padrão builder para simular isso. Você pode ler mais sobre isso aqui.
Queremos garantir que apenas combinações válidas de tipo de mensagem e canal possam ser criadas; por isso, definimos essa matriz no Channel. Validamos todos os argumentos nos construtores de MessageRequest e seus subtipos. É aí que reside, na minha opinião, a principal vantagem do projeto da implementação da Messages API pelo SDK do Java: tudo é correto por construção.
O que isso significa exatamente? Significa que, em teoria, é difícil — e, idealmente, impossível — criar uma mensagem inválida. Desafio qualquer pessoa que esteja lendo isso a tentar “usar indevidamente” a Messages API usando o SDK do Java. Vamos tentar juntos.
MessageRequest é abstrato, por isso precisamos usar um de seus subtipos concretos. Todos esses subtipos (por exemplo, MmsImageRequest) já definem o Channel e MessageType. Além disso, mesmo que criássemos nossa própria subclasse de MessageRequest, ainda assim não poderíamos ignorar a verificação de validação de MessageType e Channel, já que ela é feita no construtor de MessageRequest. Além disso, as enums são implicitamente finais, portanto não podemos adicionar nossa própria MessageType ou Channel nem sobrescrever o comportamento de validação.
Além disso, os construtores de todas as subclasses de MessageRequest são protegidos (para classes abstratas) ou privados ao pacote (para classes concretas), e nenhuma das classes concretas pode ser estendida, já que são final (o mesmo vale para os construtores). Portanto, estruturalmente, não podemos criar uma classe “ruim” MessageRequest .
Tudo bem, vamos tentar outra coisa. Se não conseguirmos usar indevidamente os construtores de forma estrutural MessageRequest, talvez possamos passar valores inválidos para os construtores. Por exemplo, será que poderíamos enviar um texto vazio? Ou que tal passar uma string sem sentido no to campo (destinatário) em vez de um número? Ou e se omitirmos parâmetros obrigatórios (como o remetente e o destinatário)?
Ah, tem mais uma: todos os tipos de mensagem baseados em arquivo (aqueles que aceitam imagem, áudio, Video etc.) têm um campo de URL. Não seria possível passar uma string de URL inválida? Ou e aquele campo “Time-to-Live” nas mensagens do Viber? Poderíamos definir um número fora dos limites aceitáveis, já que é apenas um int, certo? Ah, aqui vai uma bem específica: nas solicitações do Messenger, tanto Category e Tag são opcionais. No entanto, a API exige que, se o Category tiver o valor message-tag, a tag deve estar presente. E quanto a...
Vamos parar por aqui. Todos esses casos já foram considerados, e nenhum deles é possível. Sim, seu código será compilado se você tentar passar uma URL malformada, um número inválido ou se esquecer de definir parâmetros obrigatórios no construtor. Mas, em tempo de execução, você não conseguirá enviar a mensagem. Você nem mesmo chegará ao ponto de construir o MessageRequest objeto! Por quê? Porque todos os parâmetros são validados no construtor. No momento em que você chama build() o Builder, o construtor é chamado e, se os parâmetros usados para construir a mensagem estiverem faltando ou forem inválidos, você receberá uma IllegalArgumentException excepção explicando o problema. É a isso que me referi quando disse: “correto por construção”. Se você consegue construir um MessageRequest, então, pelo que podemos perceber, ela não está obviamente errada, e você está livre para prosseguir com o envio.
É claro que isso não significa que a mensagem seja totalmente válida ou que você não receba uma resposta de erro do servidor. Você poderia, por exemplo, tentar enviar uma mensagem de texto para um número de telefone que não exista, mas que esteja em conformidade com a norma E164. Esse tipo de validação está fora do escopo do SDK e é realizada pelo serviço de back-end com o qual a API se comunica. O que o SDK protege contra são, essencialmente, os erros 422. Então, com o que isso contrasta? Aonde quero chegar com isso? Para responder a isso, vamos analisar o oposto diametral: o cURL.
Por que preciso do SDK se posso usar a API diretamente? Tudo bem, aqui está um exemplo de como enviar uma imagem pelo Viber usando o cURL:
Embora isso seja relativamente fácil de entender, é fácil cometer erros ao escrever manualmente uma solicitação desse tipo. Não só é preciso lembrar da URL do endpoint, do método HTTP, dos cabeçalhos, do tipo de autorização etc., mas também do formato JSON. Você pode esquecer as aspas (quase tudo é uma string), mas preste atenção nisso ttl — na verdade, é um inteiro, não um int envolvido em uma string! Você precisa se lembrar de todos os campos e de seus nomes exatos — nenhum IDE vai te ajudar com o autocompletar aqui, porque você está apenas digitando texto.
Seu terminal não tem conhecimento algum sobre a API; portanto, se você digitar algo incorretamente, não há como saber até enviar a solicitação e receber uma resposta 422. E, pensando bem, esse esquema não é muito intuitivo, não é mesmo? Se você quiser definir o tempo de validade, precisa se lembrar de colocá-lo dentro de um viber_service objeto. E não se esqueça de que a URL da imagem também precisa estar dentro de um image também; ah, e você não pode incluir uma legenda com sua imagem nas mensagens do Viber, mas o cURL não vai impedir que você tente. Você pode passar o que quiser como corpo da solicitação. Nem precisa ser um JSON válido, muito menos estar em conformidade com o esquema da API!
É justo dizer, então, que, para usar a Messages API com o cURL, você precisará ter a referência da API à mão e consultá-la frequentemente para garantir que suas solicitações estejam corretas — não apenas o corpo, mas também os cabeçalhos (o método de autorização, por exemplo). É um processo propenso a erros, e não há nada que impeça você de cometer erros ou que o ajude a formular a solicitação de mensagem pretendida.
Por outro lado, se você quiser usar a Messages API por meio do SDK do Java, não precisa da referência da API. Todos os métodos públicos de que você precisa estão documentados, explicando o que é obrigatório e o que é opcional, o padrão de uso, exemplos etc. Mesmo sem ler a documentação, você pode explorar a API usando nada mais do que o preenchimento automático fornecido pelo seu IDE. Por exemplo, você não tentará definir uma legenda em uma imagem do Viber, pois não há como fazer isso no SDK do Java. Assim, como usuário, você conclui que isso não é suportado.
E quanto à capacidade de descoberta? Bem, todas as MessageRequest subclasses seguem o [Channel][MessageType]Request esquema de nomenclatura (por exemplo, SmsTextRequest). Você pode até usar seu IDE para listar todas as subclasses de MessageRequest. E há apenas uma maneira de construir solicitações: por meio do construtor associado a cada MessageRequest subclasse. Além disso, os construtores dos construtores não são públicos; portanto, a única maneira de instanciar um construtor é chamando o método estático builder() da classe. Isso, combinado com os construtores privados ao pacote, impede que você os utilize incorretamente.
Uma linguagem fortemente tipada como o Java impede que você forneça valores inválidos, não apenas em tempo de execução, mas também em tempo de compilação. Por exemplo, talvez você não saiba quais são os valores válidos para Category no objeto opcional viber_service , mas no SDK do Java, eles são claramente indicados pelo compilador ou pelo seu IDE, pois trata-se de uma enumeração.
E essa história de encapsulá-lo em um viber_service objeto? Você não precisa se preocupar com isso; basta definir o que for necessário no construtor, e o SDK cuida disso para você. Na verdade, o SDK cuida de toda a serialização e desserialização. Como usuário, você não sabe nem precisa se preocupar com qual formato de serialização está sendo usado nos bastidores — tudo já está resolvido. Essa é uma das principais vantagens de usar um SDK em vez da API: é declarativo em vez de imperativo. Você declara o que você quer enviar, e não como enviá-lo.
Veja por este ângulo: suponha que tenhamos decidido que todas as nossas APIs passarão a usar Avro ou Protobuf (ou, Deus me livre, o velho e bom XML) em vez de JSON. Esses scripts do cURL que você tinha guardados vão precisar ser reescritos. Talvez, se você tiver sorte e a gente mantiver o esquema exatamente o mesmo, e você por acaso for um gênio em RegEx, consiga pelo menos automatizar parcialmente a migração.
Mas e se mudássemos o esquema? Seguindo o exemplo acima, e se decidíssemos que o url parâmetro não precisasse mais estar dentro de um objeto? Ou se removêssemos o viber_service contêiner, que é usado para ttl e category parâmetros? Ou e se renomeássemos o viber_service tipo de mensagem para viber para manter a consistência? Se você estiver criando suas solicitações manualmente com o cURL, precisa estar ciente de todas essas mudanças. Se estiver usando o SDK do Java, basta atualizar para a próxima versão — basta alterar um único caractere no seu pom.xml ou build.gradle arquivo. Não há necessidade de refatorar nada — tudo é resolvido nos bastidores.
Embora eu tenha me concentrado no SDK do Java, vale ressaltar que a tipagem forte não é um pré-requisito para a validação. Ela apenas facilita a aplicação dessa validação por meio do compilador, em vez de uma lógica codificada manualmente.
Por exemplo, a implementação da Messages API no SDK do Python é minúscula em tamanho quando comparada ao SDK do Java (é um único arquivo com menos de 100 linhas!), e a do SDK do Ruby também é relativamente pequena. Ambos os SDKs realizam algumas validações básicas nos parâmetros.
A mesma abordagem poderia ter sido usada no SDK do Java: poderíamos aceitar uma estrutura `Map` com os parâmetros e validá-los dinamicamente, mas isso é mais propenso a erros, já que o compilador não pode nos ajudar. O tamanho da implementação do SDK do Java se deve, em grande parte, ao fato de o Java ser uma linguagem prolixa; — por exemplo, é semelhante à implementação em C# em princípio, mas possui significativamente mais linhas de código.
Pode ser um exercício interessante para o leitor comparar os Pull Requests iniciais da implementação do Messages v1 em todo o Java, Python, Ruby e PHP para avaliar isso.
No entanto, essa abordagem rigorosa no projeto do SDK tem suas desvantagens. Suponhamos que, em algum momento no futuro, a API passe a suportar o envio de vídeo pelo Viber. Você poderá aproveitar essa funcionalidade imediatamente com o cURL, mas um SDK que valide o tipo de mensagem e as combinações de canais precisará ser atualizado.
Além disso, quando a API sofre alterações frequentes, isso representa uma carga maior de manutenção para os responsáveis pelo SDK, pois a lógica de validação, os tipos de dados etc. precisam ser atualizados para refletir essas mudanças. Mas, assim que tudo se estabiliza e a API fica estável (o que geralmente ocorre quando nossas APIs passam da fase Beta para a versão GA), isso deixa de ser um problema tão significativo. Em última análise, nosso objetivo é oferecer a melhor experiência possível ao usuário, e espero que este artigo tenha convencido você do valor que os SDKs personalizados podem agregar às APIs REST.
Sempre incentivamos a participação da comunidade. Fique à vontade para se juntar a nós no Slack da Comunidade Vonage ou nos enviar uma mensagem no Twitter. Se você tiver alguma sugestão de melhorias, aprimoramentos ou se encontrar um bug, não hesite em abrir um ticket no GitHub.
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.