Tabla de contenidos
El proceso de desarrollo de generadores de código es bastante simple, y consiste en los siguientes pasos:
Como todo proyecto de desarrollo, es muy importante determinar, o al menos acotar, su alcance.
Antes de escribir un generador, es necesario determinar el formato en el que se proveeran los datos al mismo. Una opción frecuente es el uso de modelos UML, pero en ocasiones otros formatos pueden ser adecuados: MOF, esquemas de bases de datos, ensamblados .NET, sistemas de ficheros, etc. Todo depende del alcance del proyecto.
También es posible utilizar XML como modelo de entrada; esto es, interpretar instancias de las clases de System.Xml
según los nombres de elementos y los valores de los atributos. Sin embargo, se recomienda crear un modelo de objetos, tal vez utilizando la herramienta xsd
cuando sea posible o manualmente si hace falta, ya que el nivel de abstracción que esto provee simplifica el considerablemente el trabajo.
Durante el desarrollo del generador resulta conveniente contar con un modelo de entrada que ejercite toda la funcionalidad del generador. Este modelo no debe ser redundante, o sea, no hace falta ejercitar cada característica del generador más de una vez; por ejemplo, si nuestro generador debe ser capaz de generar un cuadro de texto por cada atributo de tipo string, con tener un solo atributo de tipo string en nuestro modelo de entrada basta para ejercitar esta característica.
Así como es necesario un modelo de entrada representativo, también hace falta un ejemplo de la salida que el generador debería producir. Este ejemplo de salida es lo que llamamos prototipo.
Este prototipo servirá para detectar fallas y funcionalidad faltante del generador, ya que en todo momento podremos comparar la salida del generador con el prototipo. Para realizar esta comparación es mejor utilizar diff
o alguna herramienta visual, como por ejemplo Meld.
Durante la evolución del generador, es frecuente detectar que ciertas construcciones del prototipo pueden ser reemplazadas por otras cuya lógica es equivalente, pero que son más fáciles de generar. Por ejemplo, el siguiente fragmento de código:
string[] miArray = new string[2] { "Nombre de Atributo 1", "Nombre de Atributo 2" }
es equivalente a este otro:
string[] miArray = new string[] { "Nombre de Atributo 1", "Nombre de Atributo 2" }
donde no se especifica el tamaño del array; sin embargo, es más sencillo escribir un generador para la segunda versión.
Por lo tanto, durante el desarrollo del generador es probable que el prototipo también evolucione.
Contando con un prototipo, es más fácil escribir las plantillas. Lo que debemos hacer es mirar el código buscando patrones, y reemplazar las ocurrencias de patrones por marcadores. Las ocurrencias originales se transforman en nuevas plantillas, a las cuales se les aplica el mismo procedimiento hasta que los marcadores sean atómicos, es decir, puedan ser reemplazados por cadenas simples.
Consideremos el siguiente prototipo:
// Archivo: Clase.txt (inicialmente copia de ClaseDePrueba.cs) namespace Prototipo { public class ClaseDePrueba { public int PropiedadInt { get { return _propiedadInt; } set { _propiedadInt = value; } } public string PropiedadString { get { return _propiedadString; } set { _propiedadString = value; } } private int _propiedadInt; private string _propiedadString; } }
En este prototipo encontramos un patrón: por cada atributo de la clase se implementa una propiedad pública y un campo privado. Extraemos todas las ocurrencias de este patrón a un par de plantillas externas:
// Archivo: Propiedad.txt (inicialmente parte de Clase.txt) public string PropiedadString { get { return _propiedadString; } set { _propiedadString = value; } }
// Archivo: Campo.txt (inicialmente parte de Clase.txt) // podría ser simplemente un template en línea private string _propiedadString;
y reemplazamos las ocurrencias en la plantilla original por un marcador:
// Archivo: Clase.txt (inicialmente copia de ClaseDePrueba.cs) namespace Prototipo { public class ClaseDePrueba { $Propiedades* $Campos* } }
Continuando con el procedimiento, reemplazamos las particularidades de cada plantilla por marcadores, obteniendo lo siguiente:
// Archivo: Clase.txt (inicialmente copia de ClaseDePrueba.cs) namespace $EspacioDeNombres! { public class $Nombre! { $Propiedades* $Campos* } }
// Archivo: Propiedad.txt (inicialmente parte de Clase.txt) public $TipoDeDato! $NombreDePropiedad! { get { return $NombreDeCampo!; } set { $NombreDeCampo! = value; } }
// Archivo: Campo.txt (inicialmente parte de Clase.txt) // podría ser simplemente un template en línea private $TipoDeDato! $NombreDeCampo!;
Ahora que tenemos bien definidos el modelo de entrada y el modelo de salida, podemos escribir el código que realiza la conversión.
En este punto, podemos decidir utilizar la programación convencional, utilizando el lenguaje soportado por el CLI que más nos guste (C#, VB.NET, Boo, etc.), o podemos hacer uso de la librería ExpertCoder.ExpertSystem
para estructurarlo como un sistema experto. Todo dependerá de la complejidad del generador y de la necesidad que tengamos de reutilizar lógica de transformación.
Suponiendo que escogemos la programación tradicional, el código será similar a este:
// ... // se asume claseUML de tipo NUml.Uml2.Class, obtenida del modelo // ver http://numl.sourceforge.net/index.php/Main_Page // // crea una instancia de la plantilla de clases MiGenerador.Templates.Clase cls = new MiGenerador.Templates.Clase(); // asigna sus propiedades cls.EspacioDeNombres = claseUML.Namespace.QualifiedName.Replace("::", "."); cls.Nombre = claseUML.Name; // por cada atributo, crea una propiedad y un campo foreach(UML.Property attrib in cls.OwnedAttribute) { string name = attrib.Name: string propname = name[0].ToUpper() + name.Substring(1); string fieldname = "_" + name[0].ToLower() + name.Substring(1); string datatype = attrib.Type.QualifiedName.Replace("::", "."); // crea una plantilla para propiedades MiGenerador.Templates.Propiedad prop = new MiGenerador.Templates.Propiedad(); prop.TipoDeDato = datatype; prop.NombreDePropiedad = propname; prop.NombreDeCampo = fieldname; // la agrega a la lista de propiedades de la clase cls.Propiedades.Add(prop); // idem para los campos MiGenerador.Templates.Campo field = new MiGenerador.Templates.Campo(); field.TipoDeDato = datatype; field.NombreDeCampo = fieldname; cls.Campos.Add(field); } // crea un archivo cuyo nombre se obtiene del nombre de la clase StreamWriter sw; using( sw = new StreamWriter(claseUML.Name + ".cs") ) { sw.WriteLine(cls.ToString()); }
Que claro en este ejemplo que hemos separado completamente la lógica de transformación de la presentación del producto del generador mediante el uso de plantillas.
Una vez escrito el generador, lo ejecutamos pasándole como parámetro el modelo de entrada de prueba; luego comparamos la salida con el prototipo, como ya se explicó en Sección 5.5, “Un prototipo”.
Cuando la salida del generador sea igual al prototipo, habremos terminado.
(C) 2005, Rodolfo Campero.