Olá!! Primeiro post Oficial pela Lambda3! \o/
Após o feedback bacana que tivemos com o Minicurso Groovy/Grails, feito através da parceira com a Globalcode, ficou claro que teríamos espaço para falar mais do tema… e numa esquisa rápida pela net nao encontrei referências para um artigo falando sobre Metaprogramação… então tá: Fazemos nós!! (antes, confira os slides do minicurso ao final deste post)
Começo com uma introdução… e ainda vou falar de Closures e DSLs em Groovy
Ficou gigante… então espero que gostem… aguardo o feedback!!
Metaprogramação em Groovy
Paolo Perrota, em seu livro Metaprogramming in Ruby, descreve este termo de uma maneira bastante simples:
Metaprogramação é escrever código que escreve código
Um exemplo: um compilador, que através do código fonte consegue gerar um arquivo em código executável.
A informação existente sobre uma classe ou objeto (quantos métodos possui, quais são seus atributos, o seu nome, etc) é chamada de meta informação. Dentro do escopo das linguagens estáticas, a única forma de alterar tal informação é recompilando a classe, para que o compilador possa fazer o trabalho de transformar o que você escreveu em código executável. O que acontece se, durante a execução do programa, você quiser modificar a implementação de um método? Ou ainda, criar os métodos capitalize, camelize,revertAndCapitalize para a classe String? Resposta simples: impossível.
Isto já não é verdade para linguagens dinâmicas. Discutiremos neste artigo como você pode utilizar técnicas de metaprogramação para construir soluções mais elegantes e escrever muito menos código. Para trazer uma idéia das possibilidades, aqui estão alguns exemplos (adaptados do livro do Paolo Perrotta):
- Imagine que você possui um problema que seria muito melhor resolver através de uma linguagens específica para o domínio. Pode ser complexo demais criar sua própria linguagem com um parser customizado. Mas é possível utilizar uma linguagem dinâmica como Groovy ou Ruby, transformando a sintaxe existente, até que se pareça com uma linguagem específica para o seu problema. Você poderá até mesmo criar seu próprio interpretador que lê o código escrito na sua linguagem.
- Digamos que você queria criar um programa em Groovy que se conecte com um sistema externo – talvez um web service ou um programa Java. Com metaprogramação, você poderá criar um wrapper que interceptará qualquer chamada de método e roteará para o sistema externo. Se alguém adicionar um novo método para o sistema externo mais tarde, não será necessário alterar o wrapper que você criou, pois todos os métodos atuais e futuros estarão cobertos de qualquer maneira.
- Você pode remover a duplicação de seu programa a um ponto que programadores java apenas sonham em alcançar. Imagine que você possua vários métodos semelhantes numa classe. Que tal definir todos esses métodos uma única vez, com apenas algumas linhas de código? Ou talvez você queira chamar uma sequencia de métodos com nomes similares, apenas selecionando-os através do padrão de similaridade; por exemplo: todos os métodos que iniciam com a palavra “test”
- APIs no mundo Java são normalmente avessas a mudanças. Por exemplo: o que acontece se quisermos alterar a classe String, a interface List ou a classe Integer? Sem criar subclasses, ampliando o acoplamento do sistema, é impossível. Pois em liguagens dinâmicas, é possível alterar as principais classes da API, reduzindo código e estando limitado apenas pela sua criatividade.
Groovy
“Groovy é uma linguagem dinâmica para a Plataforma Java com inúmeras funcionalidades que foram inspiradas em liguagens como Python, Ruby e Smaltalk, deixando-as disponíveis para desenvolvedores Java utilizando uma linguagem semelhante ao Java”
Do site http://groovy.codehaus.org
A chave principal do desenvolvimento da linguagem Groovy é: ser uma linguagem de ponta completamente integrada à plataforma Java. Apesar de ser referenciada como uma linguagem estritamente de script, Groovy possui inúmeras outras características: pode ser compilada para bytecode, integrada em aplicações Java, na construção de aplicações web, adicionar um controle maior sobre arquivos de build e ser a base principal de aplicações inteiras.
Desenvolvida para se integrar completamente na plataforma Java, Groovy já de saída oferece a integração com toda a API Java, bibliotecas de terceiros e seu próprio sistema legado, se transformando numa opção ao menos interessante para avaliar, durante a escolha de uma nova linguagem de programação. É possível instanciar e acessar suas classes Java sem a menor cerimônia, já que classes em Groovy, em tempo de execução para a Máquina Virtual, são objetos Java. Sua sintaxe semelhante ao Java vai agradar inclusive os mais puristas.
Do ponto de vista das linguagens dinâmicas, Groovy implementa o Meta Object Protocol, que dá a linguagem sua característica dinâmica. Vamos conhecer um pouco melhor esse conceito a seguir.
Meta Object Protocol (MOP)
A abordagem escolhida para disponibilizar funcionalidades de meta-programação em Groovy foi o modelo Meta Object Protocol, cujo princípio está em que as linguagens de programação devessem ser abertas e flexíveis, permitindo aos seus usuários ajustarem o design e as implementações para suas próprias necessidades. Em outras palavras, ajudando no processo de criação da linguagem.
Podemos dizer que, em uma linguagem baseada em MOP, a implementação da própria linguagem é estruturada como um programa orientado a objetos. Isso permite, com a utilização de técnicas OO, tornar a implementação da linguagem adaptável e flexível.
Metaprogramação em Groovy:
– Invocação dinâmica de métodos
– Interceptação de métodos/atributos
– Criação de métodos em tempo de execução
Classe Expando
Definir valores dinamicamente para um objeto
def ex = new Expando() ex.revistaFavorita = 'MundoJ' ex.linguagemFavorita = 'Groovy' println "$ex.revistaFavorita é uma ótima revista" println "Hoje estamos aprendendo metaprogramação em $ex.linguagemFavorita"
Após executar o código
MundoJ é uma ótima revista
Hoje estamos aprendendo metaprogramação em Groovy
De alguma maneira, a classe Expando permite que nós criemos propriedades em tempo de execução. Mais um exemplo:
def ex = new Expando() ex.helloWorld = { nome -> "Olá $nome!!" } println ex.helloWorld("Victor")
O que acontece é que, se a propriedade que estamos tentando adicionar dinamicamente for uma Closure, a classe Expando adiciona dinamicamente um novo método, neste caso helloWorld. Ao executar o metodo conseguimos ver o seguinte resultado:
Olá Victor!!
Mecanismo de Invocação dinâmica:
– invokeConstructor – intercepta construção de objeto
– invokeMethod – intercepta invocação de methodos para uma classe
– methodMissing – intercepta apenas chamadas a metodos inexistentes de uma classe
– get/setProperty – intercepta todos os acessos a atributos de uma classe
– propertyMissing – intercepta apenas chamadas a atributos inexistentes de uma classe
Implementar nossa própria versão da classe Expando é uma ótima oportunidade para conhecer um pouco mais sobre métodos de interceptação. Primeiro, vamos criar um teste que demonstra um dos comportamentos esperados: definir atributos dinamicamente:
void testDefinirPropriedadeDinamicamente() { def expando = new MeuExpando() expando.minhaPropriedade = "valor" assertEquals "valor", expando.minhaPropriedade }
O teste acima cria um objeto do tipo MeuExpando e define um attributo chamado novaPropriedade, acrescentando um valor. Por último, o teste verifica se o objeto possui realmente o atributo, isto é, se o atributo foi adicionado dinamicamente.
Para resolver este exemplo, precisamos utilizar dois métodos: setProperty e getProperty. O primero será chamado quando tentamos definir um valor para a propriedade novaPropriedade; o segundo, ao tentar acessá-la. A solução é simples: definir um Map que armazenará as propriedades definidas dinamicamente:
class MeuExpando { def atributosDinamicos = [:] void setProperty(String name, value) { atributosDinamicos[name] = value } Object getProperty(String name) { atributosDinamicos[name] } }
Proximo passo: métodos dinâmicos, isto é, definir como propriedade do objeto uma Closure. Mais um teste:
void testDefinirMetodoDinamicamente() { def expando = new MeuExpando() expando.olaMundo = { nome -> "Olá $nome!!" } assertEquals "Olá Victor!!", expando.olaMundo("Victor") }
Como o método olaMundo não existe, utilizaremos o método methodMissing para acessar possíveis Closures definidas dentro do attributo attributosDinamicos:
def methodMissing(String methodName, args) { def prop = atributosDinamicos[methodName] if(prop instanceof Closure) { return prop(*args) } else throw new MissingMethodException(methodName, delegate, args) }
methodMissing recebe o nome do método que está tentando ser acessado e os parametros que foram passados. Basta encontrar o valor dentro da lista attributosDinamicos e executar a Closure passando os argumentos. Para deixar a implementação consistente, caso se esteja tentando acessar um método que não foi definido ainda, será lançada a exceção MissingMethodException.
Tirando proveito de Metaclasses
Um dos pontos mais interessantes de Groovy é a sua capacidade de modificar o aspectos mais fundamentais da linguagem em tempo de execução. Mesmo para classes principais do Java, como a classe String, é possível moldar seu comportamento.
Imagine, por exemplo, que você deseje remover todos os acentos de uma String. Qual a opção usual utilizando Java? Simples: criar uma classe RemoveAcentos com possui um método remover (provavelmente estático) que faz o trabalho. Assim, em todos os lugares em que a funcionalidade é necessária, teremos que adicionar a chamada RemoveAcentos.remover.
A solução Groovy seria: adicionar o método removerAcentos() diretamente na classe String! Para tal, basta utilizarmos a propriedade metaClass, que é adicionada pelo compilador Groovy a todos os objetos:
String.metaClass.removerAcentos { // Seu codigo para remover acentos }
Com isso, toda instância da classe String passa a ter um método chamado removerAcentos():
"Aprendendo metaprogramação".removerAcentos()
A propriedade metaClass é uma instância da classe ExpandoMetaClass, que através de uma sintaxe simples, semelhante à definição de Closures, nos permite criar dinamicamente métodos, atributos, construtores e métodos estáticos.
Outro Exemplo
Problema: criar um CSV encoder para objetos dentro do domínio
class User { String name String email String password String phone String address } class Product { int code String name String description }
Test unitário:
void testEncodeArticleAsCSV() { def article = new Article(code: 12345, name: "Soap", description: "Bath soap") assertEquals "12345,Soap,My description", article.encodeAsCSV() }
Solução simples
def encodeAsCSV() { "$code,$name,$description" }
Para expandir para demais objetos no domínio
Para exportar um usuário, pode ser o caso que desejemos controlar quais campos serão exportados. Para isso, vamos definir um atributo em ambas as classes:
class User { (...) def exportFields = ["name", "email", "phone", "address"] } class Product { (...) def exportFields = ["code", "name", "description"] }
Agora, vamos adicionar à classe Object o método encodeAsCSV(), acessando o atributo exportFields:
Object.metaClass.encodeAsCSV = { def result = '' delegate.exportFields.each { fieldName -> result += delegate."$fieldName" + "," } result[0..-2] }
Neste momento, todos os objetos possuem um método encodeAsCSV.
Definitivamente uma vantagem poderosa das linguagens dinâmicas, técnicas de metaprogramação podem nos ajudar a construir sistemas melhores, mais simples e muito mais divertidos de se escrever!!
Até apróxima!