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

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

Publicado el January 31, 2024

Tiempo de lectura: 14 minutos

Introducción

Bienvenidos al final de esta serie sobre ingeniería basada en modelos. En Parte 2describí cómo utilicé estas tecnologías para ahorrar mucho tiempo al agregar soporte para nuevas API al Vonage Java SDK. En este artículo, destacaré algunos principios clave basados en las lecciones aprendidas para que puedas aprovechar al máximo las tecnologías y el enfoque.

Conozca su "por qué

Lo primero que hay que tener en cuenta antes de ir más lejos es asegurarse de que se adapta bien a su caso de uso. Como dice el refrán, "cuando todo lo que tienes es un martillo, todo parece un clavo". Es normal que las tecnologías pasen por modas pasajeras. "ciclo de la exageración de Gartner".. Aunque ciertas tecnologías y metodologías de desarrollo parecen apetecibles, al fin y al cabo son herramientas. Tener una caja de herramientas variada es estupendo porque nos permite elegir la herramienta o el enfoque adecuados para cada trabajo. Lo que es adecuado para un caso de uso puede no serlo para otro. Entonces, ¿cuándo es el MDE la elección correcta y cuándo no? He aquí algunas cuestiones a tener en cuenta.

¿Es fácil modelar el dominio?

No todos los retos encajan perfectamente en un metamodelo fácilmente definible. Aunque, en muchos casos, es posible adoptar una visión orientada a objetos de un dominio con relaciones predecibles entre los conceptos del mismo, puede haber ocasiones en las que la naturaleza de estos conceptos e incluso la forma en que se relacionan entre sí sea demasiado dinámica. Para sacar el máximo partido de un enfoque basado en modelos, el metamodelo del dominio debe ser, como mínimo, definible con un alto grado de certeza. Los Concepts deben corresponder a clases con atributos y relaciones definibles. Si hay demasiados elementos de dominio que definir y las relaciones son demasiado complejas, el metamodelo lo reflejará. En tales casos, definir el metamodelo y los modelos será una tarea demasiado ardua y puede compensar los beneficios en comparación con implementarlo directamente.

¿Qué complejidad tendrá el generador?

Si su dominio puede definirse claramente mediante un metamodelo, también debe considerar cómo se relacionan estos conceptos con el código y la lógica empresarial que desea generar. Hay argumentos a favor de ambos extremos: si la lógica es densa (es decir, depende en gran medida del modelo), la autogeneración reduce la posibilidad de errores. Por otro lado, puede dificultar el mantenimiento y el trabajo con el generador. También conviene preguntarse cuántas plantillas habrá que desarrollar y mantener, así como la complejidad global de la lógica de coordinación para invocar esas plantillas, sobre todo si se compara con el código escrito a mano.

¿Cuánta codificación manual ahorrará?

Una de las cuestiones más importantes, y quizá también la más directa, es la proporción de código repetitivo con respecto a la lógica. Un enfoque de generación de código basado en plantillas y basado en modelos funciona muy bien cuando el código generado es en su mayor parte repetitivo, de modo que el resultado se puede pegar en la base de código con modificaciones mínimas o incluso sin ninguna modificación. Sin embargo, si el código generado requiere una codificación manual adicional significativa, especialmente si se trata de modificar el código generado en lugar de, por ejemplo, añadir nuevos métodos, el valor del generador disminuye. Esto se debe a que la sobrecarga de desarrollar y mantener el generador, así como refactorizar o modificar a mano el código generado, puede ser una carga para algunos desarrolladores en comparación con escribir estas secciones del código desde cero.

¿Con qué frecuencia lo utilizará?

Incluso si un enfoque basado en modelos es factible, es decir, si tu dominio encaja perfectamente en un metamodelo y la generación de código es beneficiosa, puede que no merezca la pena el esfuerzo si sólo piensas utilizarlo una vez. Por supuesto, esto depende de otros factores, como la cantidad de código que se genere: si se trata de un proyecto grande, puede merecer la pena la inversión, aunque sea una sola vez. En otros casos, el "ahorro" puede ser modesto, pero se acumula con el tiempo gracias al uso frecuente. Otra forma de plantearse esta cuestión es: "¿Cuántos modelos vamos a tener?". Lo más probable es que cuantos más modelos cree (y con qué frecuencia los cambie), más uso le dará al generador.

¿Es fácil actualizar los modelos?

En el punto anterior, mencioné cómo tener más modelos o modelos que cambian con frecuencia le permite aprovechar mejor los beneficios de utilizar un enfoque de generación de código. Sin embargo, si tus modelos se construyen únicamente para este ejercicio y deben actualizarse constantemente para reflejar los cambios en los requisitos empresariales o en el entorno, esto puede añadir fricción. En mi caso, por ejemplo, cualquier cambio en la especificación de la API obliga a actualizar el modelo. La carga de actualizar el modelo es directamente proporcional al alcance de los cambios en la especificación. Si los modelos tienen que evolucionar rápidamente, el código generado anteriormente ya no será válido y habrá que volver a generarlo. A esto hay que añadir el hecho de que parte del código generado puede haber sido (o tener que ser) modificado a mano, como se ha comentado en un punto anterior.

¿El metamodelo tendrá que cambiar con frecuencia?

Dado que los modelos son datos estructurados, es razonable esperar que cambien, a veces incluso rápidamente. Si las actualizaciones del modelo están automatizadas, no hay ningún problema y la fricción es relativamente baja. Sin embargo, lo que no debería cambiar con frecuencia es el esquema (metamodelo). Esto es algo similar al primer punto: si los Concepts de su dominio cambian con frecuencia y son difíciles de fijar en un metamodelo estable, entonces será mucho más difícil seguir un enfoque basado en modelos. Lo ideal es que el metamodelo se revise durante la fase de creación de prototipos y rara vez una vez estable y "en producción" (es decir, que el resultado sea utilizable). Añadir campos al metamodelo no suele suponer un cambio radical, ya que pueden ser opcionales, pero eliminar clases, relaciones y atributos puede (y suele) romper los modelos y el flujo de trabajo existentes, lo que significa que en algunos casos hay que volver a empezar.

Además de la corrección de errores, ¿con qué frecuencia cambiará el generador?

Durante el desarrollo inicial de las plantillas del generador de código y la lógica de coordinación, es natural que haya muchos errores u omisiones. Una vez que la mayoría se han solucionado y la salida generada es la deseada, el generador puede considerarse estable. Sin embargo, incluso después de esto, inevitablemente habrá cambios debidos a nuevos requisitos empresariales, guías de estilo o estructura del código. Sea cual sea el motivo, hay que tener en cuenta que cualquier cambio en un generador estable puede introducir nuevos errores que habrá que corregir. Dado que, por lo general, éstos sólo pueden detectarse al inspeccionar el código de salida, lo ideal sería que el generador no tuviera que someterse a cambios frecuentes. Esto se debe a que el generador es (o puede ser) más difícil de probar que el propio código generado, por lo que los cambios rápidos en los requisitos empresariales que deben reflejarse en la lógica de las plantillas introducen una fricción adicional.

¿Está su equipo de acuerdo?

Por último, aunque un enfoque basado en modelos parezca perfecto para su caso de uso, es poco probable que tenga éxito si su jefe y sus colegas no están dispuestos a adaptarse a este flujo de trabajo. Al fin y al cabo, crear el metamodelo, el generador y los modelos, así como mantenerlos, es una carga adicional que añade complejidad y requiere que la gente entienda las tecnologías utilizadas. Los factores organizativos también influyen: de dónde proceden los modelos, quién y cómo los creará o actualizará, quién es responsable del generador, qué tipo de proceso se utilizará para extraer los resultados generados y añadirlos al código base existente, etc. Este enfoque plantea muchos retos logísticos, por no hablar de la conformidad y la auditoría. Si se utiliza este método para ayudar a generar código repetitivo, pero todos los que lo utilicen se comprometen a inspeccionar y revisar manualmente el código generado antes de incorporarlo a la base de código principal, las fricciones deberían ser relativamente escasas. Por otro lado, si se trata de una parte integral y obligatoria del proceso de desarrollo con automatización, se requiere una reflexión más profunda y la debida diligencia. En cualquier caso, las personas que utilicen los modelos, el generador o el producto resultante deben conocer y aceptar el enfoque.

Desarrollo iterativo

Supongamos que ha reflexionado sobre todas las preguntas anteriores y ha determinado que el enfoque es adecuado para su caso de uso. ¿Cómo debe proceder para construirlo? Basándome en lo que he presentado hasta ahora, puede que tenga la impresión de que el desarrollo basado en modelos es un proceso "en cascada". Si bien es cierto que el metamodelo debe ser estable y es naturalmente el punto de partida, en la práctica hay más flexibilidad de lo que la literatura y las herramientas quieren hacer creer.

En mi caso, ciertamente no empecé con el metamodelo perfecto, e incluso ahora, todavía tiene un margen sustancial de mejora. Por ejemplo, una cosa que descubrí al crear modelos es que tengo que crear tipos Java incorporados para poder referenciarlos, pero éstos podrían formar parte del metamodelo. Muchos de los atributos del modelo, en particular los booleanos como isRequest, isResponse, isHal, isQueryParams etc. surgieron cuando me di cuenta de que el generador los necesitaría. Los cambios aditivos no rompen: normalmente puedes ampliar tu metamodelo con nuevos tipos y atributos siempre que sean opcionales o tengan un valor por defecto razonable.

El desarrollo basado en modelos sigue siendo desarrollo de software, por lo que un proceso iterativo permite obtener información más rápidamente en lugar de invertir demasiado al principio, cuando se dispone de menos información. Por lo tanto, en la práctica, recomiendo que desarrolle el metamodelo, el generador y un modelo de muestra en paralelo para que pueda ver el flujo de trabajo de extremo a extremo y detectar cualquier parámetro que falte en su metamodelo, así como los errores en el texto generado desde el principio en lugar de después de haber diseñado lo que sobre el papel parece estar "completo".

¿"One-Shot" o MDE "puro"?

El caso de uso que presenté en la Parte 2 era en gran medida un enfoque "único". Es decir, una vez generado el código, ya no me sirven ni el generador ni el modelo. Por ejemplo, si se actualiza la especificación de la API o detecto algunos errores en el código de salida, a menos que haya problemas o revisiones importantes, lo más probable es que no vaya a actualizar el modelo y regenerar el código porque ya he refactorizado y evolucionado el código generado. Al fin y al cabo, el resultado es lo que me importa y lo que se mantendrá. En ese sentido, no estaba practicando realmente MDE como "se supone" que debe hacerse. El código generado es un punto de partida sobre el que construir, no el producto final. Por lo tanto, algunos pueden argumentar que no está dirigido por modelos en espíritu. Pero eso no significa que no sea útil.

En un enfoque tradicional basado en modelos, lo ideal sería que el código se regenerara a partir del modelo cada vez que éste se actualizara, de modo que la base de código y el modelo estuvieran siempre sincronizados. Piénsalo de esta manera: cuando escribes un programa en un lenguaje JVM (Java, Kotlin, Groovy, Scala, etc.), ¿qué envías a tu control de código fuente? ¿Qué mantienes? ¿De qué te preocupas? Es el código fuente. Pero ese código fuente no es lo que importa en tiempo de ejecución; es el bytecode (es decir, los .class archivos). ¿Por qué nos preocupamos tanto por nuestro código cuando, de todos modos, todo se compila en bytecode Java? Porque el código de bytes deriva de forma determinista del código fuente. Se podría argumentar que la principal baza de estos lenguajes es el compilador. Bueno, un compilador es esencialmente un transformador: tu código fuente es el modelo (conforme al metamodelo del lenguaje), el compilador es el generador, y el código de bytes es la salida del generador (He escrito anteriormente sobre esto si tienes curiosidad). De forma similar, el código generado en "MDE puro" debería ser tratado de la misma forma que el código compilado: necesario pero no algo que necesitemos mirar o que nos importe mucho. Las plantillas del generador son esencialmente el "código fuente" en MDE.

Es importante reflexionar sobre esto junto con las preguntas que he planteado antes, porque mantener manualmente la sincronización entre el modelo y el código fuente es una fuente de fricción y errores potenciales. Si adoptas un enfoque más pragmático en el que quieres obtener un código inicial y no planeas reutilizar el generador con el mismo modelo, entonces esto es menos preocupante. Además, supongamos que el código generado no necesita mucha (o ninguna) modificación manual. En ese caso, puede actualizar el modelo o el generador cuando detecte algún error en el código de salida y regenerarlo con un esfuerzo manual mínimo. Por otro lado, si el código de salida necesita ser modificado sustancialmente a mano después de la generación, probablemente no merezca la pena actualizar el modelo y regenerarlo porque hay que reconciliar de nuevo los cambios manuales con el código generado.

Basura dentro, basura fuera

Esto me lleva al siguiente punto. Sólo se obtiene de este enfoque lo que se pone en él. Esto se aplica tanto al modelo como al generador. Si se toman atajos al construir el modelo dejando en blanco campos no esenciales, el código generado no puede tener el resultado deseado. En mi ejemplo, a veces fui perezoso con la documentación y los valores utilizados para las pruebas, ya que pensé que tendría que modificarlos de todos modos. Pero cuando invertí más esfuerzo, descubrí que necesitaba hacer menos modificaciones. Como ya se ha mencionado, siempre puedes añadir o ajustar tus plantillas de generador si encuentras que se necesita funcionalidad adicional o hay errores de formato. Dedicar tiempo a asegurarse de que el modelo es correcto y lo más completo posible en relación con sus necesidades reducirá los errores y la necesidad de volver a ejecutar el generador. Esto es especialmente importante en los enfoques de una sola vez o cuando el código generado se edita manualmente después de la generación inicial.

Se aplica el principio de Pareto

En regla 80/20 se aplica en gran medida a la generación de código basada en modelos y refuerza la idea del desarrollo iterativo. Puedes obtener aproximadamente el 80% de los beneficios con el 20% del esfuerzo. Si empiezas con plantillas sencillas en las que generas incluso el esqueleto básico de las clases que necesitarás, ya habrás escrito una buena parte del código. Tienes las clases con los nombres correctos en el paquete adecuado, con la cabecera de copyright, importaciones típicas, métodos, declaraciones de campos, constructores, etc. A continuación, puede factorizar gradualmente la lógica más compleja escrita a mano si la encuentra repetitiva en el generador.

Empieza siempre por lo más fácil y no intentes hacer demasiado a la vez. Me di cuenta de que este era especialmente el caso de la generación de código de prueba: escribir un generador para crear sólo las clases de prueba y los métodos con las aserciones me ahorró mucho tiempo. Así podía centrarme en escribir a mano la lógica esencial de las pruebas sin preocuparme de la repetición de tareas. Esto también añade estructura a su flujo de trabajo, por lo que usted sabe exactamente lo que hay que hacer sin ser abrumado por la necesidad de hacer frente a todas las pequeñas cosas. Te sorprenderá lo mucho que esto puede aumentar tu motivación y productividad.

Por otro lado, es importante equilibrar esto con los aspectos prácticos del mantenimiento y la evolución del código base. Si te preocupa más mantener el código generado que el generador y los modelos, no sobredimensiones tu generador porque acabarás haciéndolo exponencialmente más complejo a cambio de beneficios muy marginales. Esto hace que el generador sea más intimidante y difícil de trabajar, por lo que la incorporación de otras personas se convierte en un reto. Como regla general, intente que cada plantilla del generador tenga un aspecto similar a la salida generada. Suponga que sus plantillas consisten más en secciones dinámicas con una lógica de ramificación pesada que en texto estático. En ese caso, probablemente valga la pena dividir esa lógica en métodos de utilidad o simplificar la plantilla para hacerla más legible.

En cualquier caso, conviene tener en cuenta que aquí no hay "magia": crear y mantener el flujo de trabajo MDE junto con sus herramientas es un gasto adicional. Hay que ser consciente del tiempo que se invierte en ello en relación con lo que se ahorra. Y esto nos lleva a justificar la razón de ser de este sistema.

Conclusión

Con esto, llegamos al final de esta miniserie de blogs sobre ingeniería pragmática basada en modelos. Espero que no sólo hayáis aprendido algunas herramientas nuevas y un enfoque de desarrollo, sino que también seáis conscientes de sus limitaciones y de cómo sacar el máximo partido a estas tecnologías. En última instancia, espero que esta introducción a la ingeniería basada en modelos y el estudio de un caso sirvan de ejemplo útil y le ayuden a tomar mejores decisiones en sus proyectos de software, en caso de que decida seguir este camino.

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.