El concepto detrás de esta librería es el de transformación de modelos. Por transformación de modelos entendemos al proceso de convertir un modelo en otro. Este proceso puede no ser reversible; es decir, es posible que luego de aplicar una transformación a un modelo no sea posible aplicarle al modelo resultante una transformación inversa para volver al modelo original.
Siguiendo esta línea de pensamiento, consideramos a la generación de código como un caso particular del concepto de transformación de modelos, donde el resultado de la transformación es código fuente. Un ejemplo concreto: la conversión de un modelo UML a un conjunto de plantillas de texto, cuya serialización resulte en código fuente C#.
Podemos considerar a un programador, desde un punto de vista teórico, como un experto en el dominio de la escritura de programas. Un programador sabe como implementar un modelo, porque cuenta con convenciones para representar en código fuente elementos de un modelo. Por ejemplo, podemos darle a un programador un modelo de clases, y pedirle que escriba un formulario para realizar ABMs de alguna de las clases. El programador seguramente programará, para cada atributo de la clase, una etiqueta con su nombre, un control gráfico apropiado para el ingreso de datos - cuadro de texto si el tipo de dato es cadena de caracteres, selector de fecha si es una fecha, un checkbox si el tipo de dato es booleano, etc.
La idea entonces es intentar volcar el conocimiento del experto en el dominio de la programación en un sistema experto, de tal forma que el ordenador sea capaz de producir, a partir de un modelo dado, un programa lo más parecido posible a lo que hubiera hecho un humano en su lugar.
La forma de trabajo, vista en su globalidad, consiste en crear un sistema experto escribiendo un conjunto de reglas y especificando las distintas precedencias entre ellas. Estas reglas son evaluadas por un motor de ejecución, quien determina cual ejecutar en base a las precedencias y al estado de activación de cada regla.
El motor de ejecución provee un entorno, donde hay información proveniente de tres fuentes:
Parámetros: estos se encuentran almacenados en ficheros de configuración.
Modelo de entrada: es el modelo que se quiere convertir.
Conocimiento deducido: el mismo sistema experto puede modificar su memoria activa. De esta manera se puede implementar un mecanismo de interacción indirecto entre reglas.
El entorno del motor mantiene referencias a los siguientes elementos:
El modelo de entrada.
El elemento actual del modelo de entrada.
El modelo de salida.
El elemento actual del modelo de salida.
Un generador típico estará compuesto por dos tipos de reglas: reglas de navegación y reglas de producción.
Las reglas de navegación se activan ante la presencia de un determinado tipo de elemento en la entrada, y procede a "navegar" las relaciones de dicho elemento, cambiando el elemento actual del modelo de entrada.
Las reglas de producción, al activarse ante un elemento en la entrada (y tal vez también ante cierto tipo de elemento a la salida), aplican un algoritmo escrito por el desarrollador para generar nodos a la salida utilizando la información actual que se encuentra en la entrada y la memoria activa.
Se intentará explicar la filosofía y la mecánica de trabajo mediante la implementación de un ejemplo. Escribiremos un generador de formularios a partir de modelos UML.
El generador se limitará a generar, por cada clase UML, una clase C# que implemente un formulario GTK#. Este formulario contará con un elemento de entrada por cada atributo UML. Los elementos serán:
ComboBox para los campos de tipo enumeración.
CheckBox para los campos de tipo bool
.
Entry para los campos de tipo string
y los demás tipos de datos no contemplados.
En este ejemplo seguiremos los pasos mencionados en el capítulo anterior - Capítulo 5, El Proceso de Desarrollo.
Habiendo definido el alcance del proyecto, definimos el formato del modelo de entrada, que como ya fue mencionado, será UML. Solo se prestará atención a las clases y sus atributos, ignorando el resto de los elementos del modelo. Supondremos que los tipos de datos primitivos de .NET (System::String, System::Boolean, etc.) están definidos en el modelo y son usados por el diseñador.
Definimos el siguiente modelo, que contiene lo mínimo indispensable para ejercitar todas las características del generador:
Cabe destacar que en el modelo estamos permitiendo el uso de espacios en los nombres de clases y atributos. Esto deberá ser tenido en cuenta por nuestro generador.
Este es el modelo, serializado en formato XMI en el fichero sample_model.xmi
:
<?xml version="1.0"?> <xmi:XMI xmlns:xmi="http://www.omg.org/XMI" xmi:version="2.0" xmlns:uml="http://schema.omg.org/spec/uml/2.0/uml.xmi" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" > <uml:Package xmi:id="_System" name="System"> <ownedType xmi:id="_System_String" xsi:type="uml:Class" name="String" /> <ownedType xmi:id="_System_Boolean" xsi:type="uml:PrimitiveType" name="Boolean" /> </uml:Package> <uml:Package xmi:id="_Test" name="Test"> <ownedType xmi:id="_Document" xsi:type="uml:Class" name="Document"> <ownedAttribute xmi:id="_Document_kind" name="kind" type="_DocumentKind" /> <ownedAttribute xmi:id="_Document_name" name="name" type="_System_String" /> <ownedAttribute xmi:id="_Document_ispublic" name="is public" type="_System_Boolean" /> </ownedType> <ownedType xmi:id="_DocumentKind" xsi:type="uml:Enumeration" name="Document Kind"> <ownedLiteral xmi:id="_DocumentKind_Text" name="Text" /> <ownedLiteral xmi:id="_DocumentKind_Spreadsheet" name="Spreadsheet" /> <ownedLiteral xmi:id="_DocumentKind_Presentation" name="Presentation" /> </ownedType> </uml:Package> </xmi:XMI>
Hay un detalle que no está presente en el diagrama: la clase Document
está definida en un paquete llamado Test
.
A continuación construimos un prototipo. Para ello, creamos un proyecto Gtk# con MonoDevelop (cualquier otra IDE o editor de textos debería bastar), y realizamos una implementación del modelo.
En un caso real deberíamos prestar especial atención al prototipo, ya que todo el código que produzca el generador estará basado en él.
El código resultante consiste en los archivos DocumentForm.cs
y Main.cs
; el primero es el que contiene lo que queremos generar, y el segundo simplemente contiene el método Main que crea la ventana.
El código de la ventana es simple: contiene unos cuantos métodos estáticos que sirven para crear filas con componentes de distintos tipos (AddComboBox
, AddEntry
, AddCheckButton
), un constructor que crea los componentes de la ventana llamando a estos métodos, y un campo privado por cada componente de ingreso de datos. Hay dos botones, uno para cerrar la ventana y otro para grabar los datos, que por ahora sólo muestra un mensaje en la terminal.
El proyecto completo para MonoDevelop, que incluye tanto al prototipo como al generador completo, puede descargarse desde esta ubicación: prototype.zip.
Analizando el código buscando patrones, obtenemos la siguiente plantilla:
// fichero: Form.txt using System; using Gtk; public class $Name!Form : Window { public $Name!Form () : base ("$Caption!") { this.SetDefaultSize (200, 100); this.DeleteEvent += new DeleteEventHandler (On$Name!FormDelete); VBox components = new VBox(); // size group SizeGroup sizeGroup = new SizeGroup(SizeGroupMode.Horizontal); $WidgetsCreation* // buttons HBox buttonsHBox = new HBox(); Label spacer = new Label(); buttonsHBox.PackStart(spacer, true, true, 0); Button saveButton = new Button(Stock.Save); saveButton.Clicked += new EventHandler(OnSaveClick); buttonsHBox.PackStart(saveButton, false, false, 2); Button closeButton = new Button(Stock.Close); closeButton.Clicked += new EventHandler(OnCloseClick); buttonsHBox.PackStart(closeButton, false, false, 2); components.PackStart(buttonsHBox, false, false, 7); this.Add(components); this.ShowAll (); } /* métodos estáticos omitidos */ void OnSaveClick (object sender, EventArgs a) { Console.WriteLine("'Save' clicked"); } void OnCloseClick (object sender, EventArgs a) { Application.Quit (); } void On$Name!FormDelete (object sender, DeleteEventArgs a) { Application.Quit (); a.RetVal = true; } $Fields* }
En este caso, hemos detectado los siguientes marcadores:
$Caption!
: el nombre del formulario - que puede contener espacios.$Name!
: el nombre de la clase - que no puede contener espacios.$WidgetsCreation*
: los bloques de código que crean los componentes.$Fields*
: la declaración de los componentes que permiten el ingreso de datos.
En la ubicación del marcador $Fields*
se insertarán instancias del Template
en línea FieldDefinition
, cuya definición es "private $FieldType! $FieldName!;
", donde $FieldType!
es uno de los siguientes: Entry
, ComboBox
o CheckButton
según sea el tipo del atributo, y $FieldName!
es el nombre del campo generado en base al atributo.
El marcador $WidgetsCreation*
será reemplazado por una colección de instancias de alguna de las siguientes plantillas, según sea el tipo del atributo:
// fichero: CheckButtonCreation.txt // $Caption! $FieldName! = AddCheckButton(components, sizeGroup, "$Caption!:"); // fichero: ComboBoxCreation.txt // $Caption! $FieldName! = AddComboBox( components, sizeGroup, "$Caption!:", new string[] {$Literals*} ); // fichero: EntryCreation.txt // $Caption! $FieldName! = AddEntry(components, sizeGroup, "$Caption!:");
Aquí, los marcadores son:
$FieldName!
: el nombre del campo que define el componente.$Caption!
: el texto que se presentará al usuario en una etiqueta.$Literals*
: una lista de cadenas de caracteres, cada una de las cuales representa un posible valor del combo, o sea, un literal de la enumeración en base a la cual se generó el ComboBox.
Las correspondientes clases derivadas de Template
son triviales, así que no perderemos tiempo en explicarlas. Un único comentario - para separar a estas clases de las clases propias del sistema experto, las ubicamos en el espacio de nombres ECTutorial.SampleGenerator.TemplateTree
.
Finalmente ha llegado la hora de escribir el código del generador. El enfoque que le daremos es el de sistemas expertos, por lo tanto comenzamos creando una clase derivada de Expert
:
// fichero: Expert.cs using ES = ExpertCoder.ExpertSystem; namespace ECTutorial.SampleGenerator { [ES.ExpertSystemInformation( "Forms Generator", "Generates data entry forms from UML classes", true)] public class Expert : ES.ExpertSystem { public Expert(ES.Environment env) : base(env) { } } }
Por ahora no tenemos nada, ya que no hemos creado ninguna regla. Sin embargo podemos resaltar es el uso de alias de using
, que nos ayudará a distinguir el origen de cada una de las clases que utilicemos, y la necesidad de proveer un Environment
a la clase base. Ya que nosotros no podemos inventar un Environment
, delegamos esta responsabilidad a quien instancie nuestro sistema experto.
Otro aspecto interesante es el uso de atributos para agregar información acerca de nuestro sistema experto. En este caso, estamos utilizado el atributo ExpertSystemInformationAttribute
para indicar el nombre del generador y su propósito, y también para decir que el generador es para que lo utilice un usuario final - hay sistemas expertos que se crean solo para ser extendidos por otros. Esta información puede ser utilizada por otras herramientas que provean una interfaz gráfica amigable para el usuario final.
Un sistema experto (en el ámbito de esta librería) está compuesto por reglas y precedencias entre reglas. Por lo tanto, comenzaremos por crear una regla, que convierta una clase en un formulario. El código es el siguiente:
// fichero: Class2Form.cs using System.IO; using ES = ExpertCoder.ExpertSystem; using UML = NUml.Uml2; using TT = ECTutorial.SampleGenerator.TemplateTree; namespace ECTutorial.SampleGenerator { public class Class2Form : ES.TypeGuardedRule { public Class2Form() : base(typeof(UML.Class)) { } [ES.Parameter("output path", "The directory where the files will be created", true)] public string OutputPath; public override void Execute(ES.Environment env) { UML.Class cls = (UML.Class)env.CurrentInputElement; TT.Form form = new TT.Form(); form.Caption = cls.Name; form.Name = cls.Name.Replace(" ", ""); env.CurrentOutputElement = form; env.Expert.Process(); StreamWriter sw; using( sw = new StreamWriter(Path.Combine(OutputPath, form.Name + "Form.cs")) ) { sw.WriteLine(form.ToString()); } } } }
Una regla consiste de una condición de activación y una acción. En este caso se trata de una regla especial, que deriva de TypeGuardedRule
; en este tipo de reglas, la condición de activación es función del tipo de datos del elemento actual del modelo de entrada y, opcionalmente, del de salida. Estos tipos de datos se indican en el constructor (en este caso, solo nos interesa el tipo de datos del elemento de entrada):
public Class2Form() : base(typeof(UML.Class))
Las reglas pueden necesitar datos externos para realizar su tarea. Estos datos se reciben como parámetros. Para acceder al valor de un parámetro desde una regla, basta con definir un campo o una propiedad y aplicarle el atributo ParameterAttribute
:
[ES.Parameter("output path", "The directory where the files will be created", true)] public string OutputPath;
Los datos que se pasan en el constructor del atributo pueden ser consultados por herramientas externas que permiten utilizar generadores de código de manera amigable al usuario final.
La acción de la regla se especifica sobreescribiendo el método Execute
. Antes de llamar a Execute
, el motor de ejecución inicializa los parámetros apropiadamente. Este método recibe como argumento el entorno de ejecución de la regla, a través del cual es posible acceder al sistema experto, consultar cuales son (y también asignar) los elementos actuales de los modelos de entrada y salida, y realizar otras tareas.
public override void Execute(ES.Environment env) { UML.Class cls = (UML.Class)env.CurrentInputElement; // ... env.CurrentOutputElement = form; env.Expert.Process(); // ...
Ahora agregamos una instancia de esta regla a nuestro sistema experto:
public class Expert : ES.ExpertSystem { public Expert(ES.Environment env) : base(env) { base.AddRule( new Class2Form() ); } }
El método AddRule
agrega una regla a la colección de reglas de un sistema experto.
La compilación del proyecto, tal como está hasta el momento, puede realizarse utilizando el siguiente Makefile
[5]:
all: generator.dll generator.dll: *.cs TemplateTree/*.cs TemplateTree/Templates/* mcs \ -r:ExpertCoder.ExpertSystem.dll \ -r:ExpertCoder.Templates.dll \ -r:ExpertCoder.Uml2.dll \ -t:library \ -out:generator.dll \ -resource:TemplateTree/Templates/CheckButtonCreation.txt \ -resource:TemplateTree/Templates/ComboBoxCreation.txt \ -resource:TemplateTree/Templates/EntryCreation.txt \ -resource:TemplateTree/Templates/Form.txt \ *.cs TemplateTree/*.cs
Cabe destacar que para que esto funcione, es necesario que las librerías de ExpertCoder
estén en la carpeta del proyecto o figuren en el MONO_PATH
.
Como ya habrá notado, el código del generador se compila en una librería, no en un ejecutable. Para correr un generador, es necesario hacer uso de la aplicación ecengine.exe
.
Esta aplicación toma como entrada un fichero XML de configuración, donde se especifica la tarea a ejecutar. En este fichero se indican el proveedor de modelo, la ubicación del modelo, el generador a usar y sus parámetros. El formato es el siguiente:
<?xml version="1.0"?> <expertCoderTask verboseLevel="3"> <!-- configuración del proveedor de modelo --> <inputModelProvider> <!-- TODO: reemplazar este path por la ubicación real. --> <dll>/path/to/ExpertCoder.Uml2ModelProviders.dll</dll> <fullClassName>ExpertCoder.Uml2.ModelProvider</fullClassName> <parameters> <param name="serializedModelPath" value="sample_model.xmi" /> </parameters> </inputModelProvider> <!-- configuración del sistema experto --> <expertSystem> <dll>generator.dll</dll> <fullClassName>ECTutorial.SampleGenerator.Expert</fullClassName> <parameters> <!-- TODO: reemplazar este path por la ubicación real. --> <param name="output path" value="/path/to/output" /> </parameters> </expertSystem> </expertCoderTask>
Ejecutamos ecengine.exe
de la siguiente manera:
$ mono /path/to/ecengine.exe -f ec_task.xml
Expert System Engine: Verbose mode ON; level: 3
Evaluated rule: ECTutorial.SampleGenerator.Class2Form; inactive
$
El sistema nos indica que evaluó la regla Class2Form
, pero que está inactiva. Lo que sucede es que el proveedor de modelo alimenta al generador con el resultado de la deserialización del modelo, que como recordaremos consiste en un IList
; sin embargo, nuestra regla se activa ante instancias de la clase Class
de la librería de UML. Además, la clase Document
está dentro de un Package
, por lo tanto también será necesario exponer el contenido de los paquetes UML.
Para solucionar este problema, añadiremos dos reglas más: una que exponga el contenido del IList
, y otra que exponga el contenido de los paquetes UML - exceptuando al paquete System
.
Este es el código de la regla ExposeDeserializedModel
:
using System.Collections; using ES = ExpertCoder.ExpertSystem; namespace ECTutorial.SampleGenerator { public class ExposeDeserializedModel : ES.TypeGuardedRule { public ExposeDeserializedModel() : base(typeof(IList)) { } public override void Execute(ES.Environment env) { foreach(object o in (IEnumerable)env.CurrentInputElement) { env.CurrentInputElement = o; env.Expert.Process(); } } } }
y este es el código de NavigatePackage
:
using System.Collections; using ES = ExpertCoder.ExpertSystem; using UML = NUml.Uml2; namespace ECTutorial.SampleGenerator { public class NavigatePackage : ES.TypeGuardedRule { public NavigatePackage() : base(typeof(UML.Package)) { } public override void Execute(ES.Environment env) { UML.Package pkg = (UML.Package)env.CurrentInputElement; if(pkg.Name != "System") { foreach(object o in pkg.OwnedElement) { env.CurrentInputElement = o; env.Expert.Process(); } } } } }
No debemos olvidar agregar estas reglas al sistema experto:
public Expert(ES.Environment env) : base(env) { base.AddRule( new Class2Form() ); base.AddRule( new ExposeDeserializedModel() ); base.AddRule( new NavigatePackage() ); }
Ejecutamos ecengine.exe
una vez más, pero bajando el verboseLevel
a "1":
$ mono /path/to/ecengine.exe -f ec_task.xml
Expert System Engine: Verbose mode ON; level: 1
* Executing rule: ECTutorial.SampleGenerator.ExposeDeserializedModel
* Executing rule: ECTutorial.SampleGenerator.NavigatePackage
* Executing rule: ECTutorial.SampleGenerator.NavigatePackage
* Executing rule: ECTutorial.SampleGenerator.Class2Form
$
¡Excelente! el generador está funcionando. En la salida vemos que generó un fichero DocumentForm.cs
. Si analizamos su contenido, veremos que es el código de la ventana, pero sin ningún componente. En el próximo paso crearemos dos reglas más: una para exponer los atributos de una clase, y otra para generar un Entry
a partir de un atributo.
La regla NavigateClass
es muy similar a NavigatePackage
, la única diferencia es que expone los atributos de una clase.
El código de la regla Attribute2Entry
es el siguiente:
using ES = ExpertCoder.ExpertSystem; using UML = NUml.Uml2; using TT = ECTutorial.SampleGenerator.TemplateTree; namespace ECTutorial.SampleGenerator { public class Attribute2Entry : ES.TypeGuardedRule { public Attribute2Entry() : base(typeof(UML.Property), typeof(TT.Form)) { } public override void Execute(ES.Environment env) { UML.Property attrib = (UML.Property)env.CurrentInputElement; TT.Form form = (TT.Form)env.CurrentOutputElement; string fieldName = "_" + attrib.Name.Replace(" ", "") + "Entry"; // entry creation TT.EntryCreation entryCreation = new TT.EntryCreation(); entryCreation.Caption = attrib.Name; entryCreation.FieldName = fieldName; form.WidgetsCreation.Add(entryCreation); // entry field definition TT.FieldDefinition definition = new TT.FieldDefinition(); definition.FieldName = fieldName; definition.FieldType = "Entry"; form.Fields.Add(definition); } } }
De este código podemos destacar el constructor:
public Attribute2Entry() : base(typeof(UML.Property), typeof(TT.Form))
que le indica a la clase base TypeGuardedRule
que debe activarse cuando a la entrada haya una propiedad UML y a la salida una instancia de nuestra plantilla Form
.
Podemos notar que hace uso de los elementos actuales de entrada y de salida:
UML.Property attrib = (UML.Property)env.CurrentInputElement; TT.Form form = (TT.Form)env.CurrentOutputElement;
adaptándolos a las interfaces apropiadas, Property
y Form
respectivamente. A partir del elemento de entrada (el atributo de la clase), crea instancias de las plantillas de definición e inicialización de un campo de tipo Entry
, y agrega estas instancias a las correspondientes colecciones en el elemento de salida - la plantilla del formulario.
Agregamos estas reglas al sistema experto:
public Expert(ES.Environment env) : base(env) { base.AddRule( new Class2Form() ); base.AddRule( new ExposeDeserializedModel() ); base.AddRule( new NavigatePackage() ); base.AddRule( new Attribute2Entry() ); base.AddRule( new NavigateClass() ); }
y probamos esta nueva version:
$ mono /path/to/ecengine.exe -f ec_task.xml
Expert System Engine: Verbose mode ON; level: 1
* Executing rule: ECTutorial.SampleGenerator.ExposeDeserializedModel
* Executing rule: ECTutorial.SampleGenerator.NavigatePackage
* Executing rule: ECTutorial.SampleGenerator.NavigatePackage
* Executing rule: ECTutorial.SampleGenerator.Class2Form
* Executing rule: ECTutorial.SampleGenerator.NavigateClass
* Executing rule: ECTutorial.SampleGenerator.Attribute2Entry
* Executing rule: ECTutorial.SampleGenerator.Attribute2Entry
* Executing rule: ECTutorial.SampleGenerator.Attribute2Entry
$
Podemos notar que ejecutó tres veces la regla Attribute2Entry
, una vez por cada atributo de la clase.
En el código generado encontramos el siguiente fragmento:
// kind _kindEntry = AddEntry(components, sizeGroup, "kind:"); // name _nameEntry = AddEntry(components, sizeGroup, "name:"); // is public _ispublicEntry = AddEntry(components, sizeGroup, "is public:");
El generador está produciendo cuadros de texto para todos los atributos, sin importar su tipo de datos; a continuación corregiremos esto introduciendo reglas más específicas.
Es posible evitar el comportamiento por defecto de un generador ante un caso específico introduciendo una nueva regla y una precedencia. La nueva regla deberá activarse cuando se produzca dicho caso específico, y la precedencia debe establecer que la nueva regla reemplaza a la regla que normalmente se hubiera disparado.
En nuestro caso, la regla que debemos sobreescribir es Attribute2Entry
cuando el tipo de datos del atributo sea System.Boolean
o una enumeración. Comencemos por escribir la regla que manejará las enumeraciones:
using ES = ExpertCoder.ExpertSystem; using UML = NUml.Uml2; using TT = ECTutorial.SampleGenerator.TemplateTree; namespace ECTutorial.SampleGenerator { public class Attribute2ComboBox : ES.Rule { public Attribute2ComboBox() : base(new ES.InputElementInhibition()) { } public override bool IsActiveInState(ES.Environment env) { UML.Property attrib = env.CurrentInputElement as UML.Property; return attrib != null && attrib.Type != null && attrib.Type is UML.Enumeration && env.CurrentOutputElement is TT.Form; } public override void Execute(ES.Environment env) { UML.Property attrib = (UML.Property)env.CurrentInputElement; TT.Form form = (TT.Form)env.CurrentOutputElement; string fieldName = "_" + attrib.Name.Replace(" ", "") + "ComboBox"; // entry creation TT.ComboBoxCreation entryCreation = new TT.ComboBoxCreation(); entryCreation.Caption = attrib.Name; entryCreation.FieldName = fieldName; UML.Enumeration enumeration = (UML.Enumeration)attrib.Type; foreach(UML.EnumerationLiteral literal in enumeration.OwnedLiteral) { entryCreation.Literals.Add("\"" + literal.Name + "\""); } form.WidgetsCreation.Add(entryCreation); // entry field definition TT.FieldDefinition definition = new TT.FieldDefinition(); definition.FieldName = fieldName; definition.FieldType = "ComboBox"; form.Fields.Add(definition); } } }
En este caso hace falta analizar más datos aparte del tipo de los elementos de de entrada y salida, por lo tanto no podemos hacer uso de la clase TypeGuardedRule
. Por esta razón derivamos nuestra regla directamente de Rule
, e implementamos nuestra condición de activación sobreescribiendo el método IsActiveInState
.
Al derivar de Rule
, es necesario indicar un mecanismo de inhibición durante la construcción de la regla. En este caso, escogimos InputElementInhibition
; esto significa que nuestra regla se ejecutará a lo sumo una vez por cada elemento de entrada.
Durante la ejecución de la regla (en el método Execute
) creamos el componente y su inicialización, y además agregamos los literales a la colección Literals
de la plantilla ComboBoxCreation
.
Además de agregar esta regla al sistema experto, debemos indicar que esta nueva regla reemplaza a Attribute2Entry
en caso de que ambas estén activas. Esto se logra añadiendo una relación de precedencia entre ambas, caracterizada de tipo reemplazo. Veamos como queda el sistema experto:
public Expert(ES.Environment env) : base(env) { ES.Rule attribute2Entry = new Attribute2Entry(); ES.Rule attribute2ComboBox = new Attribute2ComboBox(); base.AddRule( new Class2Form() ); base.AddRule( new ExposeDeserializedModel() ); base.AddRule( new NavigatePackage() ); base.AddRule( attribute2Entry ); base.AddRule( new NavigateClass() ); base.AddRule( attribute2ComboBox ); base.AddPrecedence( new ES.Precedence( attribute2ComboBox, attribute2Entry, ES.PrecedenceKind.Replacement ) ); }
En lugar de directamente agregar las reglas, nos quedamos con dos referencias - attribute2Entry
y attribute2ComboBox
. Luego utilizamos estas referencias para crear una precedencia, donde se indica que attribute2ComboBox
tiene mayor precedencia que attribute2Entry
, y que en caso de ejecutarse la reemplaza (esto está indicado por el tercer parámetro, ES.PrecedenceKind.Replacement
).
Ejecutamos el sistema experto, y obtenemos:
$ mono /path/to/ecengine.exe -f ec_task.xml
Expert System Engine: Verbose mode ON; level: 1
* Executing rule: ECTutorial.SampleGenerator.ExposeDeserializedModel
* Executing rule: ECTutorial.SampleGenerator.NavigatePackage
* Executing rule: ECTutorial.SampleGenerator.NavigatePackage
* Executing rule: ECTutorial.SampleGenerator.Class2Form
* Executing rule: ECTutorial.SampleGenerator.NavigateClass
* Executing rule: ECTutorial.SampleGenerator.Attribute2ComboBox
* Executing rule: ECTutorial.SampleGenerator.Attribute2Entry
* Executing rule: ECTutorial.SampleGenerator.Attribute2Entry
$
Podemos notar que una de las ejecuciones de Attribute2Entry
fue reemplazada por una ejecución de Attribute2ComboBox
. Esto se comprueba al revisar el código generado:
// kind _kindComboBox = AddComboBox( components, sizeGroup, "kind:", new string[] {"Text", "Spreadsheet", "Presentation"} ); // name _nameEntry = AddEntry(components, sizeGroup, "name:"); // is public _ispublicEntry = AddEntry(components, sizeGroup, "is public:");
como vemos, a partir del atributo kind
se generó un ComboBox
.
Utilizando este mecanismo de precedencias y reemplazos entre reglas es posible ir refinando el comportamiento del generador, escribiendo reglas cada vez más específicas para casos muy puntuales. Este mecanismo también brinda modularidad y extensibilidad a nuestros generadores, ya que terceras partes pueden extenderlos de maneras no previstas de antemano. Sin embargo, para que esto sea posible, es necesario exponer las reglas como propiedades o campos públicos del sistema experto, de manera tal que otros sistemas expertos puedan crear relaciones de precedencia entre sus reglas y las del generador extendido.
Si comparamos la salida del generador con el prototipo, veremos que hay algunas pequeñas diferencias; por ejemplo, el componente que maneja el atributo "is public" en el prototipo se llama "_isPublicCheckButton", mientras que en el código generado se llama "_ispublicCheckButton" (la "p" de "public" está en minúsculas). Hay dos maneras de lidiar con estas diferencias - modificar el generador o modificar el prototipo. En este caso en particular, modificar el prototipo parecería ser lo más conveniente, porque de otra forma se ensuciaría el código del generador sin aportar valor.
Makefile
s para compilar estos proyectos porque es necesario proveer muchos parámetros al compilador, y hacerlo a mano cada vez puede resultar muy tedioso.(C) 2005, Rodolfo Campero.