Avançar para a área de conteúdo

ir para o conteúdo principal

developerWorks Brasil  >  Tecnologia Java | Software livre  >

Practically Groovy: Metaprogramação com encerramentos, ExpandoMetaClass e categorias

Adicione métodos onde quiser, quando quiser

developerWorks
Opções de documento

Opções de documento que necessitam de JavaScript não são exibidas

Código de amostra


Classificar esta página

Ajude-nos a melhorar este conteúdo


Nível: Introdutório

Scott Davis, Senior IT Architect, IBM, Software Group

23/Jun/2009

Enter into the world of metaprogramming, Groovy-style. A capacidade de adicionar novos métodos a classes dinamicamente em tempo de execução — mesmo classes Java™, e até mesmo classes Java finais — é incrivelmente poderosa. Independente de ser usado para código de produção, testes de unidade, ou algo entremeio, as capacidades de metaprogramação de Groovy devem provocar a curiosidade até do mais cansado programador de Java.

Você ouviu falar por anos que o Groovy é uma linguagem de programação dinâmica para o JVM. Mas o que isso realmente significa? Nesta parte do Practically Groovy , você aprenderá sobre metaprogramação — da capacidade do Groovy de adicionar métodos a classes dinamicamente em tempo de execução. Esta flexibilidade vai além do que a linguagem Java padrão pode oferecer. Através de uma série de exemplos de códigos (todos disponíveis para download) você verá que a metaprogramação é um dos recursos mais poderosos e práticos do Groovy.

Modelando o mundo

Nosso trabalho como programadores é modelar o mundo real em software. Quando o mundo real é gentil o bastante e oferece até um simples domínio — animais com escamas ou penas põem ovos, animais com pêlos têm nascidos vivos — é fácil generalizar o comportamento em software, como demonstrado na Listagem 1:


Listagem 1. Modelando animais no Groovy
        
class ScalyOrFeatheryAnimal{
  ScalyOrFeatheryAnimal layEgg(){
    return new ScalyOrFeatheryAnimal()
  }
}

class FurryAnimal{
  FurryAnimal giveBirth(){
    return new FurryAnimal()
  }
} 
      

Sobre esta série

Groovy é uma linguagem de programação moderna executada na plataforma Java. Ele oferece integração perfeita com código Java existente enquanto introduz novos recursos drásticos como encerramentos e metaprogramação. Simplificando, o Groovy é o que seria a linguagem Java se ela tivesse sido escrita no século 21.

A chave para incorporar qualquer nova ferramenta em nosso kit de ferramentas de desenvolvimento é saber quando usá-la e quando deixá-la na caixa. O Groovy pode ser extremamente poderoso, mas somente quando aplicado adequadamente aos cenários apropriados. Para tanto, a série Practically Groovy explora os usos práticos do Groovy, ajudando você a aprender quando e como aplicá-los com sucesso.

Infelizmente, o mundo real está cheio de exceções e casos extremos — ornitorrincos com bico de pato são peludos e põem ovos. É quase como se cada um de nós considerasse cuidadosamente abstrações de software sendo direcionadas por uma equipe dedicada de ninjas opositores.

Se a linguagem de software que você utiliza para modelar o domínio for muito rígida ao lidar com exceções inevitáveis, você pode acabar soando como um funcionário público obstinado envolto por burocracia insignificante — "Desculpe, Sra. Ornitorrinco, mas a senhora vai dar a luz a um jovem vivo se quiser ser rastreada pelo nosso sistema."

Por outro lado, uma linguagem dinâmica como o Groovy oferece a flexibilidade de adaptar seu software para modelar o mundo real de forma mais precisa, ao invés de fazê-lo de forma presunçosa (e fútil), pedindo concessões ao mundo. Se a classe dos Ornitorrincos precisar de um método layEgg(), o Groovy possibilita isso, como demonstrado na Listagem 2:


Listagem 2. Adicionando um método layEgg() dinamicamente
        
Platypus.metaClass.layEgg = {->
  return new FurryAnimal()
}
      

Se toda esta conversa sobre animais peludos e ovos lhe pareceu vã, então considere a rigidez de uma das classes mais frequentemente utilizadas na linguagem Java: a Cadeia de caractere.



Voltar para parte superior


Os novos métodos do Groovy em java.lang.String

Uma das alegrias de se trabalhar com o Groovy são todos os novos métodos que ele adiciona ao java.lang.String. Métodos como padRight() e reverse() oferecem simples transformações de Cadeia de caractere, como demonstrado na Listagem 3. (Para um link para a lista GDK de todos os novos métodos adicionados à Cadeia de caractere, consulte Recursos. Como o GDK diz descaradamente em sua página inicial, "Este documento descreve os métodos adicionados ao JDK para torná-lo mais maneiro.")


Listagem 3. Métodos adicionados à Cadeia de caractere pelo Groovy
        
println "Introduction".padRight(15, ".")
println "Introduction".reverse()

//output
Introduction...
noitcudortnI
      

Mas as adições à Cadeia de caractere não terminam com simples truques. Se a Cadeia de caractere for uma URL bem formada, em uma única linha você pode transformar aquela Cadeia de caractere em uma java.net.URL e retornar os resultados de uma solicitação HTTP GET, como demonstrado na Listagem 4:


Listagem 4. Fazendo uma solicitação HTTP
println "http://thirstyhead.com".toURL().text

//output
<html>
  <head>
    <title>ThirstyHead: Training done right.</title>
<!-- snip -->

Para outro exemplo, executar um comando shell local é tão fácil quanto fazer uma chamada de rede remota. Normalmente, eu digitaria ifconfig en0 no prompt de comandos para verificar as configurações TCP/IP da minha placa de rede. (Se você utiliza Windows® ao invés de Mac OS X ou Linux®, experimente ipconfig.) No Groovy, eu posso fazer a mesma coisa programaticamente, como demonstrado na Listagem 5:


Listagem 5. Realizando um comando shell no Groovy
        
        
println "ifconfig en0".execute().text

//output
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	ether 00:17:f2:cb:bc:6b
	media: autoselect status: inactive
  //snip

      

Não estou sugerindo que a alegria do Groovy é que você não pode fazer as mesmas coisas na linguagem Java. Você pode. A alegria é que esses métodos têm sido adicionados sem problemas diretamente na classe Cadeia de caractere — o que não significa uma façanha, pois a Cadeia de caractere é uma classe final. (Mais sobre isso em breve.) A listagem 6 mostra o String.execute().text equivalente Java:


Listagem 6. Realizando um comando shell na linguagem Java
        

Process p = new ProcessBuilder("ifconfig", "en0").start();
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = br.readLine();
while(line != null){
  System.out.println(line);
  line = br.readLine();
}

      

É como se fosse empurrado de um guichê para outro no Departamento de Trânsito, não é? "Sinto muito, Sr., mas para ver a Cadeia de caractere que o senhor solicitou, primeiro o senhor precisa ficar naquela fila para obter um BufferedReader."

Sim, você pode construir métodos de conveniência e classes de utilitários para ajudar a abstrair aquela feiúra, mas todas aquelas soluções alternativas com.mycompany.StringUtil no mundo são um substituto fraco para adicionar o método diretamente aonde ele pertence: a classe de Cadeia de caractere. (Platypus.layEgg(), de fato!)

Então, como o Groovy faz isso exatamente — prende novos métodos em classes que não poderiam ser diretamente ampliadas ou modificadas? Para compreender, você precisa saber sobre os encerramentos e ExpandoMetaClass.



Voltar para parte superior


Encerramentos e ExpandoMetaClass

O Groovy oferece um recurso de linguagem inócuo, mas poderoso — encerramentos — sem o qual o Ornitorrinco jamais conseguiria por um ovo. Um encerramento é, simplificadamente, um pedaço nomeado de código executável. É um método sem uma classe circundante. A listagem 7 demonstra um encerramento simples:


Listagem 7. Encerramento simples
        

def shout = {src->
  return src.toUpperCase()
}

println shout("Hello World")

//output
HELLO WORLD

      

É muito legal ter métodos independentes, mas não tão legal quanto ter a capacidade de prender esses métodos em classes existentes. Considere o código na Listagem 8, onde ao invés de criar um método que aceita uma Cadeia de caractere como parâmetro, eu adiciono o método diretamente à classe da Cadeia de caractere:


Listagem 8. Adicionando o método shout à Cadeia de caractere
        
String.metaClass.shout = {->
  return delegate.toUpperCase()
}

println "Hello MetaProgramming".shout()

//output
HELLO METAPROGRAMMING
      

O encerramento shout() sem argumento é adicionado ao ExpandoMetaClass (EMC) da Cadeia de caractere. Cada classe — Java e Groovy — é cercada por um EMC que intercepta chamadas de método. Isso significa que embora a Cadeia de caractere seja final, podem-se adicionar métodos ao seu EMC. Como resultado, agora para o observador casual é como se a Cadeia de caractere tivesse um método shout().

Como esse tipo de relação não existe na linguagem Java, o Groovy teve que introduzir um novo conceito: delegados. O delegado é a classe cercada pelo EMC.

Sabendo que as chamadas de método ocorrem primeiro no EMC e depois no delegado, você pode fazer todo tipo de coisas interessantes. Por exemplo, observe como a Listagem 9 na verdade redefine o método toUpperCase() na Cadeia de caractere:


Listagem 9. Redefinindo o método toUpperCase()
        
String.metaClass.shout = {->
  return delegate.toUpperCase()
}

String.metaClass.toUpperCase = {->
  return delegate.toLowerCase()
}

println "Hello MetaProgramming".shout()


//output
hello metaprogramming
      

Novamente, isso pode parecer frívolo (ou até mesmo perigoso!). Embora seja provável que se precise mudar pouca coisa no comportamento na vida real do método toUpperCase(), você consegue imaginar o quanto isso é benéfico para testar a unidade de seu código? A metaprogramação oferece uma forma rápida e fácil de realizar determinismo comportamental potencialmente aleatório. Por exemplo, a listagem 10 demonstra a substituição do método random() estático da classe Matemática:


Listagem 10. Substituição do método Math.random()
        
println "Before metaprogramming"
3.times{
  println Math.random()
}

Math.metaClass.static.random = {->
  return 0.5
}

println "After metaprogramming"
3.times{
  println Math.random()
}

//output
Before metaprogramming
0.3452
0.9412
0.2932
After metaprogramming
0.5
0.5
0.5
      

Agora imagine tentar testar a unidade de uma classe que realiza uma chamada SOAP dispendiosa. Não é necessário criar uma interface e extinguir todo o mock object — é possível substituir estrategicamente o método e retornar uma resposta simples simulada. (Você verá exemplos de utilização do Groovy para teste de unidade e simulação na próxima seção.)

A metaprogramação Groovy é um fenômeno em tempo de execução — dura o mesmo tempo que o programa estiver ativo e em execução. Mas e se quiser que sua metaprogramação seja mais limitada (especialmente importante ao escrever testes de unidade)? Na próxima seção, você aprenderá como delimitar a mágica da metaprogramação.



Voltar para parte superior


Delimitando sua metaprogramação

A listagem 11 quebra o código demo que eu escrevi em um GroovyTestCase para que eu possa começar a testar a saída um pouco mais rigorosamente. (Ver "Practically Groovy: Unit test your Java code faster with Groovy" para mais informações sobre o trabalho com o GroovyTestCase.)


Listagem 11. Explorando a metaprogramação com um teste de unidade
        
        
class MetaTest extends GroovyTestCase{

  void testExpandoMetaClass(){
    String message = "Hello"
    shouldFail(groovy.lang.MissingMethodException){
      message.shout()
    }

    String.metaClass.shout = {->
      delegate.toUpperCase()
    }

    assertEquals "HELLO", message.shout()

    String.metaClass = null
    shouldFail{
      message.shout()
    }
  }
}

      

Digite groovy MetaTest no prompt de comandos para executar este teste.

Observe que você pode desfazer a metaprogramação simplesmente configurando String.metaClass para nulo.

Mas e se você não quiser que o método shout() apareça em todas as Cadeias de caractere? Bem, você pode simplesmente ajustar o EMC em uma instância única ao invés da classe, como demonstrado na Listagem 12:


Listagem 12. Metaprogramação de instância única
        

void testInstance(){
  String message = "Hola"
  message.metaClass.shout = {->
    delegate.toUpperCase()
  }

  assertEquals "HOLA", message.shout()
  shouldFail{
    "Adios".shout()
  }
}

      

Se você for adicionar ou substituir vários métodos de uma vez, a Listagem 13 mostra como definir os novos métodos em massa:


Listagem 13. Metaprogramação de muitos métodos de uma vez
        

void testFile(){
  File f = new File("nonexistent.file")
  f.metaClass{
    exists{-> true}
    getAbsolutePath{-> "/opt/some/dir/${delegate.name}"}
    isFile{-> true}
    getText{-> "This is the text of my file."}
  }

  assertTrue f.exists()
  assertTrue f.isFile()
  assertEquals "/opt/some/dir/nonexistent.file", f.absolutePath
  assertTrue f.text.startsWith("This is")
}

      

Observe que eu não me importo mais se o arquivo realmente existe no sistema de arquivos. Eu circulá-lo para outras classes neste teste de unidade e ele se comportará como se fosse um arquivo real. Quando a variável f sair do escopo no final deste teste, o comportamento personalizado também o fará.

Enquanto o ExpandoMetaClass for indiscutivelmente poderoso, o Groovy oferece uma segunda abordagem à metaprogramação com seu conjunto exclusivo de recursos: categorias.



Voltar para parte superior


Categorias e o bloco de uso

A melhor forma de explicar uma Categoria é vê-la em ação. A listagem 14 demonstra o uso de uma Categoria para utilizar um método shout() em uma Cadeia de caractere:


Listagem 14. Utilizando uma Categoria para metaprogramação
        
class MetaTest extends GroovyTestCase{
  void testCategory(){
    String message = "Hello"
    use(StringHelper){
      assertEquals "HELLO", message.shout()
      assertEquals "GOODBYE", "goodbye".shout()
    }

    shouldFail{
      message.shout()
      "foo".shout()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}
      

Se você já tiver realizado qualquer desenvolvimento Objective-C, esta técnica deve parecer familiar. A Categoria StringHelper é uma classe normal — ela não precisa estender uma classe-pai especial ou implementar uma interface especial. Para adicionar novos métodos a uma classe particular do tipo T, é necessário apenas definir métodos estáticos que aceitam o tipo T como primeiro parâmetro. Como o shout() é um método estático que vai em uma Cadeia de caractere como primeiro parâmetro, todas as Cadeias de caractere envoltas em um bloco de uso obtêm um método shout().

Então, quando você escolheria uma Categoria sobre um EMC? O EMC permite que você adicione métodos em uma única instância ou todas as instâncias de uma classe particular. Como você pode ver, definir uma Categoria permite que você adicione métodos a algumas instâncias — somente aquelas dentro de um bloco de uso.

Enquanto um EMC permite a definição de um novo comportamento dinamicamente, uma Categoria permite que você salve o comportamento em um arquivo de classe separada. Isso significa que você pode utilizá-lo em uma série de circunstâncias diferentes: testes de unidade, código de produção, e assim por diante. O gasto adicional de definir classes separadas tem seu retorno em termos de reutilização.

A listagem 15 demonstra o uso de StringHelper e de um FileHelper recentemente criado no mesmo bloco de uso:


Listagem 15. Utilizando várias categorias em um bloco de uso
        
class MetaTest extends GroovyTestCase{
  void testFileWithCategory(){
    File f = new File("iDoNotExist.txt")
    use(FileHelper, StringHelper){
      assertTrue f.exists()
      assertTrue f.isFile()
      assertEquals "/opt/some/dir/iDoNotExist.txt", f.absolutePath
      assertTrue f.text.startsWith("This is")

      assertTrue f.text.shout().startsWith("THIS IS")
    }

    assertFalse f.exists()
    shouldFail(java.io.FileNotFoundException){
      f.text
    }
  }
}


class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}


class FileHelper{
 static boolean exists(File f){
   return true
 }

 static String getAbsolutePath(File f){
   return "/opt/some/dir/${f.name}"
 }

 static boolean isFile(File f){
   return true
 }

 static String getText(File f){
   return "This is the text of my file."
 }
} 
      

Mas o aspecto mais interessante das categorias é como elas são implementadas. Os EMCs requerem o uso de encerramentos, o que significa que só é possível implementá-los no Groovy. Como as categorias não são nada mais que classes com métodos estáticos, elas podem ser definidas em código Java. Na verdade, é possível reutilizar classes Java existentes — as que nunca foram expressamente destinadas à metaprogramação — no Groovy.

A listagem 16 demonstra o uso de classes do pacote Jakarta Commons Lang (ver Recursos) para metaprogramação. Todos os métodos no org.apache.commons.lang.StringUtils coincidentemente seguem o padrão Categoria de — métodos estáticos que aceitam uma Cadeia de caractere como primeiro parâmetro. Isso significa que você pode usar a classe StringUtils direto da caixa como uma Categoria.


Listagem 16. Utilizando uma classe Java para metaprogramação
        

import org.apache.commons.lang.StringUtils

class CommonsTest extends GroovyTestCase{
  void testStringUtils(){
    def word = "Introduction"

    word.metaClass.whisper = {->
      delegate.toLowerCase()
    }

    use(StringUtils, StringHelper){
      //from org.apache.commons.lang.StringUtils
      assertEquals "Intro...", word.abbreviate(8)

      //from the StringHelper Category
      assertEquals "INTRODUCTION", word.shout()

      //from the word.metaClass
      assertEquals "introduction", word.whisper()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}

      

Tipo groovy -cp /jars/commons-lang-2.4.jar:. CommonsTest.groovy para executar o teste. (Obviamente, você precisa mudar o caminho para onde você salvou o JAR no seu sistema.)



Voltar para parte superior


Metaprogramação e REST

Somente para ter certeza de que não fique uma impressão errada de que a metaprogramação é útil apenas para teste de unidade, aqui vai um exemplo final. Recorde o RESTful Yahoo! Serviço da Web para condições climáticas atuais discutidas no "Practically Groovy: Building, parsing, and slurping XML." Combine as habilidades do XmlSlurper retiradas daquele artigo com as habilidades de metaprogramação deste arquivo, e verifique o clima em qualquer código postal em 10 linhas de código, como demonstrado na Listagem 17:


Listagem 17. Adicionando um método de clima
        
import org.apache.commons.lang.StringUtils

class CommonsTest extends GroovyTestCase{
  void testStringUtils(){
    def word = "Introduction"

    word.metaClass.whisper = {->
      delegate.toLowerCase()
    }

    use(StringUtils, StringHelper){
      //from org.apache.commons.lang.StringUtils
      assertEquals "Intro...", word.abbreviate(8)

      //from the StringHelper Category
      assertEquals "INTRODUCTION", word.shout()

      //from the word.metaClass
      assertEquals "introduction", word.whisper()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
} 
      

Como se pode ver, a metaprogramação é uma questão de flexibilidade extrema. É possível utilizar qualquer (ou todas) técnica estruturada neste artigo para adicionar métodos facilmente a uma, algumas, ou todas as classes desejadas.



Voltar para parte superior


Conclusão

Pedir ao mundo que se restrinja às limitações arbitrárias de sua linguagem simplesmente não é uma opção realística. Modelar o mundo real em software significa que é necessária uma ferramenta flexível o suficiente para lidar com todos os casos extremos. Felizmente, com os encerramentos do Groovy, ExpandoMetaClasses e categorias, um conjunto de ferramentas afiado é disponibilizado para adicionar comportamento onde e quando necessário.

Da próxima vez, revisitarei o poder do Groovy para teste de unidade. Há benefícios reais em escrever testes em Groovy, seja GroovyTestCase ou um caso de teste JUnit 4.x com anotações. Também é possível ver o GMock em ação — uma estrutura de simulação escrita em Groovy. Até lá, espero que muitos usos práticos sejam encontrados para o Groovy.




Voltar para parte superior


Download

DescriçãoNomeTamanhoMétodo de download
Source code for the article examplesj-pg06239.zip7KBHTTP
Informações sobre métodos de download


Recursos

Aprender
  • Groovy: Saiba mais sobre o Groovy no Web site do projeto.

  • Cadeia de caractere : Consulte a Especificação Groovy JDK API para todos os outros métodos adicionados pelo Groovy à classe Cadeia de caractere.

  • AboutGroovy.com: Acompanhe as últimas notícias sobre o Groovy e links para artigos.

  • Groovy Recipes (Scott Davis, Pragmatic Programmers, 2008): Saiba mais sobre Groovy e Grails no último livro de Scott Davis.

  • Mastering Grails : Série de Scott Davis voltada a esta plataforma baseada em Groovy para desenvolvimento de Web.

  • Navegue até a livraria de tecnologia para ver livros sobre estes e outros tópicos técnicos.

  • developerWorks Java technology zone: Encontre centenas de artigos sobre cada aspecto da programação Java.


Obter produtos e tecnologias
  • Jakarta Commons Lang: Esses utilitários de auxílio para a API java.lang incluem classes que podem ser utilizadas para metaprogramação com Groovy.

  • Groovy: Transfira o arquivo Groovy ZIP ou tarball mais recente por download.

Discutir


Sobre o autor

Scott Davis is a Senior IT Architect with the Portals, Content Management, and e-Commerce practice of IBM Global Services. He has been architecting and developing solutions for over 13 years, with the last 5 focused on Portals and Content Management systems. He holds a computer science degree from the University of Colorado at Boulder.




Avalie esta página


Reserve um instante para completar este formulário para nos ajudar a servi-lo melhor.



 


 


Não
são úteis
Extremamente
úteis
 






Voltar para parte superior


Java e todas as marcas registradas baseadas em Java são marcas registradas da Sun Microsystems, Inc. nos Estados Unidos e/ou em outros países. Outros nomes de empresas, produtos e serviços podem ser marcas registradas ou marcas de serviço de terceiros.