Arquitetura evolutiva e design emergente: Construindo DSLs no Groovy

Coleta mais expressiva de padrões idiomáticos

Domain-specific languages (DSLs) internas são possíveis, mas pesam na linguagem Java™ devido à sua sintaxe restritiva. Outras linguagens no JVM são mais adequadas para construí-las. EstaArquitetura evolutiva e design emergente trata dos recursos que podem ser explorados e dos problemas que serão encontrados ao usar o Groovy para construir DSLs internas.

Neal Ford, Application Architect, ThoughtWorks Inc.

Neal FordNeal Ford é um arquiteto de software e Meme Wrangler, na ThoughtWorks, uma consultoria global de TI. Projeta e desenvolve aplicativos, materiais de instrução, artigos para revistas, treinamentos e apresentações em vídeo/DVD, e é autor ou editor de livros que abordam uma variedade de tecnologias, inclusive The Productive Programmer Seu enfoque é o projeto e construção de aplicativos corporativos de grande porte. Também é orador internacionalmente aclamado nas conferências de desenvolvedores ao redor do mundo. Conheça seu Web site.



03/Set/2010

No capítulo do mês passado, foram mostrados exemplos do uso de domain-specific languages (DSLs) para a coleta de padrões idiomáticos, definidos como idiomas de design comum no seu código. (O conceito de padrões idiomáticos foi apresentado em "Composed method and SLAP.") As DSLs são um bom meio de captura de padrões porque são declarativas, são mais legíveis do que o código de origem "normal" e permitem que os padrões coletados se destaquem do código circundante.

As técnicas de linguagem para construir DSLs usam frequentemente truques inteligentes para fornecer contexto de agrupamento ao código implicitamente. Em outras palavras, as DSLs tentam "ocultar" a sintaxe turbulenta usando recursos da linguagem subjacente para tornar o seu código mais legível. Mesmo que seja possível construir DSLs na linguagem Java, seu conjunto fraco de construções para ocultar o contexto, junto com sua sintaxe rígida e implacável, torna essa linguagem pouco adequada para essa técnica. Contudo, outras linguagens baseadas em JVM podem preencher essa lacuna. Neste capítulo e no próximo, mostraremos como é possível expandir a paleta de construção de DSL de forma a incluir linguagens mais expressivas que sejam executadas na plataforma Java, iniciando com o Groovy (consulte Recursos).

Sobre esta série

Esta série visa prover uma nova perspectiva sobre conceitos de arquitetura e design de software frequentemente discutidos, mas elusivos. Por meio de exemplos concretos, Neal Ford oferece a você um embasamento sólido nas práticas ágeis da arquitetura evolutiva e design emergente. O adiamento de decisões importantes sobre arquitetura e design até o último momento viável pode impedir que a complexidade desnecessária comprometa os projetos software.

O Groovy oferece vários recursos que facilitam a construção de DSLs. O suporte a quantidades é um requisito comum em DSLs. As pessoas sempre precisam de inúmeras coisas: 7 polegadas, 4 milhas, 13 dias. O Groovy permite incluir suporte direto a quantidades numéricas por meio de classes abertas. As classes abertas permitem reabrir as classes existentes e alterá-las através da inclusão, remoção ou alteração dos métodos na classe — um mecanismo que é ao mesmo tempo poderoso e perigoso. Felizmente, existem maneiras seguras de implementar esse mecanismo. O Groovy suporta duas sintaxes diferentes para classes abertas: categorias e ExpandoMetaClass.

Classes abertas via categorias

O conceito de categorias é emprestado das linguagens como Smalltalk e Objective-C (consulte Recursos). Uma categoria cria um wrapper ao redor da chamada do código que contém uma ou mais classes abertas, usando a diretiva de bloco use

As categorias são mais bem compreendidas por meio de um exemplo. A listagem 1 mostra um teste que demonstra um novo método que adicionamos a String chamado camelize(), que converte as cadeias de caracteres limitadas por underscore para caixa alternante:

Listagem 1. Teste que demonstra o método camelize()
class TestStringCategory extends GroovyTestCase {
    def expected = ["event_map" : "eventMap",
            "name" : "name", "test_date" : "testDate",
            "test_string_with_lots_of_breaks" : "testStringWithLotsOfBreaks",
            "String_that_has_init_cap" : "stringThatHasInitCap" ]

    void test_Camelize() {
        use (StringCategory) {
            expected.each { key, value ->
                assertEquals value, key.camelize()
            }
        }
    }
}

Na Listagem 1, criamos um hash expected com caixas originais e transformadas, em seguida, quebramos StringCategory ao redor da iteração sobre o mapa, esperando que cada tecla se torne camelize. Observe que dentro do bloco use , não é necessário fazer nada de especial para chamar os novos métodos na classe.

O código para StringCategory aparece na Listagem 2:

Listagem 2. A classe StringCategory
class StringCategory {

  static String camelize(String self) {
    def newName = self.split("_").collect() {
      it.substring(0, 1).toUpperCase() +  it.substring(1, it.length())
    }.join()
    newName.substring(0, 1).toLowerCase() +  newName.substring(1, newName.length())
  }
}

As categorias são classes regulares que contêm métodos estáticos. O método estático deve ter pelo menos um parâmetro, que é o tipo que está sendo aumentado. Na Listagem 2, declaramos um único método estático que aceita um parâmetro String (tradicionalmente chamado de self, mas é possível chamá-lo como for desejado) que representa a classe que está sendo incluída no método. O corpo do método contém o código Groovy para quebrar a cadeia de caractere em partes delimitadas pelo underscore (que é o que o método split("_") faz), em seguida, coletar as cadeias de caracteres de volta e juntar os pedaços com as letras maiúsculas no lugar. A última linha trata da certificação de que o primeiro caractere da cadeia de caractere retornada está em minúscula.

Quando o StringCategory, é necessário acessá-lo dentro de um bloco use. É possível ter múltiplas classes de categorias, separadas por vírgulas, dentro dos parênteses do bloco use.

Aqui está outro exemplo de uso das classes abertas para expressar quantidades em DSL. Considere o código na Listagem 3 que implementa um compromisso de calendário simples:

Listagem 3. Uma DSL de calendário simples
def calendar = new AppointmentCalendar()

use (IntegerWithTimeSupport) {
    calendar.add new Appointment("Dentist").from(4.pm)
    calendar.add new Appointment("Conference call")
                 .from(5.pm)
                 .to(6.pm)
                 .at("555-123-4321")
}
calendar.print()

A Listagem 3 implementa o mesmo tipo de funcionalidade que os exemplos Java em "Interfaces fluentes" mas com sintaxe melhorada, incluindo várias coisas que não são possíveis no código Java. Por exemplo, observe que o Groovy permite que você omita os parênteses em alguns locais (como em volta do argumento do métodoadd()). Podemos fazer chamadas como 5.pm, que parecem estranhas para os desenvolvedores de Java. Esse é um exemplo de abertura de classe Integer (todos os números no Groovy usam classes tipo wrapper automaticamente, assim até mesmo 5 é realmente um Integer) e inclusão de uma propriedade pm. A classe que implementa essa classe aberta aparece na Listagem 4:

Listagem 4. Definição de classe IntegerWithTimeSupport
class IntegerWithTimeSupport {
    static Calendar getFromToday(Integer self) {
        def target = Calendar.instance
        target.roll(Calendar.DAY_OF_MONTH, self)
        return target
    }

    static Integer getAm(Integer self) {
        self == 12 ? 0 : self
    }

    static Integer getPm(Integer self) {
        self == 12 ? 12 : self + 12
    }
}

Essa classe de categoria inclui três novos métodos para Integer: getFromToday(), getAm() e getPm(). Observe que, na realidade, são novas propriedades, não métodos. A razão pela qual nós as escrevemos como novas propriedades está relacionada com a maneira como o Groovy trata a solicitação de método. Quando um método Groovy que não tem parâmetros é chamado, é necessário chamá-lo com um conjunto vazio de parênteses, o que permite que o Groovy faça a distinção entre um acesso de propriedade e uma chamada de método. Se escrevêssemos as extensões como métodos, a DSL precisaria chamar as extensões am e pm como 5.pm(), o que prejudicaria a capacidade de leitura da DSL. Uma das principais razões pela qual estamos usando a DSL é para melhorar a capacidade de leitura, assim seria interessante nos livrarmos do excesso de ruído. É possível fazer isso no Groovy pela criação de extensões como propriedade, como alternativa. A sintaxe para declarar propriedades é a mesma que na linguagem Java — com um par de métodos get/set— no entanto, é possível chamá-los sem parênteses.

Nessa DSL, a unidade de medida é hora, o que significa que é necessário retornar 15 para 3.pm. Ao construir DSLs que contêm quantidades, é necessário decidir a respeito das unidades e (opcionalmente) incluí-las na DSL para torná-la mais legível. Lembre-se de que estamos usando a DSL para capturar um padrão idiomático de domínio, o que significa que pessoas que não são desenvolvedores precisam lê-lo.

Agora que já foi mostrado como implementar o tempo no calendário DSL, a classeAppointment , que será mostrada na Listagem 5, fica mais clara:

Listagem 5. A classe Appointment
class Appointment {
  def name;
  def location;
  def date;
  def startTime;
  def endTime;

  Appointment(apptName) {
    name = apptName
    date = Calendar.instance
  }

  def at(loc)  {
    location = loc
    this
  }

  def formatTime(time) {
    time > 12 ? "${time - 12} PM" : "${time} AM"
  }

  def getStartTime() {
    formatTime(startTime)
  }

  def getEndTime() {
    formatTime(endTime)
  }

  def from(start_time) {
    startTime = start_time
    date.set(Calendar.HOUR_OF_DAY, start_time)
    this
  }

  def to(end_time) {
    endTime = end_time
    date.set(Calendar.HOUR_OF_DAY, end_time)
    this
  }

  def display() {
    print "Appointment: ${name}, Starts: ${formatTime(startTime)}"
    if (endTime) print ", Ends: ${formatTime(endTime)}"
    if (location) print ", Location: ${location}"
    println()
  }
}

Mesmo que não conheça nada sobre o Groovy, você provavelmente não terá problemas na leitura da classe Appointment. Observe que no Groovy, a última linha do método é seu valor de retorno. Isso faz da última linha dos métodos at(), from() e to() (o retorno de this) as chamadas de interface fluente nessa classe.

As categorias permitem fazer mudanças nas classes existentes de maneira controlada. As mudanças tem o escopo estritamente definido no bloco lexical definido pela cláusula use(). No entanto, existem momentos em que é desejado que os métodos incluídos de uma classe aberta tenham um escopo mais amplo, e é nisso que o ExpandoMetaClass do Groovy ajuda.


Classes abertas via expando

A sintaxe de classe aberta original no Groovy usava apenas categorias. No entanto, os criadores da estrutura da Web do Groovy, Grails (consulte Recursos), acharam a definição de escopo inerente nas categorias muito restritiva. Isso levou ao desenvolvimento de uma sintaxe alternativa para as classes abertas, a ExpandoMetaClass. Ao usar um expando, a metaclasse da classe (que o Groovy cria para você oportunamente) é acessada e são incluídos métodos e propriedades nela. O exemplo de calendário que usa os expandos aparece na Listagem 6:

Listagem 6. Calendários com classes abertas de expando
def calendar = new AppointmentCalendar()

calendar.add new Appointment("Dentist")
             .from(4.pm)
calendar.add new Appointment("Conference call")
             .from(5.pm)
             .to(6.pm)
             .at("555-123-4321")

calendar.print()

O código na Listagem 6 é quase o mesmo que o da Listagem 3, com exceção do bloco use necessário para as categorias.Para implementar as mudanças no Integer, acesse a metaclasse conforme mostrado na Listagem 7:

Listagem 7. Definições de expando para Integer
Integer.metaClass.getAm = { ->
  delegate == 12 ? 0 : delegate
}

Integer.metaClass.getPm = { ->
  delegate == 12 ? 12 : delegate + 12
}

Integer.metaClass.getFromToday = { ->
  def target = Calendar.instance
  target.roll(Calendar.DAY_OF_MONTH, delegate)
  target
}

Como no exemplo de categoria, precisamos de am e pm como propriedades e não como métodos (de forma que não teremos acesso a eles com parênteses quando os chamarmos) em Integer, dessa forma, incluímos uma nova propriedade na metaclasse como Integer.metaClass.getAm. Esses blocos de códigos podem aceitar parâmetros, mas não são necessários aqui (por isso o solitário -> no começo do bloco de códigos). Dentro do bloco de códigos, a palavra chave delegate faz referência à instância da classe em que os métodos estão sendo adicionados. Por exemplo, observe que na propriedade getFromToday , foi criada uma nova instância Calendar , em seguida, foi usado o valor delegado para passar no calendário o número de dias especificados por essa instância de Integer. Quando executamos 5.fromToday, o calendário passa cinco dias à frente.


Escolhendo entre categorias e expando

Tendo em vista que as categorias e os expandos fornecem o mesmo tipo de expressividade, qual você deve escolher? A melhor coisa a respeito das categorias é o escopo inerente que limita o bloco lexical. É um antipadrão DSL comum para fazer mudanças fundamentais (possivelmente quebras) nas classes de núcleo da linguagem. As categorias impigem um limite às modificações. Os expandos, por outro lado, são globais por natureza: depois que um expando for executado, aquelas mudanças aparecem no resto do aplicativo.

No geral, prefira categorias. Quando estiver fazendo mudanças em classes importantes com potencial para efeitos colaterais, é interessante limitar o escopo dessas mudanças. As categorias permitirão que o escopo das mudanças seja definido cuidadosamente. Entretanto, se você se encontrar agrupando cada vez mais códigos com as mesmas categorias, você deve escalar para os expandos. Algumas mudanças precisam ser amplas e forçar essas mudanças a se adequarem dentro dos blocos pode causar código embaralhado. Como regra principal, se você se encontrar agrupando mais de três blocos muito diferentes em uma categoria, considere torná-la um expando.

Uma última nota: o teste não é opcional neste caso. Muitos desenvolvedores parecem acreditar que testar é uma opção para grandes fileiras de códigos, mas qualquer código que faça mudanças nas classes existentes precisa de testes abrangentes. O recurso para modificar classes de núcleo é poderoso e pode ter como resultado soluções elegantes de problemas. Contudo, com o poder vem a responsabilidade, que se manifesta com os testes.


Ocorrências reais

Esta discussão sobre DSLs como forma de capturar padrões idiomáticos pode estar parecendo um pouco abstrata até o momento, então, vamos terminar com um exemplo do mundo real.

A easyb (consulte Recursos) é uma ferramenta de teste de desenvolvimento voltada ao comportamento e baseada em Groovy que permite criar cenários que combinem linguagem acessível a quem não é desenvolvedor com conhecimento de linguagem com código para implementar um teste. Um exemplo de cenário easyb aparece na Listagem 8:

Listagem 8. Cenário easyb testando uma fila
package org.easyb.bdd.specification.queue

import org.easyb.bdd.Queue

description "This is how a Queue must work"

before "initialize the queue for each spec", {
    queue = new Queue()
}

it "should dequeue item just enqueued", {
    queue.enqueue(2)
    queue.dequeue().shouldBe(2)
}

it "should throw an exception when null is enqueued", {
    ensureThrows(RuntimeException.class) {
        queue.enqueue(null)
    }
}

it "should dequeue items in same order enqueued", {
    [1..5].each {val ->
        queue.enqueue(val)
    }
    [1..5].each {val ->
        queue.dequeue().shouldBe(val)
    }
}

O código na Listagem 8 define o comportamento adequado para uma fila. Cada um dos blocos da declaração começa com it, seguido por uma descrição da cadeia de caractere e um bloco de códigos. O método de definição para it parece com este, em que spec é o esperado para descrever o teste e closure contém o bloco de códigos:

def it(spec, closure)

Observe que no último teste na Listagem 8, estamos verificando o valor que vem da chamada para dequeue(), usando esta linha de código:

queue.dequeue().shouldBe(val)

Mas a inspeção da classe Queue mostra que ela não tem um método shouldBe(). De onde ele vem?

Se olhar a definição do método it() , é possível ver onde as categorias são usadas para aumentar as classes existentes. A Listagem 9 mostra a declaração do método it():

Listagem 9. Declaração do método it()
def it(spec, closure) {
    stepStack.startStep(listener, BehaviorStepType.IT, spec)
    closure.delegate = new EnsuringDelegate()
    try {
        if (beforeIt != null) {
            beforeIt()
        }
        listener.gotResult(new Result(Result.SUCCEEDED))
    use(BehaviorCategory) {
            closure()
        }
        if (afterIt != null) {
            afterIt()
        }
    } catch (Throwable ex) {
        listener.gotResult(new Result(ex))
    }
    stepStack.stopStep(listener)
}

Aproximadamente no meio do método, o bloco closure passado como parâmetro é executado na classe BehaviorCategory , cujo um excerto é mostrado na Listagem 10:

Listagem 10. Parte da classe BehaviorCategory
static void shouldBe(Object self, value, String msg) {
    isEqual(self, value, msg)
}

private static void isEqual(self, value, String msg) {
    if (self.getClass() == NullObject.class) {
        if (value != null) {
            throwValidationException(
                "expected ${value.toString()} but target object is null", msg)
        }
    } else if (value.getClass() == String.class) {
        if (!value.toString().equals(self.toString())) {
            throwValidationException(
                "expected ${value.toString()} but was ${self.toString()}", msg)
        }
    } else {
        if (value != self) {
            throwValidationException("expected ${value} but was ${self}", msg)
        }
    }
}

BehaviorCategory é uma categoria cujos métodos aumentam o Object, o que ilustra o poder incrível das classes abertas. Ao incluir um novo método no Object, é concedido a cada instância no aplicativo de acesso a esses métodos, o que torna trivial a inclusão de um método shouldBe() em cada classe (incluindo Queue). Não é possível fazer isso usando o código Java e seria difícil fazer isso mesmo com aspectos. O uso de categorias reforça a recomendação anterior: limita o escopo das mudanças no Object ao corpo da cláusula use na DSL da easyb.


Conclusão

Queremos que os padrões idiomáticos que coletamos fiquem fora do resto do código e que as DSLs forneçam um mecanismo atraente para atingir esse objetivo. É bem mais fácil gravar as DSLs em linguagens que possuam o suporte para gravá-las, diferentemente da linguagem Java. Se os fatores externos na sua organização não permitem que você use linguagens não Java, não desista. Ferramentas, como a estrutura Spring, têm cada vez mais suporte a linguagens alternativas como Groovy ou Clojure (consulte Recursos). É possível usar essas linguagens para criar componentes e permitir que o Spring injete esses componentes nos locais adequados no aplicativo. Muitas organizações são extremamente conservadoras em relação a linguagens alternativas, contudo, existe uma rota incremental fácil através de estruturas como Spring.

No próximo capítulo, vamos resumir o tópico do uso de DSLs para coletar padrões idiomáticos de domínio com alguns exemplos no JRuby, ilustrando o quão longe é possível levar as linguagens na direção da expressividade.

Recursos

Aprender

  • The Productive Programmer (Neal Ford, O'Reilly Media, 2008): O livro mais recente do Neal Ford faz a expansão de inúmeros tópicos desta série.
  • Practically Groovy: Esta série do developerWorks explora os usos práticos do Groovy, ajudando você a saber quando e como aplicá-los com sucesso.
  • Mastering Grails: Saiba mais sobre o Grails nesta série de artigos do developerWorks.
  • "Drive development with easyb" (Andrew Glover, developerWorks, novembro de 2009): Confira esse tutorial para saber como o easyb melhora a comunicação entre desenvolvedores e outras partes interessadas.
  • Objective_C: Esse artigo da Wikipedia discute as origens das categorias com exemplos.
  • Zona de tecnologia Java do developerWorks: Localize centenas de artigos sobre todos os aspectos da programação Java.

Obter produtos e tecnologias

  • Groovy: Groovy é um dialeto Java dinâmico e moderno que é suportado por muitos dos ecossistemas corporativos Java.
  • Grails: Grails é a estrutura da Web com base no Groovy, inspirada pela Ruby on Rails.
  • easyb: easyb é uma ferramenta de teste de desenvolvimento orientada a comportamento e implementada no Groovy que usa muitas das técnicas tratadas neste capítulo.
  • Clojure: Clojure é um dialeto moderno de reconversão de Lisp como uma linguagem puramente funcional que é executada no JVM,

Discutir

  • Participe da comunidade do My developerWorks. Conecte-se a outros usuários do developerWorks ao mesmo tempo em que explora blogs, fóruns, grupos e wikis voltados aos desenvolvedores.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=516666
ArticleTitle=Arquitetura evolutiva e design emergente: Construindo DSLs no Groovy
publish-date=09032010