Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. ASP.NET

Generación de código usando T4

by
Difficulty:IntermediateLength:LongLanguages:

Spanish (Español) translation by Steven (you can also view the original English article)

No me gusta la generación de código y, por lo general, lo veo como un "olor". Si estás utilizando generación de código de cualquier tipo, existe la posibilidad de que algo esté mal con tu diseño o solución. Entonces, en lugar de escribir un script para generar miles de líneas de código, deberías retroceder un paso, pensar nuevamente en tu problema y encontrar una solución mejor. Dicho esto, hay situaciones en las que la generación de código puede ser una buena solución.

En esta publicación, hablaré sobre las ventajas y desventajas de la generación de código y luego te mostraré cómo usar las plantillas T4, la herramienta de generación de código integrada en Visual Studio, usando un ejemplo.

La generación de código es una mala idea

Estoy escribiendo una publicación sobre un concepto que creo que es una mala idea, la mayoría de las veces, y sería poco profesional de mi parte si te entregara una herramienta y no te advirtiera de sus peligros.

La verdad es que la generación de código es bastante emocionante: escribe algunas líneas de código y obtienes mucho más a cambio que quizás tengas que escribir manualmente. Así que es fácil caer en una trampa para todos:

"Si la única herramienta que tienes es un martillo, tiende a ver todos los problemas como un clavo". A. Maslow

Pero la generación de código es casi siempre una mala idea. Te remito a esta publicación, que explica la mayoría de los problemas que veo con la generación de código. En pocas palabras, la generación de código resulta en un código inflexible y difícil de mantener.

Aquí hay algunos ejemplos de dónde no debes usar la generación de código:

  • Con la arquitectura distribuida de código generado, ejecuta un script que genera los contratos de servicio y las implementaciones y mágicamente convierte tu aplicación en una arquitectura distribuida. Obviamente, eso no reconoce la excesiva cantidad de llamadas en proceso que se reducen drásticamente en la red y la necesidad de una excepción adecuada y el manejo de transacciones de los sistemas distribuidos, etc.
  • Diseñadores visuales GUI es lo que los desarrolladores de Microsoft han usado durante años (en Windows/Formularios Web y, en cierta medida, en aplicaciones basadas en XAML) donde arrastran y sueltan widgets y elementos de la interfaz de usuario y ven el código de interfaz de usuario (feo) generado para ellos entre bambalinas.
  • Naked Objects es un enfoque del desarrollo de software en el que defines tu modelo de dominio y el resto de tu aplicación, incluida la interfaz de usuario y la base de datos, todo se genera para ti. Conceptualmente, está muy cerca de Model Driven Architecture.
  • Model Driven Architecture es un enfoque para el desarrollo de software donde especificas tu dominio en detalles usando un Modelo de Independencia de Plataforma (PIM). Usando la generación de código, PIM se convierte más tarde en un Modelo específico de plataforma (PSM), que una computadora puede ejecutar. Uno de los principales puntos de venta de MDA es que especifica el PIM una vez y puedes generar aplicaciones web o de escritorio en una variedad de lenguajes de programación con solo presionar un botón que puede generar el código PSM deseado.

    Se crean muchas herramientas de RAD (Desarrollo rápido de aplicaciones) basadas en esta idea: dibujas un modelo y haces clic en un botón para obtener una aplicación completa. Algunas de estas herramientas llegan tan lejos como intentar eliminar completamente a los desarrolladores de la ecuación donde se cree que los usuarios no técnicos pueden realizar cambios seguros en el software sin la necesidad de un desarrollador.

También iba a incluir el mapeo relacional de objetos en la lista, ya que algunos ORM dependen en gran medida de la generación de código para crear el modelo de persistencia a partir de un modelo de datos conceptual o físico. He usado algunas de estas herramientas y he sufrido un poco de dolor para personalizar el código generado. Dicho esto, parece que a muchos desarrolladores les gustan mucho, así que simplemente lo dejé fuera (¡¿o no ?!);)

Si bien algunas de estas "herramientas" resuelven algunos de los problemas de programación y reducen el esfuerzo inicial requerido y el costo del desarrollo de software, hay un enorme costo de mantenimiento oculto en el uso de la generación de código, que tarde o temprano te va a molestar y más código generado que tienes, más que va a doler.

Sé que muchos desarrolladores son grandes fanáticos de la generación de código y escriben un nuevo script de generación de código cada día. Si estás en ese campo y crees que es una gran herramienta para muchos problemas, no voy a discutir contigo. Después de todo, esta publicación no trata de probar que la generación de código es una mala idea.

A veces, solo a veces, la generación de código puede ser una buena idea

Sin embargo, muy raramente, me encuentro en una situación en la que la generación de código es una buena opción para el problema en cuestión y las soluciones alternativas serían más difíciles o más feas.

Aquí hay algunos ejemplos de donde la generación de código podría ser una buena opción:

  • Necesitas escribir un montón de código repetitivo que sigue un patrón estático similar. Antes de intentar la generación de código, en este caso, deberías pensar mucho sobre el problema e intentar escribir este código correctamente (por ejemplo, usar patrones orientados a objetos si está escribiendo código OO). Si has hecho un gran esfuerzo y no has encontrado una buena solución, entonces la generación de código podría ser una buena opción.
  • Con mucha frecuencia, utilizas algunos metadatos estáticos de un recurso y la recuperación de los datos requiere el uso de cadenas mágicas (y tal vez es una operación costosa). Aquí están algunos ejemplos:
    • Código de metadatos obtenidos por reflexión: llamar al código mediante reflexión requiere cadenas mágicas; pero en el momento del diseño, sabes lo que necesitas, puedse usar la generación de código para generar los artefactos requeridos. De esta manera, evitarás utilizar reflexiones en tiempo de ejecución y/o cadenas mágicas en tu código. Un gran ejemplo de este concepto es T4MVC que crea ayudantes fuertemente tipados que eliminan el uso de cadenas literales en muchos lugares.
    • Servicios web de búsqueda estática: de vez en cuando me encuentro con servicios web que solo proporcionan datos estáticos que se pueden recuperar al proporcionar una clave, que termina como una cadena mágica en el código base. En este caso, si puedes recuperar todas las claves mediante programación, puedes generar un código para generar una clase estática que contenga todas las claves y acceder a los valores de las cadenas como ciudadanos de primera clase fuertemente tipados en tu base de código en lugar de usar cadenas mágicas. Obviamente, podrías crear la clase manualmente; pero también tendrías que mantenerlo, manualmente, cada vez que los datos cambien. Luego puedes usar esta clase para acceder al servicio web y almacenar en caché el resultado para que las llamadas subsiguientes se resuelvan de la memoria.

      Alternativamente, si está permitido, simplemente podrías generar todo el servicio en código para que el servicio de búsqueda no sea necesario en el tiempo de ejecución. Ambas soluciones tienen algunas ventajas y desventajas, así que elige la que se ajuste a tus necesidades. Este último solo es útil si las claves solo son utilizadas por la aplicación y no son proporcionadas por el usuario; de lo contrario, tarde o temprano habrá un momento en que los datos del servicio se hayan actualizado pero tú no has generado el código y la búsqueda iniciada por el usuario fallará.

    • Tablas de búsqueda estáticas: esto es muy similar a los servicios web estáticos, pero los datos se encuentran en un almacén de datos en lugar de un servicio web.

Como se mencionó anteriormente, la generación de código hace que el código sea inflexible y difícil de mantener; por lo tanto, si la naturaleza del problema que estás resolviendo es estática y no requiere un mantenimiento frecuente, ¡la generación de código podría ser una buena solución!

El hecho de que tu problema se ajuste a una de las categorías anteriores no significa que la generación de código sea una buena opción. Aún debes intentar evaluar soluciones alternativas y sopesar tus opciones.

Además, si optas por la generación de código, asegúrate de seguir escribiendo pruebas unitarias. Por alguna razón, algunos desarrolladores creen que el código generado no requiere pruebas unitarias. ¡Quizás piensen que es generado por computadoras y las computadoras no cometen errores! Creo que el código generado requiere igual (si no más) verificación automatizada. Personalmente, TDD mi generación de código: primero escribo las pruebas, las ejecuto para verlas fallar, luego genero el código y veo las pruebas pasar.

Kit de herramientas de transformación de plantillas de texto

Hay un increíble motor de generación de código en Visual Studio llamado Text Template Transformation Toolkit (AKA, T4).

Desde MSDN:

Las plantillas de texto se componen de las siguientes partes:

  • Directives: elementos que controlan cómo se procesa la plantilla.
  • Text blocks: contenido que se copia directamente a la salida.
  • Control blocks: código de programa que inserta valores variables en el texto y controla partes condicionales o repetidas del texto.

En lugar de hablar sobre cómo funciona T4, me gustaría usar un ejemplo real. Así que aquí hay un problema que enfrenté hace un tiempo para el que usé T4. Tengo una biblioteca de .NET de código abierto llamada Humanizer. Una de las cosas que quería proporcionar en Humanizer era una API amigable para desarrolladores fluidos para trabajar con DateTime.

Consideré algunas variaciones de la API y al final, me conformé con esto:

Después de saber qué aspecto tendría mi API, pensé en algunas maneras diferentes de abordar esto y en algunas soluciones orientadas a objetos, pero todas ellas requerían un poco de código repetitivo y las que no lo hacían, no me darían la API pública limpia que quería. Así que decidí ir con la generación de código.

Para cada variación he creado un archivo T4 separado:

  • In.Months.tt para In.January y In.FebrurayOf(<algún añor>) y así sucesivamente.
  • On.Days.tt para On.January.The4th, On.February.The(12) y así.
  • In.SomeTimeFrom.tt para In.One.Second, In.woSecondsFrom(<fecha>), In.Three.Minutes y así.

Aquí discutiré On.Days. El código es copiado aquí para tu referencia:

Si estás verificando este código en Visual Studio o quieres trabajar con T4, asegúrate de haber instalado el Editor Tangible T4 para Visual Studio. Proporciona IntelliSense, Resaltado de sintaxis de T4, Depurador de T4 avanzado y Transformación de T4 en la compilación.

El código puede parecer un poco aterrador al principio, pero es solo un script muy similar al lenguaje ASP. Al guardar, esto generará una clase llamada On con 12 subclases, una por mes (por ejemplo, January de Enero, February de Febrero, etc.), cada una con propiedades estáticas públicas que devuelven un día específico en ese mes. Rompamos el código y veamos cómo funciona.

Directives

La sintaxis de las directivas es la siguiente: <#@ NombreDeLaDirectiva [NombreAtributo = "ValorAtributo"] ... #>. Puedes leer más sobre las directivas aquí.

He utilizado las siguientes directivas en el código:

Template

La directiva Template tiene varios atributos que te permiten especificar diferentes aspectos de la transformación.

Si el atributo de depuración debug es true, el archivo de código intermedio contendrá información que le permitirá al depurador identificar con mayor precisión la posición exacta en tu plantilla donde se produjo una interrupción o excepción. Siempre dejo esto como true.

Output

La directiva de Output se utiliza para definir la extensión del nombre de archivo y la codificación del archivo transformado. Aquí establecemos la extensión en .cs, lo que significa que el archivo generado estará en C# y el nombre del archivo será On.Days.cs.

Assembly

Aquí estamos cargando System.Core para que podamos usarlo en los bloques de código más abajo.

La directiva Assembly carga un ensamblaje para que tu código de plantilla pueda usar sus tipos. El efecto es similar a agregar una referencia de ensamblaje en un proyecto de Visual Studio.

Esto significa que puedes aprovechar al máximo el framework .NET en tu plantilla T4. Por ejemplo, puedes usar ADO.NET para golpear una base de datos, leer algunos datos de una tabla y usarlos para la generación de código.

Más abajo, tengo la siguiente línea:

Esto es un poco interesante. En la plantilla On.Days.tt estoy utilizando el método de Ordinalize de Humanizer, que convierte un número en una cadena ordinal, que se utiliza para denotar la posición en una secuencia ordenada, como 1º, 2º, 3º y 4º. Esto se utiliza para generar The1st, The2nd y así sucesivamente.

Del artículo de MSDN:

El nombre del ensamblaje debe ser uno de los siguientes:

  • El nombre seguro de un ensamblado en la GAC, como System.Xml.dll. También puede usar la forma larga, como name="System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". Para obtener más información, visita AssemblyName.
  • El camino absoluto de assembly.

System.Core vive en GAC, por lo que podríamos usar su nombre fácilmente; pero para Humanizer tenemos que proporcionar la ruta absoluto. Obviamente no quiero codificar mi ruta local, así que usé $(SolutionDir) que se reemplaza por la ruta en la que vive la solución durante la generación del código. De esta manera, la generación de código funciona bien para todos, independientemente de dónde guardan el código.

Import

La directiva import te permite referirte a elementos en otro espacio de nombres sin proporcionar un nombre completamente calificado. Es el equivalente de la declaración using en C# o imports en Visual Basic.

En la parte superior, estamos definiendo todos los espacios de nombres que necesitamos en los bloques de código. Los bloques import que ves allí se insertan principalmente en T4 Tangible. Lo único que agregué fue:

Así que luego puedo escribir:

Sin la declaración import y sin especificar assembly por ruta, en lugar de un archivo C#, habría recibido un error de compilación quejándose de no encontrar el método Ordinalize en un entero.

Bloques de texto

Un bloque de texto inserta texto directamente en el archivo de salida. En la parte superior, he escrito algunas líneas de código C # que se copian directamente en el archivo generado:

Más abajo, entre los bloques de control, tengo algunos otros bloques de texto para documentación de la API, métodos y también para cerrar corchetes.

Bloques de control

Los bloques de control son secciones de código de programa que se utilizan para transformar las plantillas. El idioma predeterminado es C#.

Nota: El idioma en el que se escribe el código en los bloques de control no está relacionado con el idioma del texto que se genera.

Hay tres tipos diferentes de bloques de control: Estándar, Expresión y Característica de clase.

Desde MSDN:

  • <# Standard control blocks #> pueden contener sentencias.
  • <#= Expression control blocks #> pueden contener expresiones.
  • <#+ Class feature control blocks #> pueden contener métodos, campos y propiedades.

Echemos un vistazo a los bloques de controles que tenemos en la plantilla de muestra:

Para mí personalmente, lo más confuso de T4 es la apertura y el cierre de los bloques de control, ya que se mezclan con los corchetes en el bloque de texto (si estás generando un código para un lenguaje de corchetes como C#). Encuentro que la forma más fácil de lidiar con esto, es cerrar (#>) el bloque de control tan pronto como lo abras (<#) y luego escribe el código en el interior.

En la parte superior, dentro del bloque de control estándar, estoy definiendo leapYear como un valor constante. Esto es para que pueda generar una entrada para el 29 de febrero. Luego, itero más de 12 meses por cada mes, obteniendo el primer día de mes mediante firstDayOfMonth y el mes mediante monthName. Luego cierro el bloque de control para escribir un bloque de texto para la clase del mes y su documentación XML. El nombre del mes se usa como nombre de clase y en comentarios XML (usando bloques de control de expresión). El resto es simplemente un código C# normal con el que no voy a aburrirte.

Conclusión

En esta publicación hablé sobre la generación de código, proporcioné algunos ejemplos de cuándo la generación de código podría ser peligrosa o útil y también mostró cómo se pueden usar las plantillas T4 para generar código de Visual Studio usando un ejemplo real.

Si deseas obtener más información sobre T4, puedes encontrar una gran cantidad de excelente contenido en el blog de Oleg Sych.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.