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.
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.
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.
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.
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.)
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.
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.
Download | Descrição | Nome | Tamanho | Método de download |
|---|
| Source code for the article examples | j-pg06239.zip | 7KB | HTTP |
|---|
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
|