https://d226lax1qjow5r.cloudfront.net/blog/blogposts/boost-your-productivity-with-model-driven-engineering-part-2/boost-productivity_model-driven-engineering_p2.png

Aumente su productividad con la ingeniería basada en modelos (2ª parte)

Publicado el January 23, 2024

Tiempo de lectura: 8 minutos

Introducción

Bienvenido de nuevo a nuestra aventura para aumentar la productividad. En Parte 1cubrimos los conceptos principales de la ingeniería basada en modelos (MDE) y presentamos brevemente algunas "herramientas del oficio". En este artículo, presentaré un estudio de caso, específicamente, cómo utilicé estas tecnologías para ahorrar mucho tiempo al agregar soporte para nuevas API al Vonage Java SDK.

Puesta en escena: Planteamiento del problema

Tenemos muchas API en Vonage. Algunas son bastante pequeñas y simples, pero otras son enormes. Por ejemplo, nuestras API más grandes como Video, Reuniones y Conexión proactiva. Compárelos con una API más pequeña, como Number Insight v2 - ¡basta con ver la diferencia de tamaño en las barras de desplazamiento! Esas API más grandes no sólo tienen muchos puntos finales, sino también modelos de datos grandes y complejos para solicitudes y respuestas.

Una vez que una API se considera estable (estado de "Disponibilidad general"), nuestro objetivo es añadir compatibilidad con ella en nuestros SDK oficiales. Ya he escrito sobre el valor añadido de los SDKsy mi colega Jim Seconde también ha hablado de elloasí que no voy a repetir las ventajas de ofrecer SDK para API. Ni que decir tiene que un SDK de alta calidad requiere importantes recursos para su desarrollo y mantenimiento: ¡probablemente sea alrededor del 80% de mi trabajo! Sin embargo, gran parte del esfuerzo que supone añadir nuevas API a nuestros SDK es bastante laborioso y requiere muy poca reflexión. Aunque escribir boilerplate puede ser algo terapéutico, no cabe duda de que no es el mejor uso del tiempo de un desarrollador, porque esa tarea puede automatizarse.

Requisitos de implementación del SDK

Entonces, ¿qué hace un SDK fuertemente tipado, como el de Java o .NET requiere? Bueno, para empezar, (normalmente) cada endpoint necesita ser soportado, así que tener la lógica para la URL correcta, el método de petición HTTP y el tipo de autenticación. Generar un token web JSONpor ejemplo, y aplicarlo a la carga útil de la solicitud con la configuración correcta, así como otros metadatos como Content-Type y Accept cabeceras. Luego están los cuerpos de las peticiones. A veces, la carga útil forma parte de los parámetros de la consulta, como cuando se filtran los resultados de una búsqueda en una solicitud. GET solicitud. Otras veces, forma parte del cuerpo y debe serializarse como JSON. El SDK también debe gestionar las respuestas: tanto las correctas (códigos de estado HTTP 2xx) como las incorrectas (códigos 4xx y 5xx). Los puntos finales que devuelven un cuerpo de respuesta deben analizarse a partir de JSON. Por lo tanto, el SDK debe ser capaz de serializar y deserializar cargas JSON en un objeto. Aunque bibliotecas como Jackson hacen que este proceso sea declarativo, todavía tenemos que definir las clases y los campos manualmente. Hay otras cosas, como la validación (para evitar respuestas 422) y la documentación, pero espero que te hagas una idea. En definitiva, se trata de hacer la vida lo más fácil posible a los usuarios de la API cuando la utilizan desde un lenguaje de programación.

Solución MDE

Una vez introducidos el dominio del problema y los conceptos de la ingeniería basada en modelos, podemos empezar a integrarlos. Hay varias maneras de abordar este tema, en función del alcance y el tiempo disponible, y no hay una manera definitiva "correcta" de hacerlo. Por lo tanto, describiré el enfoque pragmático que adopté. Como el objetivo es maximizar la productividad, no dediqué mucho tiempo a la "sobreingeniería" o a la arquitectura de la solución por adelantado. Con MDE, esto puede funcionar, pero también tiene sus inconvenientes. Volveré a hablar de ello al final. Si desea seguir a lo largo, he hecho esto código abierto en GitHub.

Metamodelo

Como es el caso con cualquier enfoque de ingeniería basada en modelos, el primer lugar para empezar es el metamodelo. Así que aquí está, con la sintaxis textual de Emfatic a la izquierda y la vista de árbol a la derecha. Nótese que decidí crear el metamodelo usando el editor integrado de Ecore en lugar de Emfatic o una herramienta alternativa de (meta)modelado.

API metamodel

Si ha consultado alguna de las especificaciones OpenAPI mencionadas anteriormente, la estructura del metamodelo debería explicarse por sí misma. La raíz de la jerarquía es la clase Api que tiene una clase name, package (es decir, dónde estarán las clases en el SDK), la ruta del punto final base (por ejemplo, https://api-eu.vonage.com/v1/meetings para Meetings API) y, por supuesto, los puntos finales reales. Dado que todos los objetos deben estar contenidos en el elemento raíz, types son referenciados aquí aunque no sean utilizados directamente por la clase Api clase.

La clase Endpoint es la siguiente en la jerarquía. Tiene lo que cabría esperar: nombre, URL (ruta), el método de solicitud HTTP (representado como un enum), uno o más métodos de autenticación (de nuevo, representado como un enum, ya que hay 3 tipos) y, por supuesto, los tipos de solicitud y respuesta.

¿Qué es un tipo? Bueno, puede ser un tipo incorporado al que queramos hacer referencia, es decir, un tipo ya definido en el SDK, la biblioteca estándar, etc. - Básicamente, cualquier cosa que no queramos modelar. Así que un Type tiene un atributo name que podemos utilizar en estos casos (p. ej. String, UUID, Integer, URI etc.). Los tipos que sí queremos modelar son Class que extiende a Type (heredando así el atributo name ). Esto es algo así como un ejercicio de modelado parcial de una clase Java, sólo que mucho más específico para nuestras necesidades. Como se puede ver en el resto de atributos y tipos descritos en el metamodelo, hay algunos campos muy peculiares y omisiones notables. Por ejemplo, una clase Java tiene métodos y constructores, pero éstos no se incluyen aquí. ¿Por qué? Porque no los necesitamos. También he decidido modelar la documentación de Class y Field utilizando el tipo Documentation pero, una vez más, está incompleta en relación con las capacidades de Javadoc. Modelar la totalidad de Java está mucho más allá de nuestras necesidades. Para satisfacer tu curiosidad, aquí hay un metamodelo de Java 7 - ¡y eso sin todas las características de lujo de los últimos JDKs!

Generador de códigos

Ahora viene la parte que estabas esperando: ¡la generación de código! Como se mencionó en la Parte 1, hay varias herramientas de Modelo-a-Texto disponibles, pero he optado por utilizar Lenguaje de generación de Epsilon (EGL). Esto se debe principalmente a la familiaridad, y EGL no es particularmente especial. Es un lenguaje basado en plantillas, en el que una plantilla (.egl file) tiene estática y dinámicas estáticas y dinámicas. Las regiones estáticas son las predeterminadas: el texto introducido en el fichero aparecerá textualmente en la salida. En cambio, las regiones dinámicas permiten determinar mediante programación la salida, que puede (y suele) depender de alguna propiedad del modelo.

Por ejemplo, en exception.eglutilizo sólo una propiedad name para variar la salida. La plantilla request_response.egl plantilla es más compleja, utilizando for para declarar todos los campos de la clase y depende de funciones de ayuda que he definido en helper_functions.egl. Dado que EGL se construye sobre EOL, las regiones dinámicas pueden contener cualquier código EOL, incluidas las operaciones. EGL también tiene "operaciones de plantilla" (anotadas con @template), que son funciones que producen texto (o devuelven el texto como una cadena) cuando se las llama. Así, podemos construir nuestra biblioteca de funciones de utilidad y reutilizarlas a través de plantillas.

Quizá se pregunte: ¿de dónde proceden estas variables y cómo se invocan las plantillas? Al fin y al cabo, para que una plantilla sea útil, tiene que estar parametrizada con valores del modelo. ¿Y dónde se escribe la salida? Esta es la principal ventaja de utilizar Epsilon: su lenguaje de coordinación EGX. La idea de EGX es proporcionar un lenguaje basado en reglas que controle cuándo y cómo se invocan las plantillas, los parámetros con los que se invocan y a dónde se dirige la salida. El archivo specs.egx archivo define esta lógica: es lo que une el modelo y las plantillas. La sección pre se ejecuta en primer lugar y contiene principalmente declaraciones de variables que se utilizarán en el script, como el directorio de salida y los nombres comunes. También recupera del modelo varios tipos y los categoriza por propiedades, como si son peticiones o respuestas (definimos esto como una propiedad booleana de Class en nuestro metamodelo). Las operaciones declaradas sobre el tipo Class en este script se utilizan para derivar las variables que se pasarán a cada plantilla a partir de las propiedades del elemento del modelo.

Para que esto quede más claro, veamos un ejemplo. Tomemos la regla regla QueryParamsRequest. Piense en las etiquetas transform y in son como un bucle for mejorado. La colección de entrada,, en este caso,, proviene de la función queryParamsRequestTypes calculada en pre. En otros casos (por ejemplo, la regla Enum regla) procede de todas las instancias de un tipo determinado en el modelo. Para cada elemento del modelo (en nuestro ejemplo, vinculado a la variable request), los parameters se obtienen de un método de utilidad. Esto vincula los nombres de las variables que se utilizarán en el modelo a las propiedades de request - Nótese que las operaciones utilizadas self ya que la operación se declara en el tipo Classque tiene los atributos que queremos utilizar. El template es una ruta relativa a la plantilla EGL que queremos invocar para esta regla. Por último, el target es el archivo en el que queremos escribir los resultados. Por defecto, EGX sobrescribe el archivo si existe, pero esto es configurable.

Espero que esto empiece a tener sentido. Puedes ver cómo, con EGX, podemos elegir qué plantillas invocar basándonos en los tipos de elementos del modelo y cómo las variables de esas plantillas se derivan del modelo. Esta lógica de coordinación es realmente la pieza central del enfoque. Para ejecutarla, basta con introducir en la plantilla el modelo o modelos que deseamos y obtener el resultado.

Eso es todo por ahora...

En este artículo se ha explicado brevemente el enfoque MDE adoptado para ayudar en el caso concreto de la generación de boilerplate para el SDK de Java. Aunque espero que el valor que aporta sea evidente, puede que tengas más curiosidad por el proceso de desarrollo y las lecciones aprendidas de este ejercicio. En la tercera y última parte de esta serie, reflexionaré sobre el enfoque y proporcionaré algunos puntos clave a tener en cuenta si decides seguir esta ruta para tus proyectos.

Si tiene algún comentario o sugerencia, no dude en ponerse en contacto con nosotros en X, antes conocido como Twitter o pásate por nuestro Slack de la comunidad. Espero que este artículo haya sido útil y agradezco cualquier opinión. Si te ha gustado, echa un vistazo a mis otros artículos sobre artículos sobre Java.

Compartir:

https://a.storyblok.com/f/270183/400x400/46a3751f47/sina-madani.png
Sina MadaniVonage Antiguo miembro del equipo

Sina es promotora de desarrollo Java en Vonage. Procede del mundo académico y, en general, siente curiosidad por todo lo relacionado con los coches, los ordenadores, la programación, la tecnología y la naturaleza humana. En su tiempo libre, se le puede encontrar paseando o jugando a videojuegos de competición.