
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.
Aumente sua produtividade com a engenharia orientada a modelos (Parte 2)
Introdução
Bem-vindos de volta à nossa aventura para aumentar a produtividade! Na Parte 1, abordamos os principais conceitos da engenharia orientada a modelos (MDE) e apresentamos brevemente algumas “ferramentas do ofício”. Neste artigo, apresentarei um estudo de caso — especificamente, como utilizei essas tecnologias para economizar muito tempo ao adicionar suporte a novas APIs ao SDK Java da Vonage.
Contexto: Enunciado do problema
Temos muitas APIs na Vonage. Algumas delas são bem pequenas e simples, mas outras são enormes. Veja, por exemplo, nossas APIs maiores, como Video, Reuniões e Proactive Connect. Compare-os com uma API menor, como Number Insight v2 — basta observar a diferença de tamanho nas barras de rolagem! Essas APIs maiores não só possuem muitos endpoints, mas também modelos de dados grandes e complexos para solicitações e respostas.
Assim que uma API for considerada estável (status de “Disponibilidade Geral”), nosso objetivo é adicionar suporte a ela em nossos SDKs oficiais. Já escrevi sobre o valor que os SDKs agregam, e meu colega Jim Seconde também falado sobre isso, então não vou repetir os benefícios de oferecer SDKs para APIs. Nem preciso dizer que um SDK de alta qualidade exige recursos significativos para ser desenvolvido e mantido — provavelmente representa cerca de 80% do meu trabalho! No entanto, grande parte do esforço envolvido na adição de novas APIs aos nossos SDKs é bastante trabalhosa e exige muito pouco raciocínio. Embora escrever código padrão possa ser um tanto terapêutico, sem dúvida não é o melhor uso do tempo de um desenvolvedor, pois essa tarefa pode ser automatizada.
Requisitos de implementação do SDK
Então, o que é um SDK fortemente tipado, como o Java ou .NET exige? Bem, para começar, (normalmente) todos os endpoints precisam ser suportados, o que implica ter a lógica para a URL correta, o método de solicitação HTTP e o tipo de autenticação. Gerar um JSON Web Token, por exemplo, e aplicá-lo à carga da solicitação com as configurações corretas, bem como outros metadados, como Content-Type e Accept cabeçalhos. Depois, há os corpos das solicitações propriamente ditos. Às vezes, a carga faz parte dos parâmetros de consulta, como ao filtrar resultados de busca em uma GET solicitação. Outras vezes, ele faz parte do corpo da solicitação e precisa ser serializado como JSON. O SDK também precisa lidar com respostas: tanto as bem-sucedidas (códigos de status HTTP 2xx) quanto as malsucedidas (códigos 4xx e 5xx). Os endpoints que retornam um corpo de resposta precisam ser analisados a partir de JSON. Assim, o SDK precisa ser capaz de serializar e desserializar cargas de JSON em um objeto. Embora bibliotecas como Jackson tornam esse processo declarativo, ainda precisamos definir as classes e os campos manualmente. Há outros aspectos, como validação (para evitar respostas 422) e documentação, mas espero que você tenha entendido a ideia. Em última análise, trata-se de facilitar ao máximo a vida dos usuários da API ao utilizá-la a partir de uma linguagem de programação.
Solução MDE
Agora que já apresentamos o domínio do problema e os Concepts de engenharia orientada a modelos, podemos começar a pensar em como integrar tudo isso. Existem várias maneiras de abordar isso, dependendo do escopo e do tempo disponível, e não há uma maneira “certa” definitiva de fazê-lo. Portanto, descreverei a abordagem pragmática que adotei. Como o objetivo é maximizar a produtividade, não gastei muito tempo com “engenharia excessiva” nem arquitetando a solução antecipadamente. Com a MDE, isso pode funcionar, mas também tem suas desvantagens. Voltarei a discutir isso no final. Se você quiser acompanhar, disponibilizei este projeto código aberto no GitHub.
Metamodelo
Como acontece com qualquer abordagem de engenharia orientada a modelos, o primeiro passo é o metamodelo. Então, aqui está ele, com a sintaxe textual do Emfatic à esquerda e a visualização em árvore à direita. Observe que decidi criar o metamodelo usando o editor Ecore integrado, em vez do Emfatic ou de uma ferramenta alternativa de (meta)modelagem.

Se você já consultou alguma das especificações da OpenAPI mencionadas anteriormente, a estrutura do metamodelo deve, esperamos, ser relativamente autoexplicativa. A raiz da hierarquia é a Api classe, que possui um name, package (ou seja, onde as classes estarão no SDK), o caminho do endpoint base (por exemplo, https://api-eu.vonage.com/v1/meetings para a Meetings API) e, é claro, os endpoints propriamente ditos. Como todos os objetos precisam estar contidos no elemento raiz, types eles são referenciados aqui, mesmo que não sejam usados diretamente pela Api classe.
A Endpoint classe é a seguinte na hierarquia. Ela possui o que você esperaria: nome, URL (caminho), o método de solicitação HTTP (representado como uma enumeração), um ou mais métodos de autenticação (novamente, representados como uma enumeração, já que existem três tipos) e, é claro, os tipos de solicitação e resposta.
Então, o que é um tipo? Bem, pode ser um tipo embutido ao qual queremos fazer referência — ou seja, um tipo já definido no SDK, na biblioteca padrão etc. — basicamente, qualquer coisa que não queiramos modelar. Portanto, um Type simplesmente tem um name atributo que podemos usar nesses casos (por exemplo, String, UUID, Integer, URI e assim por diante). Os tipos que queremos modelar são um Class que estende Type (herdando, assim, o name atributo). Isso é, de certa forma, um exercício de modelagem parcial de uma classe Java, só que muito mais específico para nossas necessidades. Como você pode ver pelos atributos e tipos restantes descritos no metamodelo, há alguns campos bem peculiares e omissões notáveis. Por exemplo, uma classe Java tem métodos e construtores, mas esses não estão incluídos aqui. Por quê? Porque não precisamos deles. Também decidi modelar a documentação para Class e Field usando o Documentation tipo, mas, novamente, ela está incompleta em relação aos recursos do Javadoc. Modelar o Java na íntegra está muito além das nossas necessidades. Para satisfazer sua curiosidade, aqui está um metamodelo do Java 7 — e isso sem todos os recursos sofisticados dos JDKs mais recentes!
Gerador de código
Agora, a parte que vocês estavam esperando: geração de código! Como mencionei na Parte 1, existem várias ferramentas de “Model-to-Text” disponíveis, mas optei por usar a Generation Language (EGL) da Epsilon. Isso se deve principalmente à familiaridade, e a EGL não é particularmente especial. Trata-se de uma linguagem baseada em modelos, na qual um modelo (.egl ) contém estático e dinâmicas . As regiões estáticas são o padrão: o texto inserido no arquivo aparecerá literalmente na saída. Por outro lado, as regiões dinâmicas permitem que você determine a saída programaticamente, o que pode (e geralmente vai) depender de alguma propriedade do modelo.
Por exemplo, em exception.egl, estou usando apenas uma propriedade: name para variar o resultado. O request_response.egl modelo é mais complexo, utilizando for loops para declarar todos os campos da classe e contando com funções auxiliares que defini em helper_functions.egl. Como o EGL é construído sobre o EOL, as regiões dinâmicas podem conter qualquer código EOL, incluindo operações. O EGL também possui “operações de modelo” (anotadas com @template), que são funções que geram texto (ou retornam o texto como uma string) quando chamadas. Assim, podemos construir nossa biblioteca de funções utilitárias e reutilizá-las em vários modelos.
Você deve estar se perguntando: de onde vêm essas variáveis e como os modelos são chamados? Afinal, para que um modelo seja útil, ele precisa ser parametrizado com valores do modelo. E para onde a saída é gravada? É aí que entra a principal vantagem de usar o Epsilon: sua linguagem de coordenação EGX. A ideia do EGX é fornecer uma linguagem baseada em regras que controla quando e como os modelos são chamados, os parâmetros com os quais são chamados e para onde a saída é direcionada. O specs.egx arquivo define essa lógica — é ele que une o modelo e os modelos. A pre seção é executada primeiro e contém principalmente declarações de variáveis que serão usadas no script, como o diretório de saída e nomes comuns. Ela também recupera do modelo vários tipos e os categoriza por propriedades, como se são solicitações ou respostas (definimos isso como uma propriedade booleana de Class em nosso metamodelo por esse motivo). As operações declaradas no tipo Class neste script são usadas para derivar, a partir das propriedades do elemento do modelo, as variáveis que serão passadas para cada modelo de saída.
Para deixar isso mais claro, vamos dar um exemplo. Considere a regra QueryParamsRequest. Pense no transform e in como se fossem um for . A coleção de entrada, neste caso, provém do queryParamsRequestTypes calculada em pre. Em outros casos (por exemplo, a Enum regra), ela provém de todas as instâncias de um determinado tipo no modelo. Para cada elemento do modelo (em nosso exemplo, vinculado à variável request), o parameters são obtidos a partir de um método utilitário. Isso vincula os nomes das variáveis a serem usados no modelo às propriedades de request — observe que as operações utilizadas self , já que a operação está declarada no tipo Class, que possui os atributos que pretendemos usar. O template é um caminho relativo para o modelo EGL que queremos invocar para esta regra. Por fim, o target é o arquivo no qual queremos gravar os resultados. Por padrão, o EGX sobrescreve o arquivo se ele existir, mas isso é configurável.
Espero que isso esteja começando a fazer sentido! Você pode ver como, com o EGX, podemos escolher quais modelos invocar com base nos tipos de elementos do modelo e como as variáveis desses modelos são derivadas do modelo. Essa lógica de coordenação é, na verdade, o ponto central dessa abordagem. Para executá-la, basta fornecer ao modelo o(s) modelo(s) que desejamos e obter o resultado.
Por enquanto é só isso...
Este artigo explicou resumidamente a abordagem MDE adotada para auxiliar no caso específico da geração de código padrão para o SDK do Java. Embora eu espere que o valor agregado seja evidente, talvez você esteja mais curioso sobre o processo de desenvolvimento e as lições aprendidas com esse exercício. Na terceira e última parte desta série, vou refletir sobre a abordagem e apresentar algumas lições importantes a serem lembradas caso você decida seguir esse caminho em seus projetos.
Se você tiver algum comentário ou sugestão, sinta-se à vontade para entrar em contato conosco no X, anteriormente conhecido como Twitter ou dê uma passada no nosso Slack da Comunidade. Espero que este artigo tenha sido útil e agradeço quaisquer comentários ou opiniões. Se você gostou, confira meus outros artigos sobre Java.
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.