Uma Linguagem para Descrição de Reestruturações - ICEB-UFOP

Propaganda
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
Download