No artigo anterior, 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. 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.
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 – ao final do artigo, consulte a seção Recursos e saiba mais informações.
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 {<br /> def expected = ["event_map" : "eventMap",<br /> "name" : "name", "test_date" : "testDate",<br /> "test_string_with_lots_of_breaks" : "testStringWithLotsOfBreaks",<br /> "String_that_has_init_cap" : "stringThatHasInitCap" ]<br /><br /> void test_Camelize() {<br /> use (StringCategory) {<br /> expected.each { key, value -><br /> assertEquals value, key.camelize()<br /> }<br /> }<br /> }<br />}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 {<br /><br /> static String camelize(String self) {<br /> def newName = self.split("_").collect() {<br /> it.substring(0, 1).toUpperCase() + it.substring(1, it.length())<br /> }.join()<br /> newName.substring(0, 1).toLowerCase() + newName.substring(1, newName.length())<br /> }<br />}
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()<br /><br />use (IntegerWithTimeSupport) {<br /> calendar.add new Appointment("Dentist").from(4.pm)<br /> calendar.add new Appointment("Conference call")<br /> .from(5.pm)<br /> .to(6.pm)<br /> .at("555-123-4321")<br />}<br />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étodo add()).
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 Integer 5 é realmente um) 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 {<br /> static Calendar getFromToday(Integer self) {<br /> def target = Calendar.instance<br /> target.roll(Calendar.DAY_OF_MONTH, self)<br /> return target<br /> }<br /><br /> static Integer getAm(Integer self) {<br /> self == 12 ? 0 : self<br /> }<br /><br /> static Integer getPm(Integer self) {<br /> self == 12 ? 12 : self + 12<br /> }<br />}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 {<br /> def name;<br /> def location;<br /> def date;<br /> def startTime;<br /> def endTime;<br /><br /> Appointment(apptName) {<br /> name = apptName<br /> date = Calendar.instance<br /> }<br /><br /> def at(loc) {<br /> location = loc<br /> this<br /> }<br /><br /> def formatTime(time) {<br /> time > 12 ? "${time - 12} PM" : "${time} AM"<br /> }<br /><br /> def getStartTime() {<br /> formatTime(startTime)<br /> }<br /><br /> def getEndTime() {<br /> formatTime(endTime)<br /> }<br /><br /> def from(start_time) {<br /> startTime = start_time<br /> date.set(Calendar.HOUR_OF_DAY, start_time)<br /> this<br /> }<br /><br /> def to(end_time) {<br /> endTime = end_time<br /> date.set(Calendar.HOUR_OF_DAY, end_time)<br /> this<br /> }<br /><br /> def display() {<br /> print "Appointment: ${name}, Starts: ${formatTime(startTime)}"<br /> if (endTime) print ", Ends: ${formatTime(endTime)}"<br /> if (location) print ", Location: ${location}"<br /> println()<br /> }<br />}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 mais informações em
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()<br /><br />calendar.add new Appointment("Dentist")<br /> .from(4.pm)<br />calendar.add new Appointment("Conference call")<br /> .from(5.pm)<br /> .to(6.pm)<br /> .at("555-123-4321")<br /><br />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 = { -><br /> delegate == 12 ? 0 : delegate<br />}<br /><br />Integer.metaClass.getPm = { -><br /> delegate == 12 ? 12 : delegate + 12<br />}<br /><br />Integer.metaClass.getFromToday = { -><br /> def target = Calendar.instance<br /> target.roll(Calendar.DAY_OF_MONTH, delegate)<br /> target<br />}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 expandindo
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
é 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<br /><br />import org.easyb.bdd.Queue<br /><br />description "This is how a Queue must work"<br /><br />before "initialize the queue for each spec", {<br /> queue = new Queue()<br />}<br /><br />it "should dequeue item just enqueued", {<br /> queue.enqueue(2)<br /> queue.dequeue().shouldBe(2)<br />}<br /><br />it "should throw an exception when null is enqueued", {<br /> ensureThrows(RuntimeException.class) {<br /> queue.enqueue(null)<br /> }<br />}<br /><br />it "should dequeue items in same order enqueued", {<br /> [1..5].each {val -><br /> queue.enqueue(val)<br /> }<br /> [1..5].each {val -><br /> queue.dequeue().shouldBe(val)<br /> }<br />}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(<em>spec</em>, <em>closure</em>)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) {<br /> stepStack.startStep(listener, BehaviorStepType.IT, spec)<br /> closure.delegate = new EnsuringDelegate()<br /> try {<br /> if (beforeIt != null) {<br /> beforeIt()<br /> }<br /> listener.gotResult(new Result(Result.SUCCEEDED))<br /><strong> use(BehaviorCategory) {<br /><em> closure</em>()<br /> }</strong>
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) {<br /> isEqual(self, value, msg)<br />}<br /><br />private static void isEqual(self, value, String msg) {<br /> if (self.getClass() == NullObject.class) {<br /> if (value != null) {<br /> throwValidationException(<br /> "expected ${value.toString()} but target object is null", msg)<br /> }<br /> } else if (value.getClass() == String.class) {<br /> if (!value.toString().equals(self.toString())) {<br /> throwValidationException(<br /> "expected ${value.toString()} but was ${self.toString()}", msg)<br /> }<br /> } else {<br /> if (value != self) {<br /> throwValidationException("expected ${value} but was ${self}", msg)<br /> }<br /> }<br />}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.
É 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.
Aguarde o próximo capítulo e 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.
artigo publicado originalmente no developerWorks Brasil, por Neal Ford
Neal 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.







