BT

Groovy 2.0: Novidades em Detalhe

Postado por Guillaume Laforge , traduzido por Rafael Sakurai em 10 Ago 2012 |

A recentemente lançada versão 2.0 do Groovy traz para a linguagem funcionalidades estáticas fundamentais, como a verificação estática de tipos e a compilação estática. Também adota melhorias relacionadas ao JDK 7 como as melhorias de sintaxe do projeto Coin e o suporte à nova instrução da JVM, o invokedynamic, Além disso, a linguagem se tornou mais modular que antes. Neste artigo, são abordadas em detalhes essas novas funcionalidades.

Verificação estática de tipos

O Groovy, por natureza, é e sempre será uma linguagem dinâmica. No entanto, é frequentemente utilizado como uma "linguagem de script para Java", ou como um "Java melhorado" (ou seja, com menos código repetitivo e com características mais poderosas). Muitos desenvolvedores Java usam e adicionam o Groovy às suas aplicações Java, como uma extensão da linguagem, para escrever regras de negócio, para possibilitar futuras customizações para diferentes clientes etc.

Para cada caso de uso baseado no Java, os desenvolvedores não precisam de todas as capacidades dinâmicas oferecidas pela linguagem. Geralmente esperam que o compilador Groovy ofereça comportamento semelhante ao dado pelo javac. Em particular, os desenvolvedores querem detectar erros de compilação (mais do que erros de execução) para casos como: digitação incorreta de nomes de variáveis e métodos, atribuições incorretas de tipos, entre outros. Por esse motivo, o Groovy 2.0 passou a suportar a verificação estática de tipos.

Identificando erros óbvios de digitação

A verificação estática de tipos é feita utilizando os mecanismos de transformação AST (Árvore de Sintaxe Abstrata) existentes no Groovy. Tais mecanismos funcionam como um plugin opcional para o compilador, acionado através de uma anotação. Para usar a verificação estática de tipos, basta adicionar a anotação @TypeChecked ao método ou à classe para ativá-la, com o nível de granularidade desejado. Vejamos o recurso em ação neste primeiro exemplo:

import groovy.transform.TypeChecked
void someMethod() {}
@TypeChecked
void test() {
  // erro de compilação:
  // não encontrado método sommeeMethod()
  sommeeMethod()
  def name = "Marion"
  // erro de compilação:
  // a variável naaammme não foi declarada
  println naaammme
}

A anotação @TypeChecked no método test(), instrui o compilador do Groovy a executar a verificação estática dos tipos neste método em particular, durante a sua compilação. Ao chamar o método "someMethod()", com erros óbvios de digitação, e imprimir a variável "name" também com erros de digitação o compilador lançará erros de compilação por não encontrar o método e a variável.

Checagem de atribuições e valores retornados

A verificação estática de tipos valida também se o tipo de retorno e os valores atribuídos estão coerentes:

import groovy.transform.TypeChecked
@TypeChecked
Date test() {
  // erro de compilação:
  // não pode ser atribuído um valor do tipo Date para variável do tipo int
  int object = new Date()
  String[] letters = ['a', 'b', 'c']
  // erro de compilação:
  // não se pode atribuir um valor do tipo String para variável do tipo Date
  Date aDateVariable = letters[0]
  // erro de compilação:
  // valor de retorno não pode ser String, para um método retornando Date
  return "today"
}

No exemplo, o compilador reclama porque, é claro, não se pode atribuir um tipo Date a uma variável do tipo int; como também não se pode retornar uma String no lugar de um Date, conforme especificado na assinatura do método. O erro de compilação no meio do script também é interessante, não apenas pela reclamação da atribuição incorreta, mas também por mostrar a inferência de tipos em ação. Isso porque o verificador de tipos certamente sabe que "letters[0]" é do tipo String, pois foi declarada como um array de Strings.

Mais sobre a inferência de tipos

Outra funcionalidade interessante é a validação que o verificador de tipos faz no retorno e nos valores:

import groovy.transform.TypeChecked
@TypeChecked
int method() {
  if (true) {
    // erro de compilação:
    // retorno não pode ser do tipo String em método que retorna int
    'String'
  } else {
    42
  }
}

Como o método retorna um valor do tipo primitivo int, a verificação de tipos também valida os valores retornados em diferentes construções dentro de blocos if/else, try/catch ou switch/case. Nesse outro exemplo, um ramo do bloco if retorna uma String em vez de um tipo primitivo int, e o compilador gera uma mensagem de erro.

Conversões comuns de tipos ainda são permitidas

A verificação estática de tipos não irá reclamar de certas conversões automáticas suportadas pelo Groovy. Por exemplo, para a assinatura de métodos que retornam String, boolean ou Class, o Groovy converte os valores de retorno para esses tipos automaticamente:

import groovy.transform.TypeChecked
@TypeChecked
boolean booleanMethod() {
  "Strings não vazias, são consideradas como valores true"
}
assert booleanMethod() == true
@TypeChecked
String stringMethod() {
  // StringBuilder é convertido para String chamando o método toString()
  new StringBuilder() << "non empty string"
}
assert stringMethod() instanceof String
@TypeChecked
Class classMethod() {
  // a classe java.util.List será retornada
  "java.util.List"
}
assert classMethod() == List

A verificação estática de tipos é também inteligente o bastante para fazer a inferência de tipos:

import groovy.transform.TypeChecked
@TypeChecked
void method() {
    def name = " Guillaume "
    // O tipo String é inferido (mesmo dentro do GString)
    println "NAME = ${name.toUpperCase()}"
    // Suporte ao método do Groovy GDK
    // (incluindo a sobrecarga de operadores do GDK)
    println name.trim()
    int[] numbers = [1, 2, 3]
    // Elemento n é um int
    for (int n in numbers) {
        println n;
    }
}

Apesar de "name" ser uma variável declarada com def, o verificador de tipos entende que é do tipo String. Então, quando essa variável é usada em conjunto com String, o verificador sabe que pode invocar o método toUpperCase() da classe String, ou o método trim() depois, que é um método adicionado pelo Kit de Desenvolvimento do Groovy (GDK), decorando a classe String. Por fim, ao iterar sobre os elementos do array de tipos primitivos int, o verificador também entenderá que um elemento do array é obviamente um int.

Misturando características dinâmicas e métodos de tipagem estática

Um aspecto importante a se ter em mente é que o uso da verificação estática de tipos restringe quais funcionalidades do Groovy se pode usar. Muitas características dinâmicas em tempo de execução não serão permitidas, por não poderem ser verificadas em tempo de compilação. (Um exemplo do que não é permitido é adicionar um novo método em tempo de execução através das metaclasses do tipo.) Mas quando for necessário utilizar alguma característica dinâmica em particular, como os construtores do Groovy, pode-se optar por não usar a verificação estática de tipos.

A anotação @TypeChecked pode ser utilizada em classes ou métodos. Para ativar a verificação de tipos em toda a classe, é suficiente adicionar a anotação à classe. Se for preciso que apenas alguns métodos tenham a verificação de tipo, adiciona-se a anotação a esses métodos. Ainda é possível solicitar que toda a classe seja verificada, exceto um método específico. Nesse caso é só adicionar a anotação @TypeChecked (TypeCheckingMode.SKIP), ou a versão abreviada @TypeChecked(SKIP). O script abaixo ilustra a situação onde o método greeting() tem verificação de tipos, enquanto que o método generateMarkup() não tem:

import groovy.transform.TypeChecked
import groovy.xml.MarkupBuilder

// este método tem verificação de tipos
@TypeChecked
String greeting(String name) {
    generateMarkup(name.toUpperCase())
}

// este método não tem verificação de tipos
// e é permitido o uso de características dinâmicas como o "markup builder"
String generateMarkup(String name) {
    def sw =new StringWriter()
    new MarkupBuilder(sw).html {
        body {
            div name
        }
    }
    sw.toString()
}
assert greeting("Cédric").contains("CÉDRIC")

Inferência de tipos e verificação com o instanceof

A versão atual do Java não suporta a inferência geral de tipos; por isso encontramos hoje muitos locais onde o código é prolixo e cheio de repetições. Isso atrapalha o entendimento e, sem o suporte de uma IDE, dificulta a escrita do código. Um caso típico é a verificação através do instanceof: frequentemente verifica-se a classe de um objeto com a palavra-chave instanceof dentro de uma condição if e depois, dentro do bloco if, é preciso utilizar casts para poder usar métodos do tipo em uso. No Groovy, com o novo modo de verificação estática de tipos, podemos nos livrar completamente dessas conversões.

import groovy.transform.TypeChecked
import groovy.xml.MarkupBuilder
@TypeChecked
String test(Object val) {
    if (val instanceof String) {
        // diferentemente do Java:
        // return ((String)val).toUpperCase()
        val.toUpperCase()
    } else if (val instanceof Number) {
        // diferentemente do Java:
        // return ((Number)val).intValue().multiply(2)
        val.intValue() * 2
    }
}
assert test('abc') == 'ABC'
assert test(123) == '246'

No exemplo acima, o verificador estático de tipos "sabe" que o parâmetro val é do tipo String, dentro do bloco if, e do tipo Number no bloco else, sem precisar de qualquer conversão (cast).

Lowest Upper Bound (Mínimo Limite Superior)

O verificador estático vai um pouco além em termos de inferência, pois possui conhecimento mais detalhado do tipo dos seus objetos. Considere o seguinte código:

import groovy.transform.TypeChecked
// tipo de retorno inferido:
// uma lista de números que são comparáveis e serializáveis
@TypeChecked test() {
    // um inteiro e um BigDecimal
    return [1234, 3.14]
}

Neste exemplo, o método retorna uma lista que contém dois números: um Integer e um BigDecimal. Mas a verificação estática executa o que chamamos de "lowest upper bound" (mínimo limite superior), que é na verdade uma lista de números que são serializáveis e comparáveis. Não é possível identificar esse tipo com os padrões de notações de tipos do Java, mas se tivéssemos um operador de intersecção como "&", seria algo parecido com List<Number & Serializable & Comparable>.

Fluxo de tipagem

Embora não seja recomendado como boa prática, algumas vezes os desenvolvedores usam a mesma variável não-tipada para armazenar valores de tipos diferentes:

import groovy.transform.TypeChecked
@TypeChecked test() {
    def var = 123           // o tipo inferido é int
    var = "123"             // atribui uma String para o var
    println var.toInteger() // sem problemas, não precisa de conversão/cast
    var = 123
    println var.toUpperCase() // erro, var é do tipo int!
}

A variável var é inicializada com um valor int; depois uma String é atribuída a esta variável. O algoritmo de "flow typing" segue o fluxo das atribuições e entende que a variável agora é do tipo String; então o verificador estático poderá usar o método toInteger() adicionado pelo Groovy à String. Logo depois, um número é colocado na variável var. Assim, quando o método toUpperCase() é invocado, a verificação do tipo lança um erro de compilação, pois o método toUpperCase() não pode ser aplicado em um Integer.

Existem alguns casos especiais para o algoritmo de fluxo de tipagem, como quando uma variável é compartilhada com uma closure. O que acontece quando uma variável local é referenciada em uma closure dentro de um método? Onde a variável foi definida? Vejamos um exemplo:

import groovy.transform.TypeChecked
@TypeChecked test() {
    def var = "abc"
    def cl = {
        if (new Random().nextBoolean()) var = new Date()
    }
    cl()
    var.toUpperCase() // erro de compilação!
}

A variável local var recebe uma String; mas depois var pode receber um Date se algum valor aleatório for verdadeiro. Tipicamente, somente em tempo de execução é possível determinar se a condição dentro do bloco if da closure será executada ou não. Consequentemente, no momento de compilação, não há como o compilador saber se var contém agora uma String ou um Date. Por isso o compilador reclama da chamada ao método toUpperCase(), por não poder deduzir se a variável contém uma String ou não. Esse exemplo é um pouco artificial, mas há casos mais interessantes:

import groovy.transform.TypeChecked
class A           { void foo() {} }
class B extends A { void bar() {} }
@TypeChecked test() {
    def var = new A()
    def cl = { var = new B() }
    cl()
   // var é pelo menos uma instância de A
   // então é possível invocar o método foo()
   var.foo()
}

No método test(), var recebe uma instância de A e depois uma instância de B dentro do closure, que por sua vez é chamada em seguida. Então é possível inferir que var é do tipo A.

Todas essas verificações adicionadas ao compilador do Groovy são feitas em tempo de compilação, porém o bytecode gerado continua sendo o mesmo código dinâmico gerado pelas versões anteriores, sem nenhuma alteração no comportamento em tempo de execução.

Agora que o compilador conhece mais sobre o seu programa, em termos de tipos, abrem-se algumas possibilidades interessantes, por exemplo, compilar o código estaticamente. A vantagem óbvia é que o bytecode gerado se assemelhará mais com o criado pelo javac, fazendo com que o código compilado estaticamente em Groovy seja tão rápido quanto o código em Java, entre outras vantagens.

Compilação estática

O exemplo a seguir mostra a nova anotação @CompileStatic:

import groovy.transform.CompileStatic
@CompileStatic
int squarePlusOne(int num) {
    num * num + 1
}
assert squarePlusOne(3) == 10

Ao utilizar a anotação @CompileStatic, ao invés da anotação @TypeChecked, o código será compilado estaticamente e o bytecode gerado será muito parecido com o de Java, sendo executado tão rapidamente quanto o bytecode gerado pelo javac. Como a anotação @TypeChecked, a anotação @CompileStatic pode ser utilizada em classes e métodos, e com @CompileStatic(SKIP) pode-se evitar a compilação estática de um método específico dentro de uma classe marcada com @CompileStatic.

Outra vantagem da compilação estática é que o tamanho do bytecode gerado para um método será menor que um bytecode gerado pelo Groovy para métodos dinâmicos, uma vez que para dar suporte às características dinâmicas do Groovy, o bytecode dinâmico contém instruções adicionais para execução no runtime do Groovy.

A compilação estática pode ser usada também em frameworks ou bibliotecas e ajuda a evitar interações nocivas quando se utiliza metaprogramação em várias partes do código. As características dinâmicas disponibilizadas na linguagem Groovy dão aos desenvolvedores muito poder e flexibilidade, mas se alguns cuidados não forem tomados, pode haver pressuposições em diferentes partes do sistema com relação a quais características da metaprogramação estão sendo usadas, e isto poderá causar consequências não intencionais.

Considere, por exemplo, o que acontece ao utilizar duas bibliotecas diferentes, em que ambas adicionam um nome similar a um método, mas com diferentes implementações para uma de suas classes principais. Qual é o comportamento esperado? Usuários com experiência em linguagens dinâmicas já viram este problema antes e provavelmente ouviram referências ao monkey patching. Como o código compilado não é executado no ambiente de execução do Groovy, o código fica protegido contra esse problema.

Embora os aspectos de tempo de execução dinâmica da linguagem não possam ser usados no contexto estático, todos os mecanismos de transformação AST funcionarão tão bem como antes, uma vez que as transformações do AST acontecem em tempo de compilação.

Em termos de desempenho, o código Groovy compilado estaticamente é geralmente tão rápido quanto o código gerado com o javac. Nos comparativos realizados pela equipe de desenvolvimento, o desempenho se mostrou idêntico em vários casos, sendo só algumas vezes ligeiramente mais lento.

Historicamente, graças à integração transparente do Java com o Groovy, fazíamos a recomendação de otimizar algumas rotinas usando Java para obter mais performance. Mas agora, com a opção de compilação estática, isso não será mais necessário, e quem desejar desenvolver seus projetos completamente em Groovy poderá fazê-lo.

O Java 7 e o tema JDK 7

A gramática da linguagem Groovy na verdade deriva da própria gramática do Java, mas, obviamente, o Groovy fornece alguns atalhos para deixar o desenvolvedor mais produtivo. A familiaridade com a sintaxe para desenvolvedores Java sempre foi o principal atrativo, graças à facilidade de aprendizado. E claro, é de se esperar que os usuários do Groovy desejem os benefícios dos refinamentos na sintaxe oferecidos pelo Java 7 com o Project Coin.

Além dos aspectos de sintaxe, o JDK 7 também traz novidades interessantes para suas APIs. E pela primeira vez em muito tempo, há uma nova instrução para o bytecode chamada invoke dynamic, que é destinada a ajudar aos implementadores a desenvolver suas linguagens dinâmicas mais facilmente e com melhor desempenho.

Melhorias de sintaxe vindas do Projeto Coin

Desde o início (em 2003), o Groovy tem trazido várias melhorias de sintaxe e características que vão além do Java. Uma delas são as closures e também a habilidade de colocar mais que apenas valores discretos nos blocos switch/case (o Java 7 apenas adicionou o suporte a Strings). Mas algumas das melhorias são novas, tais como literais binárias, caracteres sublinhado em literais numéricas, ou blocos com múltiplos catch, todas suportadas pelo Groovy 2. A única omissão das melhorias do Projeto Coin são os "try com recursos", para o qual o Groovy já fornece algumas alternativas através da API do Kit de Desenvolvimento do Groovy.

Literais binárias

No Java 6 e versões anteriores, assim como no Groovy, números podem ser representados nas bases decimal, octal e hexadecimal, e com o Java 7 e o Groovy 2, também pode ser usada a notação binária com o prefixo "0b" (zero, b):

int x = 0b10101111
assert x == 175
byte aByte = 0b00100001
assert aByte == 33
int anInt = 0b1010000101000101
assert anInt == 41285

Caractere "sublinhado" em literais numéricas

Ao escrever literais numéricas longas, é difícil ver como os dígitos são agrupados. Ao permitir a utilização de caracteres de sublinhado (_) nos números literais, fica mais fácil identificar esses grupos:

long creditCardNumber = 1234_5678_9012_3456L
long socialSecurityNumbers = 999_99_9999L
double monetaryAmount = 12_345_132.12
long hexBytes = 0xFF_EC_DE_5E
long hexWords = 0xFFEC_DE5E
long maxLong = 0x7fff_ffff_ffff_ffffL
long alsoMaxLong = 9_223_372_036_854_775_807L
long bytes = 0b11010010_01101001_10010100_10010010

Múltiplos blocos catch

Ao tratar exceções, frequentemente o mesmo bloco de código é replicado para duas ou mais exceções para que recebam o mesmo tratamento. Uma forma alternativa é separar o código repetido em métodos, ou, uma forma deselegante é capturar uma Exception, ou pior, um Throwable.

Com o bloco multi-catch, é possível definir algumas exceções para serem tratadas pelo mesmo bloco catch:

try {
    /* ... */
} catch(IOException | NullPointerException e) {
    /* Bloco para tratar duas exceções */
}

Suporte a invoke dynamic

Como mencionado, o JDK 7 vem com uma nova instrução de bytecode chamada invoke dynamic. Seu objetivo é auxiliar os desenvolvedores de linguagens dinâmicas no serviço de elaborar suas linguagens em cima da plataforma Java, simplificando a chamada de métodos dinâmicos, através da definição de call sites, locais onde uma seção de chamadas a métodos dinâmicos pode ser colocada em cache. Há ainda os method handles, como tratadores de métodos, class values, que armazenam alguns tipos de metadados das classes dos objetos, entre outros. Mas uma ressalva deve ser feita: apesar de prometer melhoras de desempenho, a "invocação dinâmica" ainda não foi completamente otimizada dentro da JVM, e ainda não fornece o melhor desempenho possível; mas o desempenho melhora a cada nova versão.

O Groovy trouxe suas implementações próprias para acelerar a seleção de métodos e invocação com o "call site caching", armazenar metaclasses (o equivalente dinâmico das classes) com o seu registro de metaclasses, para otimizar o cálculo de tipos primitivos nativos; isso entre outras otimizações, para que o código gerado tivesse a mesma performance que o bytecode gerado pelo javac. Com a chegada da "invocação dinâmica", foi possível implementar o Groovy em cima destas APIs e dessa instrução do bytecode da JVM, para obter desempenho melhor e simplificar o código interno.

Quem utiliza o JDK 7, poderá utilizar a nova versão dos JARs do Groovy, que foram compilados com o suporte à invocação dinâmica. Esses JARs são facilmente reconhecidos: possuem o classificador "-indy" em seus nomes.

Habilitando o suporte à invocação dinâmica

Para habilitar a invocação dinâmica no Groovy, além de utilizar o JDK 7 e incluir os JARs com o sufixo "-indy" em seus nomes, é necessário utilizar a flag "--indy" ao executar o compilador "groovyc" ou o comando "groovy". Se o parâmetro "--indy" for omitido, os mesmos JARs podem ser utilizados junto com o JDK 5 ou 6, sem habilitar a invocação dinâmica.

De forma similar, em projetos que utilizam o Ant para builds, é necessário especificar o atributo "indy=true", como no exemplo abaixo:

...
<taskdef name="groovyc"
        classname="org.codehaus.groovy.ant.Groovyc"
        classpathref="cp"/>
...
<groovyc srcdir="${srcDir}" destdir="${destDir}" indy="true">
    <classpath>
...
    </classpath>
</groovyc>
...

Ao se fazer a integração de aplicações Java com o Groovy, utilizando o GroovyShell, por exemplo, é possível habilitar o suporte à invocação dinâmica, passando uma instância do CompilerConfiguration para o construtor do GroovyShell, no qual é possível acessar e configurar opções de otimização:

CompilerConfiguration config = new CompilerConfiguration();
config.getOptimizationOptions().put("indy", true);
config.getOptimizationOptions().put("int", false);
GroovyShell shell = new GroovyShell(config);

Como se espera que o invoke dynamic seja o substituto do "method dispatch", também é necessário desabilitar as otimizações primitivas, o que gera código adicional no bytecode. Mesmo perdendo performance em alguns casos, se comparado ao código gerado com as otimizações primitivas ativadas, versões futuras da JVM com melhorias no JIT serão capazes de otimizar as chamadas aos métodos, removendo conversões automáticas desnecessárias.

Melhorias de desempenho promissoras

Em nossos testes, notamos ganhos interessantes de desempenho em algumas áreas, comparando com outros programas que ainda não estão usando o suporte à invocação dinâmica. A equipe do Groovy ainda tem melhorias de desempenho planejadas para o Groovy 2.1, mas notamos que a JVM ainda não foi toda otimizada e ainda tem um longo caminho para sê-la. Mas, felizmente, nas atualizações do JDK 7 (em particular da oitava atualização) já estão disponíveis algumas melhorias; então a situação só deve melhorar. Além disso, como a invocação dinâmica é usada para a implementação dos Lambdas no JDK 8, é certeza que mais melhorias virão.

Um Groovy mais modular

Tal como o Java, o Groovy não é apenas uma linguagem, ele também é um conjunto de APIs que servem para vários propósitos como: templates, construção de interfaces gráficas de usuário usando Swing, scripts do Ant, integração via JMX, acesso a dados via SQL, disponibilização de servlet, entre outros. O Groovy disponibiliza todas estas características e APIs dentro de um grande e único JAR.

Entretanto, nem todo mundo precisa de tudo isso o tempo todo em suas aplicações: pode-se estar interessado no mecanismo de templates e de servlets se estiver escrevendo uma aplicação web; mas pode-se precisar apenas da construção de interfaces gráficas com Swing quando estiver trabalhando com um programa cliente para desktop. Para isso, temos os módulos do Groovy.

Módulos do Groovy

O objetivo principal dos aspectos de modularidade da nova versão é dividir o JAR original do Groovy em pequenos módulos, ou seja, pequenos JARs. O JAR do núcleo do Groovy ficou duas vezes menor e temos os seguintes módulos disponíveis:

  • Ant: para scripts do Ant, automatizando tarefas administrativas;
  • BSF: para integração do Groovy em aplicações Java com o velho Apache Bean Scripting Framework;
  • Console: módulo contendo o console do Groovy Swing;
  • GroovyDoc: para documentar suas classes Groovy e Java;
  • Groovysh: módulo correspondente ao terminal de linha de comando Groovysh;
  • JMX: para expor e consumir beans JMX;
  • JSON: para produzir e consumir informações no formato JSON;
  • JSR-223: para integrar o Groovy às aplicações Java através da API javax.scripting do JDK 6 ou superior;
  • Servlet: para escrever e disponibilizar templates e scripts de servlets do Groovy;
  • SQL: para consultar bases relacionais;
  • Swing: para construção de interfaces gráficas usando o Swing;
  • Templates: para uso do mecanismo de templates;
  • Test: para suporte a testes, como o GroovyTestCase, mocking e mais;
  • TestNG: para escrever testes usando o TesteNG no Groovy;
  • XML: para produzir e consumir documentos em XML.

Com o Groovy 2, pode-se apenas escolher os módulos que são necessários, ao invés de colocar tudo no classpath. No entanto, ainda será fornecido o JAR "completo", caso não queira complicar suas dependências apenas para economizar alguns megabytes de espaço em disco. Também são fornecidos os JARs compilados com suporte à invocação dinâmica, para aqueles que estão executando no JDK 7.

Módulos de extensão

O trabalho de fazer o Groovy mais modular também rendeu uma nova e interessante característica: módulos de extensão. Como o Groovy foi dividido em módulos menores, foi criado um mecanismo para que módulos possam contribuir com métodos de extensão. Dessa forma, esses módulos podem fornecer métodos de instância e métodos estáticos para outras classes, incluindo classes do JDK e de bibliotecas de terceiros.

O Groovy usa esse mecanismo para decorar as classes do JDK, adicionando novos métodos para as classes String, File, classes streams e outras; por exemplo, um método getText() na classe URL permite obter o conteúdo de uma URL remota chamada a partir de um método get do HTTP. Note também que esses métodos de extensão em seus módulos também são reconhecidos pela verificação e compilação de tipos estáticos. A seguir é apresentado como adicionar novos métodos para tipos existentes.

Adicionando um método de instância

Para adicionar novos métodos em um tipo já existente, precisa-se criar uma classe auxiliar que contenha esses métodos. Dentro da classe auxiliar, todos os métodos de extensão serão públicos (padrão no Groovy, mas obrigatório se for implementado em Java) e estáticos (embora os métodos possam estar disponíveis nas instâncias da classe). Os métodos sempre receberão no primeiro parâmetro uma instância a ser chamada pelo método. Os demais parâmetros são passados ao chamar o método. Esta é a mesma convenção usada para as categorias do Groovy.

No exemplo a seguir, será adicionado um método greets() na classe String, quer irá saudar o nome da pessoa, passado por parâmetro:

assert "Guillaume".greets("Paul") == "Hi Paul, I'm Guillaume"

Para chamar o método, é preciso criar uma classe auxiliar com o novo método de extensão:

package com.acme
class MyExtension {
    static String greets(String self, String name) {
        "Hi ${name}, I'm ${self}"
    }
}

Adicionando um método estático

Para métodos estáticos de extensão, é usado o mesmo mecanismo e convenção. O exemplo a seguir mostra como adicionar um método estático à classe Random, para obter um inteiro randômico entre dois valores:

package com.acme
class MyStaticExtension {
    static String between(Random selfType, int start, int end) {
        new Random().nextInt(end - start + 1) + start
    }
}

Pode-se usar o método de extensão da seguinte maneira:

Random.between(3, 4)

Descrição do módulo de extensão

Uma vez que sua classe auxiliar (em Groovy ou mesmo em Java) contenha os métodos estendidos, é preciso criar um descritor para o módulo. Cria-se um arquivo chamado org.codehaus.groovy.runtime.ExtensionModule, no diretório META-INF/services do arquivo do módulo. Quatro campos essenciais devem ser definidos para informar o nome e a versão do módulo ao runtime Groovy, além de associar suas classes auxiliares aos métodos estendidos, através de uma lista de nomes de classes separados por vírgula. Veja um exemplo de descritor do módulo:

moduleName = MyExtension
moduleVersion = 1.0
extensionClasses = com.acme.MyExtension
staticExtensionClasses = com.acme.MyStaticExtension

Com esse descritor da extensão do módulo no classpath, é possível utilizar os métodos de extensão no código, sem precisar importar ou fazer qualquer outra coisa, já que os métodos são registrados automaticamente.

Obtendo uma extensão

Com a anotação @Grab no seu script, pode-se obter o repositório de dependências do Maven, como o Maven Central, por exemplo. Adicionando a anotação @GrabResolver, é possível especificar a localização de dependências também. Ao se obter uma dependência de um módulo de extensão usando este mecanismo, o método de extensão também será instalado automaticamente. Idealmente, para consistência, o nome e versão do seu módulo devem ser coerentes com o id e a versão do seu artefato.

Resumo

O Groovy é bem popular entre os desenvolvedores Java, e oferece uma plataforma madura e um ecossistema para as aplicações. Além disso, a equipe de desenvolvimento do Groovy continua melhorando a linguagem e suas APIs, ajudando os desenvolvedores a melhorarem sua produtividade na plataforma Java.

Pode-se dizer que o Groovy 2.0 atende a três necessidades principais:

  1. Maior desempenho: através do suporte à invocação dinâmica do JDK 7 para acelerar o Groovy, mas também da compilação estática do JDK 5, e além. Especialmente para aqueles que estão dispostos a abandonar alguns aspectos dinâmicos para se proteger do "monkey patching", pode-se ganhar a mesma velocidade do Java;
  2. Mais integração com Java: com o suporte às melhorias do Projeto Coin do Java 7, o Groovy e o Java estão com sintaxes mais parecidas. E com a verificação estática de tipos, ambas as linguagens podem ter o mesmo nível de verificação e segurança de tipos que o fornecido pelo javac;
  3. Mais modularidade: com um novo patamar de modularidade, o Groovy abre as portas para produtos "menores", por exemplo, para a integração de aplicações móveis no Android. Permite ainda que as APIs do Groovy cresçam e evoluam através de novas versões e novos módulos de extensão, e que os usuários colaborem com métodos de extensão para os tipos já existentes.

Sobre o autor

Como chefe de desenvolvimento do Groovy na SpringSource (uma divisão da VMware), Guillaume Laforge é gerente oficial do projeto Groovy. Laforge iniciou a criação do framework de aplicações web Grails e fundou o projeto Gaelyk, um kit leve para desenvolvimento de aplicações Groovy no Google App Engine. Também é palestrante frequente, apresentando Groovy, Grails, Gaelyk e DSLs em eventos como JavaOne, GR8Conf, SpringOne2GX, QCon, Devoxx, entre outros.

Olá visitante

Você precisa cadastrar-se no InfoQ Brasil ou para enviar comentários. Há muitas vantagens em se cadastrar.

Obtenha o máximo da experiência do InfoQ Brasil.

Dê sua opinião

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber menssagens dessa discussão
Comentários da comunidade

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber menssagens dessa discussão

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber menssagens dessa discussão

Dê sua opinião

Conteúdo educacional

Feedback geral
Bugs
Publicidade
Editorial
InfoQ Brasil e todo o seu conteúdo: todos os direitos reservados. © 2006-2013 C4Media Inc.
Política de privacidade
BT