Download Traducción de especificaciones a código ejecutable mediante

Document related concepts
no text concepts found
Transcript
Traducción de especificaciones a código ejecutable mediante
transformadores de ejemplares
R. Heradio Gil, J. F. Estívariz López, I. Abad Cardiel, J. A. Cerrada Somolinos
Dpto. de Ingeniería de Software y Sistemas Informáticos. Universidad Nacional de Educación a Distancia
{rheradio|jose.estivariz|iabad|jcerrada}@issi.uned.es
Resumen
La programación generativa y el desarrollo
dirigido por modelos consideran que el aumento
de la productividad en la realización de software
pasa por elevar el nivel de abstracción de los
lenguajes de programación mediante el uso de
especificaciones o modelos. Un factor clave para
el éxito de estos paradigmas es la traducción
automática de los modelos a código ejecutable.
Para que la traducción automática sea viable, el
dominio de aplicación de los modelos debe
reducirse hasta que la variabilidad entre los
productos
que
puedan
generarse
sea
significativamente inferior a los aspectos
comunes. La estrategia más extendida para las
traducciones consiste en sintetizar productos a
partir de la especificación. Otro enfoque es aplicar
transformaciones sobre la especificación. Aunque
existen lenguajes específicos para la expresión de
transformaciones, exigen superar una curva de
aprendizaje notable y carecen de algunas
prestaciones típicas de los lenguajes de
programación. En este artículo, con el propósito
de aprovechar la proximidad entre los productos
que pueden generarse dentro de un dominio,
proponemos que la aplicación de transformaciones
no se realice sobre la especificación, sino sobre
alguno de los productos. Es decir, el traductor se
define como un programa que toma un producto
previamente desarrollado del dominio, al que
llamaremos ejemplar, y lo transforma para
adecuarlo a una especificación. En lugar de
plantear un lenguaje específico, hemos
incorporado al lenguaje de programación Ruby
una librería con las abstracciones necesarias para
la construcción de transformadores.
Palabras clave: programación generativa,
transformación de programas, línea de productos.
1. Introducción
Los términos línea de productos y familia de
sistemas hacen referencia a dos formas diferentes
de percibir un dominio. Desde un punto de vista
orientado al problema, una línea de productos se
define como un conjunto de sistemas que
satisfacen alguna necesidad del mercado. Desde
una perspectiva orientada a la solución, una
familia de sistemas es un conjunto de sistemas
software muy similares entre sí. La Ingeniería de
Líneas de Productos Software (Product Line
Software Engineering) propone sacar partido de la
coincidencia que generalmente se da entre estos
dos conjuntos, de manera que los productos que
conforman una línea no se construyan uno a uno
de forma aislada, sino colectivamente haciendo
uso intensivo de la reutilización [13].
Por otro lado, la Programación Generativa
(Generative Programming) [5] y el Desarrollo
Dirigido por Modelos, cuyo máximo exponente es
la Arquitectura Dirigida por Modelos (Model
Driven Architecture) de OMG (Object
Management Group) [9], consideran que el
aumento de la productividad en la realización de
software pasa por elevar el nivel de abstracción de
los lenguajes de programación mediante la
utilización de especificaciones o modelos. Como
se señala en [21], un factor clave para el éxito de
estos paradigmas es la traducción automática de
los modelos a código ejecutable. Para que esto sea
viable, el dominio debe restringirse lo suficiente
como para que las coincidencias entre los sistemas
que integran una familia sean notablemente
superiores a las discrepancias [3].
La estrategia más común para las traducciones
consiste en sintetizar productos a partir de una
especificación [20]. Otro enfoque es aplicar
transformaciones sobre la especificación [16].
Existen algunos lenguajes y herramientas
específicos para la expresión de transformaciones
[1], [2] y [4]. La debilidad de estos lenguajes es su
especificidad: exigen superar una curva de
aprendizaje notable, carecen de funcionalidades
(contenedores e iteradores…) y facilidades para
organizar y reutilizar código (clases, módulos o
paquetes…) propias de los lenguajes de
programación de propósito general actuales…
En este artículo, con el objetivo de aprovechar
la proximidad entre los productos que pueden
generarse dentro de un dominio, proponemos que
las transformaciones no se apliquen sobre la
especificación, sino sobre alguno de los
productos. Es decir, el traductor se define como
un programa que toma un producto previamente
desarrollado del dominio, al que nos referiremos
como ejemplar, y lo transforma para adecuarlo a
una especificación. En [12] se propone una
estrategia similar para el mantenimiento de
software que utiliza varios lenguajes específicos
para distintos propósitos. Nosotros, en lugar de
plantear un lenguaje específico aislado, hemos
incorporado al lenguaje de programación Ruby
[18] una librería con las abstracciones necesarias
para facilitar la construcción de transformadores.
Hemos elegido Ruby porque
• Es totalmente Orientado a Objetos (OO).
• Su sintaxis es sorprendentemente concisa.
• Tiene una gran capacidad para el tratamiento
de textos y ficheros. De hecho, incorpora las
expresiones regulares [10] como tipo básico
del lenguaje.
• Ofrece potentes contenedores (listas y tablas)
e iteradores.
• Existen implementaciones del intérprete de
Ruby para distintos sistemas operativos, lo
que asegura un alto grado de portabilidad.
El resto del artículo se estructura como sigue:
en la sección 2, se resume un ejemplo propuesto
en [6] donde se generaliza un programa con
técnicas OO. Este ejemplo servirá para
caracterizar las dificultades de la OO para
construir líneas de productos. En la sección 3, se
plantea la resolución del ejemplo anterior con
nuestro enfoque. En la sección 4, se muestran las
operaciones disponibles para que los traductores
efectúen sus transformaciones. En la sección 5, se
introducen los operadores propuestos para
coordinar un grupo de traductores y detectar las
colisiones que puedan producirse entre ellos.
Finalmente, en la sección 6 se presentan las
conclusiones y trabajo futuro.
2. Ejemplo de creación de una línea de
productos con técnicas OO
En [6] se propone un programa que emula el
reciclaje de basura. El programa recibe un cubo
con basura mezclada (papel y aluminio) y un par
de cubos donde deberá separarla.
Por limitaciones de espacio, sólo se incluye el
diagrama de clases del programa escrito en Java
(Figura 1). El código fuente de los ejemplos y de
la librería escrita en Ruby para la creación de
transformadores
está
disponible
en
www.issi.uned.es/miembros/pagpers
onales/ruben_heradio/prole05.zip
Figura 1. Programa de reciclaje inicial
A continuación, en [6] se aborda la
fabricación de la línea de productos “reciclaje de
cualquier tipo de basura” mediante la
generalización del programa anterior.
En las llamadas a métodos, Java (al igual que
C++ y Smalltalk) sólo soporta polimorfismo sobre
un parámetro: el receptor de la llamada (single
dispatching) [8]. Sin embargo, la generalización
del programa exige la aplicación de polimorfismo
sobre dos parámetros: el tipo de basura y el tipo
de cubo donde se depositará. En [6] se propone
obtener esta prestación con el encadenamiento de
dos llamadas a métodos.
Como puede apreciarse en la Figura 2, la
generalización del programa inicial supone un
esfuerzo de reestructuración importante.
return false;
}
Figura 5. Especificación en Java del tipo de basura
cristal (III)
3. Desarrollo de una línea de productos
mediante la transformación de un
ejemplar
Figura 2. Programa de reciclaje generalizado
Esta solución presenta el inconveniente de que
la especificación de un producto de la línea se
realiza en el mismo lenguaje de programación con
que se codificó. En este caso, no sólo el nivel de
abstracción que ofrece Java está lejos del dominio,
sino que al usuario de la línea se le impone
conocer algunos detalles de implementación.
Concretamente, la especificación de un sistema
que contemple la separación de cristal implica la
adición de las clases Glass (Figura 3) y GlassBin
(Figura 4), y de un método en la clase TypedBin
(Figura 5).
class Glass extends Trash {
static double val = 0.23f;
Glass(double wt) { super(wt); }
double getValue() { return val; }
static void setValue(double nval)
{
val = nval;
}
boolean addToBin(TypedBin[] tb) {
for (int i=0; i<tb.length; i++)
if (tb[i].add(this))
return true;
return false;
}
}
Figura 3. Especificación en Java del tipo de basura
cristal (I)
class GlassBin extends TypedBin {
boolean add(Glass a) {
return addIt(a);
}
}
Figura 4. Especificación en Java del tipo de basura
cristal (II)
boolean add(Glass a) {
En el problema anterior, se puede elevar el nivel
de abstracción de las especificaciones y ocultar los
detalles de implementación si se utiliza un
enfoque generativo como el de la Figura 6.
Especificación
Traductor
Código final
Generador
Transformaciones
Ejemplar
Figura 6. Resolución de la línea de productos “reciclaje
de cualquier tipo de basura” desde un enfoque
generativo
Para determinar la variabilidad entre los
sistemas de una familia existen varias
metodologías de análisis de dominio [17]. La
aplicación de cualquiera de ellas al dominio que
nos ocupa, revelará que la variabilidad se reduce a
los tipos de basura que es capaz de clasificar un
programa de reciclaje. Hecho esto, se puede
construir un lenguaje con el que especificar un
producto. Para ello, se puede utilizar algún
metalenguaje como XML (eXtensible Markup
Language) [7] o alguna herramienta de
metamodelado como GME (Generic Modeling
Environment) [15]. Conviene que los “meta”
permitan expresar restricciones que faciliten la
detección de errores en las especificaciones.
Mientras que los esquemas (Schemas) de XML
permiten expresar pocas restricciones, GME
ofrece el potente lenguaje OCL (Object
Constraint Language) de UML (Unified Modeling
Language) [19]. Por simplicidad, en este artículo
se utilizará XML. La Figura 7 es la especificación
de un programa de reciclaje de los tipos de basura
papel, aluminio y cristal en un lenguaje XML.
<specification>
<outDir value="generation"/>
<trash name="Paper"
value="0.97"/>
<trash name="Aluminum"
value="1.67"/>
<trash name="Glass"
value="0.23"/>
</specification>
Figura 7. Especificación XML de un programa de
reciclaje de papel, aluminio y cristal
Para aprovechar la similitud entre los
programas de reciclaje que se pueden generar,
construiremos el traductor de la Figura 6 partiendo
de uno de ellos al que llamaremos ejemplar.
Concretamente, nuestro ejemplar será el programa
planteado inicialmente en [6] (Figura 1). Un
generador se encargará de interpretar la
especificación y aplicar al ejemplar las
transformaciones oportunas.
4. Operaciones propuestas para la
transformación de un ejemplar
Para la transformación de un ejemplar, los
generadores cuentan con tres operaciones:
1. Sustitución. Expresa el intercambio de un
fragmento de texto por otro. Nuestra
implementación en Ruby ofrece dos métodos:
• sub(regExp, text, name)
• gsub(regExp, text, name)
El parámetro regExp es una expresión regular
que selecciona el fragmento de texto que se
desea sustituir; text es el nuevo texto; name
es un parámetro opcional que sirve para
nombrar la sustitución. Mientras gsub indica la
sustitución de todos los fragmentos de texto que
encajen con regExp, sub sólo actúa sobre la
primera ocurrencia.
2. Producción. Una sustitución puede aplicarse
sobre varios ficheros del ejemplar y sobre un
fichero del ejemplar pueden aplicarse varias
sustituciones. Una producción expresa un
fichero del ejemplar, el grupo de sustituciones
que se aplicará sobre él y el fichero que se
generará
como
resultado.
Nuestra
implementación proporciona el método:
• prod(iFile, oFile, subList,
name)
El parámetro iFile es un fichero del ejemplar;
oFile es el fichero que se generará tras la
aplicación de la lista subList de
sustituciones; subList es opcional y si no se
explicita, sobre iFile se aplicarán todas las
sustituciones previamente definidas; name
también es opcional y sirve para nombrar la
producción.
3. Generación. Expresa la ejecución de un
conjunto
de
producciones.
Nuestra
implementación ofrece el método:
• gen(prodList)
El parámetro prodList es opcional y sirve
para indicar la lista de producciones que se
desea ejecutar. Si no se explicita, se ejecutarán
todas las producciones previamente definidas.
Con nuestra librería, un generador se construye
heredando de la clase Generator (en Ruby, la
herencia se expresa con el símbolo <). La Figura
8 es el generador que transforma el ejemplar de la
Figura 1 siguiendo una especificación como la de
la Figura 7.
class RecycleGen < Generator
def initialize(out_dir, trashes)
#Copying Trash
prod(EXEMPLAR_DIR+'\Trash.java',
out_dir+'\Trash.java', [])
#Generating kind of trashes
trashes.each { |kind, price|
gsub(/Paper/, kind, kind)
sub(/\d+.\d\d/, price,
"#{kind}_price")
prod("#{EXEMPLAR_DIR}"+
"\\Paper.java",
"#{out_dir}\\#{kind}.java",
[kind, "#{kind}_price"])
}
#Generating Recycle
sub(/ArrayList .+
Bin(,ArrayList .+Bin)*/x,
trashes.keys.collect {
|kind|
"ArrayList "+kind+"Bin"
}.join(','),
'argRecycle')
sub(/(if.+;\s*)+/,
trashes.keys.collect {|kind|
"if(t instanceof #{kind}) " +
"#{kind}Bin.add(t);\n"
}.join,
'ifRecycle')
prod("#{EXEMPLAR_DIR}"+
"\\Recycle.java",
"#{out_dir}\\Recycle.java",
['argRecycle', 'ifRecycle'])
end
end #RecycleGen
Figura 8. Generador de la línea de productos “reciclaje
de cualquier tipo de basura”
Si se compara esta solución con la de la
Figura 2, nuestra línea de productos no sólo es
más usable (especificaciones más próximas al
dominio), sino más fácil de construir (exige
menos esfuerzo de diseño y codificación).
5. Coordinación de un grupo de
generadores y detección de colisiones
Cuando la variabilidad de una familia de sistemas
es compleja, conviene dividirla y encapsularla en
distintos generadores. En general, estas divisiones
no estarán confinadas en un solo fichero del
ejemplar, sino dispersas en varios ficheros. Por
eso, un generador puede actuar sobre más de un
fichero del ejemplar y sobre un fichero del
ejemplar puede actuar más de un generador.
La coordinación de un grupo de generadores
puede ser:
1. Secuencial. Se ejecuta un generador tras otro
(Figura 9).
generador_1.gen
generador_2.gen
...
generador_n.gen
Figura 9. Ejecución secuencial de generadores
2. Adición. Se obtiene un nuevo generador
cuyas sustituciones y producciones son la
suma de las sustituciones y producciones de
otros generadores. Nuestra implementación
ofrece dos métodos equivalentes: + aprovecha
la sobrecarga de operadores soportada por
Ruby (Figura 10) y add mantiene la notación
convencional para la invocación a métodos.
(generador_1 +
generador_2 +
...
generador_n).gen
Figura 10. Suma y ejecución de generadores
3. Superposición.
Las
sustituciones
y
producciones de un generador se actualizan
con las de otro que se superpone. Es decir, las
que
tienen el
mismo
nombre
se
“sobrescriben”, y las que no, se añaden.
Nuestra implementación dispone de dos
métodos equivalentes: el sobrecarcargado <<
(Figura 11) y sup! (en Ruby, se suele utilizar
el sufijo ! para nombrar los métodos que
modifican el receptor de la llamada).
(generador_1 <<
generador_2 <<
...
generador_n).gen
Figura 11. Superposición y ejecución de generadores
Una manera de asegurar la calidad de los
productos generados consiste en generar también
juegos
de
prueba
que
verifiquen
su
funcionamiento [22]. Para incorporar esta
prestación a la línea de productos “reciclaje de
cualquier tipo de basura”, añadiremos al ejemplar
la clase RecycleTest, que se sirve del marco
de trabajo JUnit [11]. Después, se desarrollará el
generador TestGen que a partir de una
especificación como la de la Figura 7 generará el
juego de pruebas correspondiente mediante la
transformación de RecycleTest. La Figura 12
incluye la suma y ejecución de dos instancias de
los generadores RecycleGen y TestGen.
(
RecycleGen.new(out_dir, trashes)
+
TestGen.new(out_dir, trashes)
).gen
Figura 12. Generación de un programa de reciclaje de
basura y el juego de pruebas que lo verifica
Si varios generadores actúan sobre la misma
área de texto de un fichero del ejemplar para
generar un mismo fichero de salida, se producirá
una colisión. De igual modo, también pueden
producirse colisiones entre las producciones de un
generador. Para la detección de colisiones, nuestra
implementación proporciona los siguientes
métodos que comprueban si la operación
interrogada puede realizarse sin que se den
colisiones:
• prod?(iFile, oFile, subList)
• gen?(prodList)
•
•
add?(generator)
sup?(generator)
•
•
6. Conclusiones y trabajo futuro
A través de un ejemplo, en este artículo se han
contrastado dos enfoques distintos para el
desarrollo de una línea de productos: la OO y la
programación generativa. El enfoque generativo
separa la implementación del uso de la línea,
facilitando la utilización de lenguajes de
especificación próximos al dominio. Un factor
clave para el éxito de este enfoque es la traducción
automática de una especificación a código
ejecutable.
Para el desarrollo de traductores se ha
presentado una aproximación basada en la
transformación de un ejemplar de la línea de
productos. Es decir, los productos no se sintetizan
a partir de una especificación, sino que se generan
mediante la transformación de otro producto
previamente desarrollado. Aunque existen
lenguajes específicos para la expresión de
transformaciones, exigen superar una curva de
aprendizaje considerable y carecen de algunas
prestaciones típicas de los lenguajes de
programación. Por esta razón, hemos preferido
desarrollar en Ruby una librería para la
construcción de transformadores y aprovechar así
toda la potencia de un lenguaje de programación
OO.
Nuestra librería permite dividir y encapsular la
gestión de la variabilidad en varios generadores.
El carácter disperso de la variabilidad nos ha
llevado a adoptar un enfoque similar al de algunas
propuestas
de
orientación
a
aspectos.
Concretamente, las operaciones de sustitución y
producción presentadas en la sección 4 poseen
cierta analogía con los pointcuts y advices de
AspectJ [14].
Además de la expresión de transformaciones,
nuestra librería facilita la coordinación de un
grupo de generadores y la detección de las
colisiones que se puedan producir.
El ámbito de aplicación de la librería
propuesta no se reduce a la generación de código
Java (como se ha mostrado en este artículo) sino
que se extiende a cualquier lenguaje. De hecho, en
otros trabajos la hemos aplicado para generar
código de lenguajes tan dispares como Perl,
Modula-2, SQL y HTML.
•
Como trabajo futuro nos planteamos:
Estudiar nuevos problemas de aplicación.
Enriquecer nuestra librería con descendientes
de la clase padre Generator especializados
en la generación de código de algún tipo. En
este
sentido,
estamos
desarrollando
descendientes que incorporan expresiones
regulares específicas para el tratamiento de
código de distintos lenguajes.
Dotar a la librería presentada de la capacidad
de producir documentación sobre el proceso
de generación que facilite la depuración de las
transformaciones.
Referencias
[1] Baxter, I.; Pidgeon, C.; Mehlich, M. DMS:
Program Transformation for Practical Scalable
Software Evolution. International Conference
on Software Engineering (ICSE), Edinburg,
Scotland, May 2004, pp. 625-634.
[2] Brand, M.; Heering, J.; Klint, P.; Olivier, P.
Compiling
language
definitions: the
ASF+SDF compiler. ACM Transactions on
Programming Languages and Systems.
Volume 24, Issue 4 (July 2002), pp. 334-368.
[3] Cleaveland, J. C. Program Generators with
XML and JAVA. Prentice Hall, 2001.
[4] Cordy, J.; Dean, T.; Malton, A.; Schneider, K.
Source
Transformation
in
Software
Engineering using the TXL Transformation
System. Special Issue on Source Code
Analysis and Manipulation, Journal of
Information and Software Technology (44,13)
October 2002, pp. 827-837.
[5] Czarnecki, K.; Eisenecker, U. W. Generative
Programming.
Methods
Tools
and
Applications. Addison-Wesley, 2000.
[6] Eckel, B. Thinking in Patterns. Revision 0.9,
5-20-2003.
Capítulo
titulado
“Pattern
refactoring”. http://www.mindview.net.
[7] Extensible Markup Language (XML).
http://www.w3.org/XML
[8] Forax, R.; Duris, D.; Rousel G. A Reflective
Implementation of Java Multi-Methods. IEEE
Transactions on Software Engineering. New
York: Dec 2004.Vol.30, Iss. 12; pg. 1055.
[9] Frankel, D. Model Driven Architecture:
Applying MDA to enterprise Computing. John
Wiley and Sons, 2003.
[10] Friedl, J. Mastering Regular Expressions.
O’Reilly, 2002.
[11] Gamma, E.; Beck, K. JUnit Testing
Framework. http://www.junit.org
[12] Gray, J. et al. Model-Driven Program
Transformation of a Large Avionics
Framework. Generative Programming and
Component Engineering (GPCE) 2004, LNCS
3286, pp. 361-378, 2004.
[13] Kang, K.C.; Jaejoon Lee; Donohoe, P.
Feauture-Oriented Product Line. Software,
IEEE. Volume 19, Issue 4, July-Aug. 2002
Page(s):58 – 65.
[14] Laddad, R. AspectJ in Action. Manning,
2003.
[15] Ledeczi, A. et al. Composing domainspecific
design
environments.
IEEE
Computer. Volume 34, Issue 11, Nov. 2001
Page(s):44 – 51.
[16] Rutherford M. J.; Wolf, A.L. A Case for
Test-Code Generation in Model-Driven
Systems. Generative Programming and
Component Engineering (GPCE) 2003, LNCS
2830, pp. 377-396, 2003.
[17] Sooyong P.; Minseong K.; Vijayan S. A
scenario, goal and feature-oriented domain
analysis approach for development software
product lines. Industrial Management + Data
Systems. Wembley: 2004. Vol. 104, Iss. 3/4;
p. 296.
[18] Thomas, D.; Fowler C.; Hunt, A.;
Programming
Ruby:
The
Pragmatic
Programmers' Guide. Pragmatic Bookshelf;
2nd edition (October 1, 2004).
[19] UML
2.0
OCL
Specification.
http://www.omg.org/docs/ptc/03-10-14.pdf
[20] Visser, E. A survey of Rewriting Strategies in
Program Transformation Systems. Workshop
on Reduction Strategies in Rewriting and
Programming (WRS’01) - Electronic Notes in
Theoretical Computer Science, vol. 57,
Utrecht, The Netherlands, May 2001.
http://www.sciencedirect.com
[21] Weis, T.; Ulbrich, A.; Geihs, K. Model
metamorphosis. Software, IEEE. Volume 20,
Issue 5, Sept.-Oct. 2003 Page(s):46 – 51.
[22] Yuefeng Zhang. Test-driven modeling for
model-driven development. Software, IEEE
Volume 21, Issue 5, Sept.-Oct. 2004
Page(s):80 – 86.