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.