JPearl – Uma Linguagem para Descrição de Reestruturações em Programas Java Marcelo de Almeida Maia1, Ademir Alvarenga de Oliveira1 1 Departamento de Computação – Universidade Federal de Ouro Preto (UFOP) Campus Morro do Cruzeiro – ICEB – 35.400-000 – Ouro Preto – MG – Brasil {marcmaia,ademirao}@iceb.ufop.br Abstract. This paper describes JPearl, a language for specifying restructurings on Java programs. The language conception was based on JaTS[Castor e Borba 2001], [Castor, et. al. 2001], a transformation language for Java. It is shown some improvements, where the most important are the ability to compose restructurings and a more flexible way to represent matching rules and executable declarations. The design choices were driven by the necessity of describing complex refactorings, such as those described in [Fowler 1999]. The conclusion is that JPearl is sufficiently flexible, giving adequate freedom to the user. Resumo. Este artigo descreve JPearl, uma linguagem para especificar reestruturações em programas Java. A concepção da linguagem foi baseada em JaTS [Castor e Borba 2001], [Castor, et. al. 2001], uma linguagem para transformações em programas Java. São apresentadas algumas contribuições, onde as mais importantes são a habilidade para composição de reestruturações e uma maneira mais flexível para representar as regras de casamento e as declarações executáveis. As decisões de projeto foram guiadas pela necessidade de especificar refabricações complexas, como as descritas em [Fowler 1999]. A conclusão é que JPearl se mostra suficientemente flexível, fornecendo um grau de liberdade adequado para o usuário. 1. Introdução O uso de orientação por objetos – OO na construção de software se tornou uma prática popular. Apesar dos conceitos OO serem considerados simples, a aplicação destes conceitos no projeto de programas não é necessariamente uma tarefa trivial. O trabalho em padrões de projeto, proposto originalmente em [Gamma, et. Al. 1995], evidencia que para se obter um projeto de qualidade são necessários experiência e conhecimento por parte do projetista. Não raramente são construídos grandes sistemas cujo projeto oferece claramente margens a melhorias, mesmo quando profissionais experientes são responsáveis pelo projeto. Esta situação ocorre pois a evolução dos requisitos de um software é inerente ao seu ciclo de vida, implicando em incrementos ou alterações que normalmente requerem o seu reprojeto. Bons projetistas de software geralmente sentemse incomodados com a necessidade de reprojeto de software, pois pode parecer que tal reprojeto foi conseqüência de um projeto de baixa qualidade. Contudo, o uso sistemático de reprojeto no ciclo de desenvolvimento de software tem ganho adeptos com a proposta de Extreme Programming [Beck 1999]. Uma abordagem sistemática para o reprojeto é a refabricação1, que consiste na aplicação de itens de reestruturações (refabricações) aos programas de tal forma a preservar o comportamento observável do sistema [Fowler 1999]. Na prática, para a aplicação efetiva de refabricação é necessária a assistência de ferramentas. Existem várias ferramentas de assistência para aplicação de refabricações [Fowler 2002]. Dentre as ferramentas observadas, foi notado um problema comum: a falta de flexibilidade para definição de novos itens de reestruturação. Esta flexibilidade é considerada fundamental, pois assim como a atividade de projeto requer a habilidade e experiência do projetista, o qual pode definir seus próprios padrões de projeto, também a atividade de refabricação tem a mesma característica, podendo o projetista desejar definir seus próprios itens de reestruturação. Existem algumas possibilidades para a inclusão de novos itens de reestruturação em uma dada ferramenta. Em ferramentas com código-fonte aberto é possível incluir um item de reestruturação diretamente. Contudo, esta tarefa não é trivial pois requer que o projetista conheça, com detalhes, como os itens de reestruturação são implementados. Esta tarefa poderia ser facilitada se a ferramenta disponibilizasse um meta-modelo para descrição da reestruturação, de tal forma que o projetista necessitasse apenas conhecer a API para definir e acoplar na ferramenta um item de reestruturação. Outra possibilidade é o uso de linguagens para transformação de programas [Cordy e Carmichael 1993][Felix e Hausler 1999]. Castor e Borba definiram JaTS, uma linguagem para transformação de programas Java [Castor e Borba 2001], [Castor, et. al. 2001]. Em JaTS, as transformações são descritas basicamente através de uma precondição, uma cláusula à direita e uma cláusula à esquerda. Cada cláusula define uma ou mais declaração de tipos. Os tipos declarados na cláusula à esquerda são casados com os tipos de um programa Java. A cláusula à direita define os tipos que serão produzidos com a transformação. A aplicação de uma transformação para um tipo Java é executada em três fases: parsing, transformação e unparsing. A fase de transformação é executada em três passos: casamento, troca e execução. No casamento, a árvore de sintaxe do tipo Java a ser transformado casa com a cláusula à esquerda. No passo de troca, as variáveis da cláusula à direita são trocadas com os valores da variáveis da cláusula à esquerda que já foram casados. O terceiro passo consiste executar algumas estruturas de JaTS na árvore definida pela cláusula à direita. Apesar de permitir definir transformações para tipos de Java de maneira interessante, JaTS poderia oferecer algumas características mais poderosas para expressar, mais adequadamente, transformações que definam itens de reestruturação complexos. Dentre estas características pode-se destacar: 1) Permitir composição de transformações. Um item de reestruturação, como os definidos em [Fowler 1999], podem ser extremamente complexos e descritos a partir de outros itens de reestruturação; 2) Permitir a parametrização das transformações. 3) Permitir maior flexibilidade na descrição de declarações executáveis, permitindo a utilização dos próprios recursos de Java para descrever estas declarações; 4) Oferecer facilidades para 1 Do inglês, refactoring. adaptação da linguagem em outras ferramentas. Uma ferramenta de refabricação é realmente útil quanda está integrada a um ambiente de desenvolvimento. O objetivo deste trabalho é descrever uma linguagem para descrever refabricações reais, isto é, a linguagem deve ser capaz de descrever de maneira natural e direta, a mecânica de todos os itens de reestruturação apresentados em [Fowler 1999]. A idéia é oferecer contribuições para os itens apontados sobre JaTS anteriormente. A linguagem será validada, experimentando suas construções na descrição de uma refabricação que possa ilustrar suas capacidades. Não é objetivo deste artigo, propor uma linguagem que permita somente a descrição de refabricações válidas, isto é, aquelas que preservam o comportamento observável do sistema. Além disso, neste trabalho não será definida a semântica formal da linguagem, a qual deverá ser apresentada em outra ocasião. A seguir é apresentado um contexto de trabalhos relacionados a JPearl. Em seguida são apresentadas algumas dificuldades existentes na definição da linguagem. Na Seção 4, é apresentada a linguagem propriamente dita. Na Seção 5, são apresentados aspectos relativos à sua implementação. Na Seção 6, é apresentada uma avaliação e a conclusão do trabalho. 2. Sistemas de metaprogramação Um sistema de metaprogramação é uma ferramenta cujos tipos de dados básicos incluem programas ou fragmentos de programas de uma linguagem específica, chamada de linguagem-objeto do sistema. Tais sistemas facilitam a escrita de metaprogramas, isto é, programas sobre programas. Metaprogramas recebem como entrada, programas e fragmentos na linguagem-objeto, executam operações sobre eles, e possivelmente geram programas na linguagem-objeto modificados como saída [Cameron e Ito 1984]. Uma área primária para aplicação de sistemas de metaprogramação é a transformação de programas fonte-para-fonte. É uma preocupação dos sistemas de transformação de programas que certas relações semânticas sejam preservadas entre o programa original e o programa gerado. Assim, o conceito de programação transformacional foi caracterizado como uma metodologia de construção de programas por aplicações sucessivas de regras de transformação. Usualmente o processo inicia com uma especificação (formal) do problema e termina com um programa executável, sendo garantido que a versão final do programa satisfaz a especificação inicial [Partsch e Steinbrüggen 1983]. Existem outras aplicações possíveis para metaprogramação. Estas incluem utilitários como pretty-printers [Oppen, 1980], geradores de documentos [van den Brand e Visser 1996], verificadores de plágio [Donaldson, et. al. 1981][Wise 1992]. Enfim, existe potencialmente uma grande variedade de programas sobre programas em diversas áreas. A preocupação inicial em relação à aplicabilidade de JPearl como um sistema de metaprogramação é a reestruturação de programas Java, não necessariamente preservando a semântica, mas possivelmente evoluindo o programa. 3. Dificuldades Existentes Um dos aspectos relevantes no projeto de classes é o controle da relação de dependência entre elas. Qualquer modificação feita em uma classe pode interferir em si própria e em suas classes dependentes. A referência cruzada entre as classes é um aspecto importante que deve ser tratado na implementação de uma linguagem de reestruturação. É necessário que haja uma definição da abrangência de cobertura desejada. Ou seja, uma classe não necessariamente conhece quais são todas suas classes dependentes e portanto esta informação deve ser fornecida pelo projetista, possivelmente, no momento de se aplicar uma reestruturação àquela classe. Assim, a manipulação do contexto é uma funcionalidade que a linguagem de reestruturação deve oferecer de maneira amigável. Na composição de reestruturações é necessário que as reestruturações sendo compostas troquem informação entre si e com o ambiente de execução. Na definição de regras de reestruturação é necessário o conhecimento das estruturas sintáticas disponíveis sobre a linguagem fonte utilizada, Java, neste caso. Além disso, cada estrutura sintática tem suas características e funcionalidades próprias que precisam ser de conhecimento do projetista. Um dos requisitos no projeto da linguagem é permitir que novos recursos possam ser introduzidos de maneira que não afete sua estrutura básica, e que possam ser implementados com facilidade. 3. A Linguagem JPearl Neste trabalho uma reestruturação é definida como uma regra para o casamento de padrão e geração de programas, sem a necessidade de preservação de propriedades semânticas. A linguagem JPearl2 define dois tipos de reestruturações: 1) reestruturações primitivas que são aplicadas em um único arquivo e 2) reestruturações compostas que são construídas a partir de outras reestruturações, sejam primitivas ou compostas. Para oferecer maior clareza e separação de conceitos na definição de reestruturações, foram definidas duas linguagens: 1) JPearl-RL (JPearl Rule Language) para definição das regras de casamento e de saída que ocorrem nas reestruturações primitivas, 2) JPearl-CL (JPearl Core Language) para definição das reestruturações compostas e das precondições e ações em reestruturações primitivas. 4.1. Reestruturação Primitiva A sintaxe para uma reestruturação primitiva pode ser descrita da seguinte maneira: <PrimitiveRestruct> ::= “file” “restructuring” <Identifier> “{” matchRule <MatchRule> matchRuleActions <MatchRuleActions> preconditionActions <PreconditionActions> precondition <Precondition> outputRule <OutputRule> outputRuleActions 2 Acrônimo para Java Programs Evolving with A Restructuring Language <OutputRuleActions> “}” onde, 1. MatchRule é a regra de casamento com o arquivo de entrada. 2. MatchRuleActions são ações auxiliares ao casamento de um arquivo de entrada. 3. PreconditionActions são ações auxiliares à definição de uma precondição. 4. Precondition é uma condição que deve ser satisfeita pelo arquivo de entrada. 5. OutputRule é uma regra que descreve como deve ser construído o arquivo de saída. 6. OutputRuleActions são ações auxiliares à produção do arquivo de saída. Execução de uma Reestruturação Primitiva Uma reestruturação primitiva ocorre em oito passos básicos: 1) Ocorre a análise da regra de casamento (MatchRule), sendo gerada a árvore de sintaxe correspondente; 2) Ocorre a execução das ações da regra de casamento (matchRuleActions), que eventualmente modificam a árvore de sintaxe gerada no passo anterior; 3) Ocorre a análise do arquivo de entrada, sendo gerada a árvore de sintaxe correspondente que é casada com a árvore obtida no passo 2. 4) Ocorre a execução das ações da precondição (PreconditionActions) 5) A precondição (Precondition) é verificada e os passos seguintes somente ocorrem se o resultado for verdadeiro. 6) Ocorre a análise da regra de saída (OutputRule), produzindo a AST de saída. 7) As ações da regra de saída (OutputRuleActions)são executadas, afetando possivelmente a AST de saída. 8) O arquivo de saída é escrito. Existem propriedades públicas de uma reestruturação primitiva que são o arquivo de entrada e o arquivo de saída que são definidos externamente, onde a reestruturação é utilizada. Se o arquivo de entrada e o arquivo de saída são o mesmo arquivo ou se o arquivo de saída já existe, então o arquivo de saída é reescrito, caso contrário é criado um novo arquivo para representar o arquivo de saída. Variáveis de Reestruturação As variáveis de reestruturação ocorrem em qualquer parte de uma reestruturação. Elas servem para recolher informações sobre o casamento de um arquivo. Toda variável de reestruturação é declarada com um tipo e um nome. Todos os tipos de variável são predefinidos pois correspondem ao tipo de uma estrutura sintática de Java. Por exemplo, uma variável do tipo TypeDeclaration casa necessariamente com uma declaração de tipo. Sintaxe para declaração de variáveis de reestruturação dentro de regras (JPearl-RL): <VarDeclRL> ::= “#”<TipodaVariavel>“:”<Identificador> A declaração de uma variável em uma regra deve ocorrer no primeiro ponto onde ela é referenciada. Sintaxe para declaração de variáveis de reestruturação dentro de ações ou dentro de reestruturações compostas (JPearl-CL): <VarDeclCL> ::= “#”<TipodaVariavel> “#”<Identificador> [“=” <ExpressaoJPearl>“;”] O escopo de uma variável dentro de uma reestruturação primitiva é definido como todo o corpo da reestruturação após a declaração da variável. Exceção é feita quando a declaração da variável ocorrer dentro de um bloco de um método, pois neste caso o escopo segue a mesma regra de Java. O escopo de uma variável dentro de uma reestruturação composta segue as mesmas regras de Java, como será mostrado mais adiante. Sintaxe para acesso de variáveis de reestruturação <VarAccess> ::= “#”<Identificador> Regras de Casamento As regras de casamento e de saída são escritas na linguagem JPearl-RL, que é um superconjunto de Java. Para simplificar a exposição, não definiremos a sintaxe rigorosamente, pois ela é estruturalmente semelhante à de Java. Contudo, ela pode ser ilustrada da seguinte maneira: Sintaxe para declaração de regras de casamento: <Rule> ::= ( <VarAccess> | <VarDeclRL> | <Literal> | “#[” <Rule> “#]” | “@extern” <Rule> | “#{{” <Rule> “#}}” )* A própria sintaxe da linguagem Java define onde devem estar posicionadas as variáveis. Em outras palavras, da mesma forma que, em um programa deve-se colocar a declaração de pacote antes da cláusula imports, tem-se que uma variável que representa a declaração de pacote também deve estar posicionada de maneira análoga. A estrutura de uma regra equivale à estrutura de um programa Java. As variáveis que são declaradas na regra de casamento poderão ser utilizadas na regra de saída, nas ações e na precondição. Estas variáveis coletarão ramos da árvore de sintaxe do arquivo de entrada que serão anexados à arvore do arquivo de saída. Para possibilitar descrever reestruturações mais genéricas foi criada a definição de regra opcional. Um fragmento de regra será opcional se estiver entre os delimitadores #[ e ]#. As variáveis neste fragmento não necessitam casar, pois a parte correspondente do programa não necessita estar presente no arquivo de entrada. É importante ressaltar que nem todos os fragmentos poderão ser opcionais. Por exemplo, uma variável que representa um nome de uma classe nunca pode ser opcional. Caso isto aconteça, tem-se um erro sintático na regra. Para possibilitar a parametrização das regras é permitido anotar um fragmento de regra com o modificador “@extern”. Isto significa que a possibilidade de casamento não é única, e será definida externamente, seja numa reestruturação composta, seja no ambiente de execução. Para possibilitar enxergar um ramo de árvore através do conceito todo-parte é possível associar à uma variável uma sub-regra de casamento, delimitada por chaves duplas, isto é, #{{<Rule>}}#. Seja o seguinte exemplo. #[#PackageDeclaration: pckg]# #[#ImportDeclarationSet: imports]# public class #Identifier: cn { #[#ClassBodyDeclarationSet: cbds1]# @extern #FieldDeclaration: fd #{{ #[#FieldModifiers: fm]# #Type: t }}# #[#ClassBodyDeclarationSet:cbds2]# } #Identifier: v; A variável pckg será casada com um declaração de pacote no programa de entrada, a qual pode ser opcional. A variável imports será casada com um conjunto de declarações de importação. Em seguida, deve ocorrer o casamento dos literais public e class, do identificador com o nome da classe e do delimitador de classe. Na regra acima, o objetivo é casar qualquer declaração de campo com a variável fd. Todo o corpo da classe que estiver acima desta declaração de campo casará com a variável cbds1 e todo o corpo que estiver abaixo casará com a declaração de cbds2. Como a definição de qual campo será casado não pôde ser definido sintaticamente, foi necessário o uso do modificador externo para que uma intervenção externa escolha o respectivo campo. Após definida qual declaração de campo casa com fd, ocorre o casamento desta declaração com as variáveis fm, t e v. Regras de Saída A regra de saída define como será (re)escrito o arquivo de saída. As variáveis definidas na regra de casamento podem ser utilizadas na regra de saída. Se na primeira existe uma variável chamada var1, e na segunda também, então elas são a mesma variável. Os marcadores de trecho de regra opcional não possuem nenhum significado na Regra de Saída. O marcador de trecho de regra definido externamente significa que os ramos a serem introduzidos na árvore serão definidos em uma reestruturação composta ou no ambiente externo. A construção de regras todoparte tem a mesma utilidade que na regra de casamento. A regra de saída tem, portanto, a mesma sintaxe da regra de casamento. Seja o seguinte exemplo, onde um programa Java que casou com a regra de entrada do exemplo anterior é reescrito sem a respectiva declaração de campo. #PackageDeclaration: pckg #ImportDeclarationSet: imports public class #Identifier: cn { #ClassBodyDeclarationSet: cbds1 #ClassBodyDeclarationSet: cbds2 } A Linguagem JPearl-CL As estruturas da linguagem JPearl-CL são semelhantes às estruturas existentes em Java, declaração de classes, declaração de campos, declaração de métodos, comandos, estruturas de controle. Existem duas diferenças básicas entre JPearl-CL e Java. Uma diferença é a habilidade que a primeira tem em acessar e declarar variáveis de reestruturação. Na linha 28 da Figura 5 do Apêndice 1, é declarada a variável #mdgetter como sendo do tipo #MethodDeclaration. Na linha 29 da mesma figura, a variável #newFieldDecl, que foi declarada na seção outputRule (linha 19) da mesma reestruturação, é acessada para definir o tipo de retorno da declaração de método #mdgetter que está sendo construída. A outra diferença é a habilidade de se definir reestruturações compostas em JPearl-CL. Este habilidade será detalhada na Seção 3.2. A seguir será definido como as precondições e as ações podem ser definidas utilizando JPearl-CL. Precondições As precondições são expressões booleanas escritas em JPearl-CL. Observe o exemplo definido na Figura 6 do Apêndice 1. A expressão booleana acessa as variáveis #cbds1 e #cbds2 declaradas na seção matchRule, e acessa também as variáveis #mdgetter e #mdsetter declaradas na seção preconditionActions. Assim, podemos perceber que o escopo no qual uma seção de precondição de uma reestruturação primitiva está inserido oferece visibilidade às variáveis declaradas nas seções matchRule, matchRuleActions e preconditionActions. Ações para Precondições e para Regras de Casamento e de Saída As ações para precondições e para regras de casamento e de saída são definidas através de um método void execute(), específico para cada uma das respectivas seções da reestruturação, que é chamado em um momento predefinido durante a execução de uma reestruturação primitiva, como descrito na seção seguinte. É possível declarar variáveis externas ao método execute() que serão visíveis após sua declaração dentro de toda reestruturação. Por exemplo, as variáveis #mdgetter e #mdsetter declaradas respectivamente nas linhas 05 e 06 da Figura 6 do Apêndice 1 podem ser acessadas na seção de precondição da respectiva reestruturação. Além disso, é possível declarar variáveis locais ao método que certamente serão visíveis somente dentro do método, e também é possível declarar métodos auxiliares que são visíveis dentro da respectiva seção de ação. 4.2. Reestruturação Composta Uma reestruturação composta é escrita na linguagem JPearl e portanto é bastante similar a um programa Java. Entretanto existem algumas peculiaridades que caracterizam as diferenças. Sintaxe para declaração reestruturações compostas: <CompRestruct> ::= “composed” “restructuring” <Identifier> “{” <RestructVarSet> <RestructMethSet> “}” Uma reestruturação composta se assemelha a uma declaração de classe. Qualquer reestruturação, seja primitiva ou composta, pode ser instanciada, isto é, reestruturações são consideradas objetos Java. Podem ser definidas variáveis em uma reestruturação composta, as quais podem se referir a qualquer objeto Java, inclusive um objeto de reestruturação. A diferença ocorre em relação às declarações de campos com tipos de estruturas sintáticas e campos com tipos de reestruturações. Os tipos e os identificadores devem ser prefixados com o símbolo # para facilitar o préprocessamento. Toda reestruturação composta deve definir um método void execute() que processa efetivamente a reestruturação. Podem ser definidos métodos auxiliares ao método execute(). As regras de escopo de declaração de variáveis e métodos são idênticas às regras de Java. As declarações variáveis podem ser prefixadas com o modificador @extern. Isto significa que o valor desta variável deverá ser atualizado externamente em outra reestruturação composta ou atualizado pelo ambiente de execução caso seu valor seja nulo. Note que aqui o modificador @extern tem a mesma semântica que nas reestruturações primitivas. Na Figura 7 do Apêndice 1 apresentamos uma reestruturação composta. A primeira variável declarada coveredFiles é simplesmente uma enumeração que deve ser fornecida externamente. Note que quando se define uma reestruturação é responsabilidade do usuário definir como os efeitos colaterais produzidos por uma modificação devem ser controlados. Por exemplo, a remoção de um campo de uma classe implica na alteração de todas as suas classes dependentes. A definição de classes dependentes não pode ser inferida automaticamente pois depende da definição de um espaço de classes. No exemplo apresentado o usuário definirá quais são os arquivos que deverão ser analisados para controlar os efeitos colaterais relativos à remoção de um campo. Estes arquivos são passados para a reestruturação através da variável externa coveredFiles. Se, por exemplo, a reestruturação FieldMovement for utilizada em outra reestruturação composta, esta variável pode ser atualizada na respectiva reestruturação. Caso a reestruturação FieldMovement seja executada diretamente, as variáveis externas devem ser atualizadas pelo ambiente de execução, seja, por exemplo, com a intervenção do usuário através de diálogos, ou através de variáveis de sistemas. Note que isto não depende da linguagem, mas sim da implementação do ambiente de execução. As outras variáveis externas do exemplo são sourceFile e targetFile que correspondem, respectivamente, ao arquivo que contém o campo de uma determinada classe que será movido, e ao arquivo que contém a classe que receberá o respectivo campo. A reestruturação composta FieldMovement instancia reestruturações primitivas para descrever toda a restruturação. Note que os tipos utilizados para instanciar as reestruturações são definidos através dos seus respectivos nomes. Note, ainda, que existem duas instâncias da reestruturação primitiva ClassFieldAddition, o que ilustra a inserção, na classe destino, de um campo correspondente ao campo sendo movido, e da possível inserção de um campo na classe origem para referir-se ao campo movido. O método execute() processa a transformação. Note que o método é escrito de tal forma que sua estrutura é completamente correspondente à mecânica da refabricação MoveField apresentada em [Fowler 1999], o que mostra que a boa legibilidade da reestruturação é uma característica evidente. 5. Aspectos de Implementação Como foi visto na seção anterior, JPearl é dividida em suas sublinguagens: JPearlRL e JPearl-CL. A linguagem JPearl-RL é interpretada. A partir de um arquivo de entrada e de uma reestruturação ocorre a interpretação as regras de reestruturação descritas. O processo de interpretação tem a capacidade de desviar o seu fluxo de execução para os componentes previamente compilados da transformação (ações e precondição). O processo de interpretação funciona da seguinte maneira: 1) Inicialmente, o interpretador executa uma análise do código do arquivo de entrada gerando sua árvore de sintaxe abstrata. O mesmo é feito para a regra de casamento. 2) Após terminar a análise da regra de casamento, são executadas as ações relativas a esta regra. 3) Depois disso, as árvores são comparadas enquanto as variáveis de reestruturação são casadas com as sub-árvores da árvore de sintaxe abstrata do arquivo de entrada. 4) Se o casamento não for possível o processo de reestruturação é cancelado. 5) Se o casamento for realizado com sucesso, as precondições são verificadas e caso sejam verdadeiras, a árvore de sintaxe para o arquivo de saída é montada a partir das definições da regra de saída. Finalmente, uma Pretty Printer imprime a árvore de saída para um arquivo. Diferentemente de JPearl-RL, a linguagem JPearl-CL é pré-processada. O préprocessador gera classes para definições escritas em linguagem JPearl-CL. Ao ser aplicado em uma reestruturação composta, ação ou precondição o préprocessador constrói classes contendo o código descrito na definição destes componentes substituindo as ocorrências de construções JPearl-CL por construções Java que denotam as respectivas construções JPearl-CL. A seguir mostramos a correspondência entre os elementos de JPearl e da saída gerada pelo pré-processador: • As ações são transformadas em classes que encapsulam os respectivos métodos. • Uma transformação composta é transformada em uma classe contendo as Reestruturação.JPearl Regra de Casamento JPearl-RL Ações da Regra de Casamento JPearl-CL Precondições JPearl-CL Ações da Regra de Saída JPearl-CL Ações das Precondições JPearl-CL Regra de Saída JPearl-RL Pré-processador JPearl-CL Arquivo1.java Arquivo2.java ArquivoN.java JAVAC JAR Pré-Processamento / Compilação de JPearl-CL Interpretação de JPearl-RL Reestruturação.jar Regra de Casamento Precondições Ações da Regra de Casamento Ações das Precondições Casamento Ações da Regra de Saída Regra de Saída Verificação de Precondições AST AST Parser de Casamento Casamento Programa / Regra Parser de Programa Java Resulltado AST AST Arquivo de Entrada Java Construção do Arquivo de Saída AST Pretty Printer Arquivo de Saída Java Figura 1: Diagrama do Sistema de Compilação / Execução mesmas propriedades as quais também são pré-processadas. • Variáveis auxiliares cujos tipos tem seus correspondentes em Java não sofrem alteração. • Variáveis cujos tipos são correspondentes aos nodos sintáticos continuam com o respectivo tipo do nodo sintático definido pelo parser do arquivo de entrada. • Variáveis cujos tipos correspondem a uma transformação primitiva passam a ser de um tipo de uma classe interna do sistema chamada Restructuring. Esta classe funciona como um container para as variáveis da reestruturação, armazenadas em uma HashTable, para os parâmetros da reestruturação, para os localizadores dos elementos da reestruturação. • Os métodos das reestruturações continuam os mesmos. As ocorrências das declarações e acessos às variáveis dentro dos mesmos são alteradas conforme as regras acima. • São criados métodos get/set para as variáveis externas. Depois de geradas estas classes são compiladas e empacotadas, em um jarfile, juntamente com as regras JPearl-RL. A visão geral do sistema de execução descrito acima pode ser visto na Figura 1. 6. Conclusões A linguagem JPearl-CL foi concebida de forma a permitir que qualquer classe Java pudesse ser utilizada na descrição das reestruturações. Isto praticamente liberta o programador de qualquer restrição que uma linguagem específica pudesse impor. Por outro lado, poderia se argumentar que a linguagem não oferece nenhuma vantagem em relação a Java. Na verdade, a vantagem de se utilizar JPearl ao invés de Java é que a a integração JPearl-RL e JPearl-CL oferece um ambiente integrado para especificar regras casamentos e reescrita com ações semânticas complexas sem obrigar o usuário a lidar com gerador de compilador do tipo antlr ou javacc. Em outras palavras, é oferecida uma alternativa sob medida para o problema de definição de refabricações. Como conseqüência desta liberdade, pode-se observar, por exemplo, que o problema da cobertura dos arquivos afetados por uma reestruturação pode ser resolvido pelo próprio usuário de JPearl da maneira mais conveniente. Certamente, são oferecidas ferramentas para auxiliá-lo, porém isto não é uma característica intrínsica da linguagem. Outra conseqüência é que a API fornecida para os diferentes nodos sintáticos da linguagem pode ser suficientemente rica para facilitar a definição de reestruturações complexas, contendo, por exemplo, os métodos getSubClasses, setFieldAccesses, setFieldAssignments, que são métodos associados a um tipo particular de nodo sintático, os quais ajudam a manipular amigavelmente alterações dependentes de contexto, e que foram utilizados na reestruturação apresentada na Figura 7 do Apêndice 1. Em princípio, os nodos até podem ser estendidos via herança pelo usuário de tal forma que novas funcionalidades sejam acrescentadas por eles próprios. Em reestruturações compostas, as variáveis externas são definidas pelo conjunto de variáveis externas definidas na própria transformação mais o conjunto de variáveis externas das reestruturações agregadas. As variáveis externas não estão amarradas a nenhum tipo prévio de atualização. Elas podem ser atualizadas por uma reestruturação mais geral, através de intervençào do usuário disparada pelo ambiente de execução, ou até mesmo por acesso a variáveis de ambiente. O fato é que isto não é intrínsico à linguagem, mas sim dependente da implementação do ambiente de execução. Se por um lado isto pode dar margem a diferentes implementações de um mesmo conceito, por outro lado, no estágio atual de desenvolvimento da linguagem, isto permite uma maior flexibilidade para experimentação de mecanismos alternativos. A ordem imposta na apresentação das seções de uma transformação pode parecer uma arbitrariedade desnecessária, mas foi julgada importante por induzir o usuário de JPearl a entender como é o fluxo de execução da transformação. Apesar de JPearl ter sido originalmente inspirada em JaTS, e portanto existirem elementos semelhantes, a concepção das duas linguagens tem alguns princípios distintos. Enquanto as transformações em JaTS são definidas para um conjunto de tipos, em JPearl as reestruturações primitivas são definidas sobre um arquivo. Em JPearl, a descrição sintática é separada das ações contribuindo para a clareza das regras de casamento e de saída. As regras de casamento introduziram elmentos importantes como o modificador @extern e a estrutura de agregação #{{ ... }}#. Os conceitos de composição de reestruturações e flexibilidade para o usuário são elementos centrais em JPearl. A reutilização de reestruturações se mostrou bastante viável, uma vez que o projetista pode escolher onde alocar responsabilidades dentro de reestruturações compostas. Desta maneira, um projeto de reestruturações podem seguir os mesmos princípios de um projeto orientado por objetos, no que se refere à distribuição de responsabilidades e à colaboração entre as transformações. Um dos objetivos deste trabalho foi experimentar construções para JPearl com alto grau de expressividade. Isto permite considerá-la uma ferramenta útil para ser integrada em ambientes completos de desenvolvimento. Como trabalhos futuros, tem-se a necessidade de uma validação definitiva da linguagem através da descrição de todas as refabricações apresentadas em [Fowler 1999]. Além disso, o empacotamento e a integração da linguagem em ferramentas de desenvolvimento é uma tarefa importante para efetivar a distribuição da linguagem que deverá ser feita sob uma base de código-fonte aberto. Outra possibilidade de trabalho é a análise estrutural do projeto de sistemas baseados em anti-padrões e a reestruturação automática destes sistemas para adequação à padrões adequados. Além disso, a descrição formal da linguagem é outro trabalho importante a ser realizado. Referências Bibliográficas Beck, K. Extreme Programming Explained – Embrace Change, Addison Wesley, 1999. Castor, F. e Borba, P. (2001) “A Language For Specifying Java transformations”, V Simpósio Brasileiro de Linguagens de Programação, Curitiba, Brazil, p. 236-251. Castor, F., Oliveira, K., Souza, A., Santos, G., e Borba, P. (2001) “JaTS: A Java Transformation System”, XV Simpósio Brasileiro de Engenharia de Software, Rio de Janeiro, Brazil, p.374-379. Cordy, J. e Carmichael, I. The TXL Programming Language Syntax and Informal Semantics. Department of Computing and Information Science. Queen´s University at Kingston. Kinston, Canada, 1993. Felix, M. e Hausler, E. (1999) “LET: Uma Linguagem para Especificar Transformações”, III Simpósio Brasileiro de Linguagens de Programação, p. 109123, Porto Alegre, Brazil. Fowler, M. Refactoring – Improving the Design of Existing Code, Addison Wesley, 1999. Fowler, M. (2002) “Refactoring Home Page”, http://www.refactoring.com, February. Gamma, E., Helm, R., Johnson, R. e Vlissides, J. Design Patterns – Elements of Reusable Object-Oriented Software, Addison Wesley, 1995. Maia, M. e Oliveira, A. The JPearl User Guide. Relatório Técnico. Departamento de Computação. Universidade Federal de Ouro Preto, 2002, Em preparação. Apêndice 1 – Reestruturações Necessárias para a Refabricação MoveField 01 file restructuring ClassCreation 02 outputRule 03 public class @extern #Identifier: cn { 04 } Figura 2: Reestruturação ClassCreation 01 file restructuring ClassFieldSelection 02 matchRule 03 [#PackageDeclaration: pckdecl] 04 [#ImportDeclarationSet: impdeclset] 05 #TypeDeclarationSet: tds {{ 06 #TypeDeclarationSet: tds1 07 @extern #TypeDeclarationSet: tds2 {{ 08 [#ClassModifier: cm] class #ClassName: cn 09 [#ExtendClause: ec] [#ImplementsClause: ic] { 10 #ClassBodyDeclarationSet : cbds {{ 11 #ClassBodyDeclarationSet : cbds1 12 @extern #FieldDeclaration : selectedFieldDecl 13 {{#Type: t #VariableDeclarator: selectedVar;}} 14 #ClassBodyDeclarationSet : cbds2 15 }} 16 } 17 }} 18 #TypeDeclarationSet: tds3 19 }} Figura 3: Reestruturação ClassFieldSelection 01 file restructuring ClassFieldExtraction 02 matchRule 03 <#include ClassFieldSelection.matchRule> 04 outputRule 05 #pckdecl 06 #impdeclset 07 #tds1 08 #cm class #cn #ec # ic { 09 #cbds1 10 #cbds2 11 } 12 # tds3 Figura 4: Reestruturação ClassFieldExtraction 01 file restructuring ClassFieldAddition 02 matchRule 03 [#PackageDeclaration: pckdecl] 04 [#ImportDeclarationSet: impdeclset] 05 #TypeDeclarationSet: tds {{ 06 #TypeDeclarationSet: tds1 07 @extern #TypeDeclarationSet: tds2 {{ 08 [#ClassModifier: cm] class #ClassName: cn 09 [#ExtendClause: ec] [#ImplementsClause: ic] { 10 #ClassBodyDeclarationSet:cbds 11 } 12 }} 13 #TypeDeclarationSet: tds3 14 }} 15 outputRule 16 #pckdecl 17 #impdeclset 18 #cm class #cn #ec #ic { 19 #FieldDeclaration : newFieldDecl 20 {{#Type: t #VariableDeclarator: insertedVar}}; 21 #cbds 22 #MethodDeclaration:mdgetter 23 #MethodDeclaration:mdgetter 24 } 25 #tds3 26 outputRuleActions 27 void execute () { 28 #MethodDeclaration #mdgetter = new #MethodDeclaration(); 29 #mdgetter.setResultType(#newFieldDecl.getType()); 30 #mdgetter.setMethodName(“get”+#newFieldDecl.getFieldName()); 31 #mdsetter = new #MethodDeclaration(); 32 #mdgetter.setResultType(“void”); 33 #mdgetter.setMethodName(“set”+#newFieldDecl.getFieldName()); 34 #mdgetter.addParameter(#newFieldDecl.getType(), 35 #newFieldDecl.getFieldName().substring(0,0)); 36 } Figura 5: Reestruturação ClassFieldAddition 01 file restructuring FieldSelfEncapsulation { 02 matchRule 03 <#include ClassFieldSelection.matchRule> 04 preconditionActions 05 #MethodDeclaration #mdgetter = new #MethodDeclaration(); 06 #MethodDeclaration #mdsetter = new #MethodDeclaration(); 07 void execute () { 08 #mdgetter.setResultType(#selected_fd.getType()); 09 #mdgetter.setMethodName(“get”+#selectedField.getFieldName()); 10 #mdsetter.setResultType(“void”); 11 #mdsetter.setMethodName(“set”+#selectedField.getFieldName()); 12 #mdsetter.addParameter(#selected_fd.getType(), 13 #selected_fd.getFieldName().substring(0,0)); 14 } 15 precondition 16 !(#cbds1.existsMethodDeclaration(#mdgetter) || 17 #cbds1.existsMethodDeclaration(#mdsetter) || 18 #cbds2.existsMethodDeclaration(#mdgetter) || 19 #cbds2.existsMethodDeclaration(#mdsetter)) 20 outputRule 21 #pckdecl 22 #impdeclset 23 #tds1 24 #cm class #cn #ec #ic { 25 #cbds1 26 #selected_fd 27 #cbds2 28 #mdgetter 29 #mdsetter 30 } 31 #tds3 32 outputRuleActions 33 void execute () { 34 #mdgetter.setBlock(“return “+#selectedField.getFieldName()+”;”); 35 #mdsetter.setBlock(#selectedField.getFieldName()+”=”+ 36 #selectedField.getFieldName().substring(0,0)); 37 #Expression #mdgetterCall = new #Expression(); 38 #mdgetterCall.setExpression(“get”+#selectedField.getFieldName()+”();”); 39 #cbds.setFieldAccesses(#selectedField, #mdsetterCall); 40 41 #Expression #mdsetterCall = new #PrimaryExpression(); 42 #mdsetterCall.setPrimaryPrefixName(“set”+#selected_fd.getFieldName()); 43 #cbds.setFieldAssignments(#selectedField, #mdgetterCall); 44 } Figura 6: Reestruturação FieldSelfEncapsulation 01 composed restructuring FieldMovement { 02 @extern Enumeration coveredFiles; 03 @extern String sourceFile, targetFile; 04 #ClassDeclaration #sourceClass, #targetClass, #sourceSubClass; 05 FieldSelfEncapsulation fse = new FieldSelfEncapsulation(); 06 ClassFieldAddition cfa = new ClassFieldAddition(); 07 ClassFieldSelection cfs = new ClassFieldSelection(); 08 ClassFieldAddition cfa2 = new ClassFieldAddition(); 09 #Expression #mdgetterCall = new #Expression(); 10 #Expression #mdsetterCall = new #PrimaryExpression(); 11 void execute () { 12 13 // Self Encapsulate the Field to be moved in the source class 14 fse.setFile(sourceFile); 15 fse.setExternVar(#tds2, #sourceClass); 16 fse.execute(); 17 #sourceClass = fse.getVar(“#tds2”); 18 19 // Add the field in the target class 20 cfa.setFile(targetFile); 21 cfa.setExternVar(#tds2, #targetClass); 22 cfa.setVar(“#t”, fse.getVar(“#t”)); // Name will be set externally 23 cfa.execute(); 24 #targetClass = cfa.getVar(“#tds2”); 25 26 // Replace references of source field to target object in the source class 27 replaceReferences (sourceFile, #sourceClass, #targetClass); 28 29 // Verify for occurrences on subclasses 30 if (! fse.getVar(“#selectedFieldDecl”).isPrivate()) { 31 while (coveredFiles.hasMoreElements()) { 32 String subClassSource = coveredFiles.nextElement(); 33 while (#sourceClass.getSubClasses(subClassSource).hasMoreSubClasses()) { 34 #sourceSubClass = 35 #sourceClass.getSubClasses(subClassSource).nextSubClass(); 36 replaceReferences(subClassSource, #sourceSubClass, #targetClass); 37 } 38 } 39 40 // Remove the field on the source class 41 ClassFieldExtraction cfe = new ClassFieldExtraction(); 42 cfe.setFile(sourceFile); 43 cfe.setExternVar(“#selectedFieldDecl”, fse.getVar(“#selectedFieldDecl”)); 44 cfe.execute(); 45 46 } 47 48 // Replace references of source field to target object in the source class 49 replaceReferences (String sourceFile, #ClassDeclaration source, target) { 50 if (#sourceClass.containsFieldDeclaration(#targetClass.getName()) { 51 // Select the field to act as reference 52 cfs.setFile(sourceFile); 53 cfs.setExternVar(“#tds2”, #source); 54 cfs.setExternVar(“#t”, #target.getName()); 55 cfs.execute(); 56 // Set the accesses and assignments 57 #mdgetterCall.setExpression(cfs.getVar(“selectedVar”).getName()+”.”+ 58 cfa.getVar(“#mdgetter”.getMethodName()+”()”); 59 #mdsetterCall.setPrimaryPrefixName(cfs.getVar(“selectedVar”).getName()); 60 #mdsetterCall.addPrimarySuffix(“.”+ 61 cfa.getVar(“#mdsetter”.getMethodName()); 62 } 63 else { 64 // Create a field to act as reference 65 cfa2.setFile(sourceFile); 66 cfa2.setExternVar(“#tds2”, #source); 67 cfa2.setVar(“#t”, #target.getName()); 68 // Field name defined externally 69 cfa2.execute(); // Field name has been defined 70 #mdgetterCall.setExpression(cfa2.getVar(“#insertedVar”).getName()+”.”+ 71 cfa2.getVar(“#mdgetter”.getMethodName()+”()”); 72 #mdsetterCall.setPrimaryPrefixName(cfs.getVar( 73 “#selectedVar”).getName()); 74 #mdsetterCall.addPrimarySuffix(“.”+ 75 cfa.getVar(“#mdsetter”.getMethodName()); 76 } 77 cfs.getVar(“#cbds”).setFieldAccesses(cfs.getVar(“#selectedVar”), 78 #mdsetterCall); 79 cfs.getVar(“#cbds”).setFieldAssignments(fse.getVar(“#selectedField”, 80 #mdgetterCall); // The assignment of the arguments is 81 // responsibility of the called method 82 } Figura 7. Reestruturação FieldMovement