DSLs funcionam bem na solução para problemas de negócios emergentes, usando linguagens
específicas do domínio, pois elas
são concisas (contendo o mínimo de sintaxe de ruído possível) e legíveis
(mesmo por não desenvolvedores). Além disso elas se destacam de código mais
centrado em API.
Nesta artigo vou abrir a discussão usando
DSLs para coletar padrões idiomáticos, mostrando como desenvolver DSLs
mais sofisticadas em Ruby, efetuando leverage de JRuby.
Ruby é atualmente a linguagem mais popular para desenvolver DSLs
internas. A maior parte da infraestrutura sobre a qual você pensa ao
desenvolver em Ruby é baseada em DSL – Ruby on Rails, RSpec, Cucumber,
Rake e muitos outros – pois é responsável por hosting de DSLs internas.
E a técnica da moda
de desenvolvimento direcionado por comportamento (BDD) precisou de uma
base de DSL forte para obter sua popularidade. Esta parcela ajudará a
entender por que Ruby é tão popular entre aficionados de DSL.
Classes abertas em Ruby
Usar classes abertas para incluir novos métodos em uma classe integrada é
uma técnica comum para incluir a expressividade em DSLs. Em
Ruby, há o mesmo mecanismo, mas com uma única sintaxe. Por exemplo, para
criar uma DSL de receita, é necessário ter uma maneira para capturar
quantidades. Considere o fragmento de DSL da Listagem 1:
Listagem 1. Sintaxe de destino para minha DSL de receita baseada em Ruby
recipe = Recipe.new "Spicy bread"
recipe.add 200.grams.of "flour"
recipe.add 1.lb.of "nutmeg"
Para tornar esse código executável, deve incluir os métodos gram e lb a números abrindo a classe Numeric, conforme mostrado na Listagem 2:
Listagem 2. Definições de classes abertas em Ruby
class Numeric
def gram
self
end
alias_method :grams, :gram
def pound
self * 453.59237
end
alias_method :pounds, :pound
alias_method :lb, :pound
alias_method :lbs, :pound
Em Ruby, nomes de classes devem ser iniciados por uma letra maiúscula,
que também é a regra para constantes de Ruby – o que significa que cada
nome de classe também é uma constante.
Quando Ruby “vê” uma definição de
classe, verifica se essa classe já foi carregada em seu caminho de
classe. Como nomes de classes são constantes, é possível ter somente uma
classe de um nome específico.
Se a classe já estiver carregada, a
definição de classe reabre a classe, permitindo que eu faça as mudanças.
Na Listagem 2, eu reabro a classe Numeric (que trata de números fixos e de vírgula flutuante) para incluir os métodos gram e pound.
Diferentemente de Groovy, Ruby não tem a regra que métodos que não
aceitam parâmetros devem ser chamados com parênteses vazios, o que
significa que Ruby não precisa distinguir entre propriedades e métodos.
Ruby também inclui outro mecanismo útil de DSL: o método de classe alias_method. Você deseja aprimorar a fluência de suas DSLs o máximo possível,
sugerindo que você deve tratar de casos como pluralização. Se quiser
ver esforços elaborados para obter esse resultado, verifique o código de
pluralização em Ruby on Rails para tratar de nome de classes de modelos
de pluralização.
Não quero formar frases gramaticalmente estranhas
como recipe.add 2.gram.of(“flour”) em minha DSL quando estiver claramente adicionando mais de uma grama. O mecanismo
alias_method em Ruby facilita a criação de nomes alternativos para métodos para aprimorar a capacidade de leitura.
Para essa finalidade, a Listagem 2 inclui um método pluralizado para gram e ambas as abreviações alternativas e versões pluralizadas para pound.
Desenvolvendo interfaces fluentes
Uma das metas de usar uma DSL para capturar padrões idiomáticos é a
capacidade de eliminar sintaxe com ruído da versão da linguagem de
programação de suas abstrações. Considere o fragmento de código de DSL
de receita com ruído na Listagem 3:
Listagem 3. Definição de receita com ruído
recipe = Recipe.new "Spicy bread"
recipe.add 200.grams.of "flour"
recipe.add 1.lb.of "nutmeg"
recipe.directions << "mix ingredients"
recipe.directions << "cook for 30 minutes at 250 degrees"
Apesar de a sintaxe da Listagem 3
para incluir ingredientes e instruções da receita ser bem concisa, a
repetição com ruído é incorporada pelo nome da variável de host (recipe). Uma versão mais limpa aparece na Listagem 4:
Listagem 4. Definição de receita contextualizada
alternate_recipe = Recipe.new("Milky Gravy")
alternate_recipe.consists_of {
add 1.lb.of "flour"
add 200.grams.of "milk"
add 1.gram.of "nutmeg"
steps(
"mix ingredients",
"cook for some amount of time"
)
}
A inclusão do método consists_of na interface fluente
permite que eu use transporte de contêineres (integrado a Ruby por meio
de blocos de encerramento delimitados com chaves ({}) para
eliminar a repetição do objeto de host com ruído.
A implementação desse
método é trivial em Ruby, conforme mostrado a Listagem 5:
Listagem 5. A Definição de Classe Recipe, incluindo o método consists_of
class Recipe
attr_reader :ingredients
attr_accessor :name
attr_accessor :directions
def initialize(name="")
@ingredients = []
@directions = []
@name = name
end
def add ingredient
@ingredients << ingredient
return self
end
def steps *direction_list
@directions = direction_list.collect
end
def consists_of &block
instance_eval &block
end
end
O método consists_of aceita um bloco de códigos. Essa é a
sintaxe que você vê com o e comercial (símbolo &) antes do nome do
parâmetro. O e comercial (símbolo &) identifica o parâmetro como o
portador de um bloco de códigos.
O método executa o bloco de códigos
usando o método instance_eval, um dos métodos integrados a Ruby. O método instance_eval executa o código passado a ele alterando a definição do objeto de host.
Ou seja, ao executar código por meio de instance_eval, você altera self (a versão de Ruby de this da linguagem Java) para ser a variável que chamou instance_eval. A
ssim, é possível chamar os métodos add e steps sem usar o objeto de host recipe se os chamar com recipe.instance_eval, que é o que o método consists_of faz. Leitores regulares irão reconhecer esse conceito na aparência da sintaxe da parcela Leveraging reusable code, Part 2, reproduzida aqui na Listagem 6:
Listagem 6. Realizando a fluência de blocos de código no código Java ssando inicializadores de Instância
MarketingDescription desc = new MarketingDescriptionImpl() {{
setType("Box");
setSubType("Insulated");
setAttribute("length", "50.5");
setAttribute("ladder", "yes");
setAttribute("lining type", "cork");
}};
Apesar de a sintaxe ser extremamente semelhante, a versão Java sofre de
duas limitações sérias. Primeiro, é uma sintaxe fora do comum na
linguagem Java. A maioria dos desenvolvedores nunca encontra o
inicializador da instância na codificação do dia a dia.
Segundo, como
usa classes internas anônimas (o único mecanismo semelhante a bloco de
códigos em Java), quaisquer variáveis do escopo externo devem ser
declaradas como final, que impõe sérias limitações nos tipos de coisas que podem ser feitas dentro do bloco de códigos.
Em Ruby, o método instance_eval é um recurso de linguagem padrão (e não exótico), o que significa que é mais comumente usado.
Polindo
Uma técnica comum que muitas DSLs usam (especialmente aquelas que têm
como alvo não desenvolvedores) é efetuar leverage de idiomas falados.
Moldar a sintaxe do computador com relação a um idioma falado é possível
se sua linguagem de computador base for flexível o suficiente.
Considere a DSL de receita que criei. Criar uma DSL apenas para conter
estruturas de dados simples (como listas de ingredientes e instruções)
parece um pouco de superqualificação. Por que não manter essas
informações em estruturas de dados padrão?
Ao codificar as operações em
uma DSL, posso executar ações extras (como efeitos colaterais benéficos)
além de preencher estruturas de dados. Por exemplo, eu posso querer
capturar informações de nutrição para cada ingrediente conforme defino o
mesmo na DSL, permitindo que eu forneça um valor agregado da nutrição
da receita quando pronta.
A classe NutritionProfile é um portador de dados simples, mostrado na Listagem 7:
Listagem 7. Registro de nutrição da Receita
class NutritionProfile
attr_accessor :name, :protein, :lipid, :sugars, :calcium, :sodium
def initialize(name, protein=0, lipid=0, sugars=0, calcium=0, sodium=0)
@name = name
@protein, @lipid, @sugars = protein, lipid, sugars
@calcium, @sodium = calcium, sodium
end
def self.create_from_hash(name, h)
new(name, h['protein'], h['lipid'], h['sugars'], h['calcium'], h['sodium'])
end
def to_s()
"\tProtein: " + @protein.to_s +
"\n\tLipid: " + @lipid.to_s +
"\n\tSugars: " + @sugars.to_s +
"\n\tCalcium: " + @calcium.to_s +
"\n\tSodium: " + @sodium.to_s
end
end
Para preencher um banco de dados desses registros de nutrição, crio um arquivo de texto que contém um registro em cada linha:
ingredient "flour" has protein=11.5, lipid=1.45, sugars=1.12, calcium=20, and sodium=0
Como você pode provavelmente adivinhar, cada linha desse arquivo de
definição é uma DSL baseada em Ruby. Em vez de pensar nessa sintaxe como
apenas uma linha de texto, considere sua “aparência” do ponto de vista
de uma linguagem de computador, conforme mostrado na Figura 1.
Figura 1. Definição de texto de ingrediente como uma Chamada de Método

Cada linha começa com ingredient, que é o nome do método. O primeiro parâmetro é o nome do ingrediente. A palavra has é chamada de uma palavra bolha – uma palavra que torna a DSL mais legível, mas não contribui para a
definição final.
O restante da linha consiste em pares de nome/valor,
separados por vírgulas. Considerando que isso não é ainda sintaxe Ruby
legal, como faço para converter em Ruby? Essa tarefa é chamada polindo: pegar sintaxe quase legal e poli-la para virar sintaxe real.
A tarefa de polir essa DSL é tratada pela classe NutritionProfileDefinition, mostrada na Listagem 8:
Listagem 8. Classe NutritionProfileDefinition
class NutritionProfileDefinition
def polish_text(definition_line)
polished_text = definition_line.clone
polished_text.gsub!(/=/, '=>')
polished_text.sub!(/and /, '')
polished_text.sub!(/has /, ',')
polished_text
end
def process_definition(definition)
instance_eval polish_text(definition)
end
def ingredient(name, ingredients)
NutritionProfile.create_from_hash name, ingredients
end
end
O ponto de entrada dessa classe é o método process_definition, mostrando na Listagem 9:
Listagem 9. O Método process_definition
def process_definition(definition)
instance_eval polish_text(definition)
end
Esse método chama polish_text usando instance_eval, alternando o contexto de execução de polish_text para a instância NutritionProfileDefinition. O método
polish_text, mostrado na Listagem 10, faz as substituições e conversões necessárias para converter o quase código em código:
Listagem 10. O método polish_text
def polish_text(definition_line)
polished_text = definition_line.clone
polished_text.gsub!(/=/, '=>')
polished_text.sub!(/and /, '')
polished_text.sub!(/has /, ',')
polished_text
end
O método polish_text consiste em substituições de cadeias
de caracteres simples para converter a sintaxe de definição em sintaxe
Ruby, convertendo o sinal de igual em um identificador hash (=>), livrando-se das instâncias em excesso da palavra and e convertendo has em uma vírgula.
Essa linha de código polida é passada para instance_eval, executando-a por meio do método ingredient da classe
NutritionProfileDefinition.
Esse código pode ser escrito na linguagem Java, mas as limitações
sintáticas de Java incluiriam tanto ruído que você perderia os
benefícios da interface fluente, renderizando o simulado do exercício.
Ruby oferece açúcar sintático suficiente para tornar factível (e
desejável) efetuar cast de abstrações como DSLs.
Método ausente
Diferentemente do exemplo anterior, o próximo não pode ser feito em
código Java, mesmo com sintaxe inadequada. Um mecanismo conveniente em
linguagens que comumente hospedam DSLs é método ausente.
Ao
chamar um método que não existe em Ruby, ela não gera uma exceção
imediatamente. Você tem a oportunidade de incluir um método method_missing em sua classe que tratará de quaisquer chamadas a métodos ausentes.
Isso é muito usado em DSLs que desenvolvem estruturas de dados internas.
Considere este exemplo de XMLBuilder em Ruby, mostrado na Listagem 11:
Listagem 11. Usando XMLBuilder em Ruby
xml = Builder::XmlMarkup.new(:indent => 2)
xml.person {
xml.name("Neo")
xml.catch_phrase("Whoa")
}
puts xml.target!
Esse código emite saída de um documento XML com a estrutura mostrada na DSL. O Builder trabalha sua mágica por meio de method_missing. Ao chamar um método na variável xml, esse método ainda não existe, então ele cai em method_missing
que constrói o XML correspondente.
Isso torna o código para a
biblioteca do Builder muito pequena. A maioria de seus mecanismos
depende dos recursos de linguagem subjacentes de Ruby. Um problema
permanece com esta abordagem, no entanto, conforme ilustrado na Listagem
12:
Listagem 12. Colisões de Método Ausente com Métodos Integrados
xml = Builder::XmlMarkup.new(:indent => 2)
xml.person {
xml.name("Neo")
xml.catch_phrase("Whoa")
xml.class("pod-born")
}
puts xml.target!
Se depender exclusivamente de method_missing, o código da Listagem 12 não funcionará, pois o método class já está definido em Ruby como parte de Object, que (como na linguagem Java) é a classe base para todas as classes.
Obviamente, method_missing
não funcionará com métodos existentes. Isso pode parecer condenar essa
abordagem. No entanto, Jim Weirich (o criados de Builder), apareceu com
uma solução elegante: ele criou BlankSlate. BlankSlate é uma classe que herda de Object, mas programaticamente remove todos os métodos normalmente encontrados em Object. Isso permite que ele efetue leverage da infraestrutura method_missing sem quaisquer efeitos colaterais irritantes.
Esse mecanismo BlankSlate é tão poderoso e útil que foi integrado à próxima versão principal de Ruby. Em Ruby 1.9, SimpleObject torna-se o ponto mais alto da hierarquia de objeto – com Object como seu descendente imediato. Ter SimpleObject torna desenvolver DSLs de builder muito mais simples, pois você não precisará mais de BlankSlate.
A capacidade de criar uma DSL como Builder ilustra por que a
expressividade e o poder nas linguagens são tão críticos.
A quantia de código no Builder de Ruby é muito menor do que em
bibliotecas semelhantes de outras linguagens, pois foi escrito sobre um
meio de design mais flexível: Ruby.
Builders em Groovy vs. Ruby
A inspiração para a classe Builder em Ruby veio de classes builders
semelhantes em Groovy. Jim Weirich, o criador de Builder em Ruby,
gostava do conceito mas não da implementação em Groovy, pois usa uma
estratégia de mapeamento elaborada entre as tags XML e o XML gerado.
Weirich criou XMLBuilder (e BlankSlate) como
uma solução mais simples e mais elegante para o problema. Isso é
interessante porque oferece uma visão de como as comunidades de
linguagem tendem a solucionar problemas.
Em geral, a comunidade Java
tende a desenvolver elementos estruturais (como estruturas e padrões de
design) para solucionar problemas, desenvolvendo camadas de abstração
sobre camadas.
Em Ruby, desenvolvedores tendem a usar metaprogramação
para desenvolver para baixo, usando o mecanismo subjacente mais simples
para o qual podem efetuar leverage.
Contraste qualquer estrutura da Web
Java com Ruby on Rails ou compare builders, nos quais Weirich usou
metaprogramação para remover o que não precisava, permitindo que ele
efetuasse leverage de um recurso de linguagem.
Conclusão
Insisto sempre que o design de sistemas
de software engloba seu código de origem completo, o que sugere que você
tem uma paleta de design mais ampla se usar linguagens mais
expressivas.
Isso se aplica não somente à sua opção da linguagem de
propósito geral (Java, Ruby, Groovy, Clojure), mas também às linguagens
que podem ser escritas sobre sua linguagem base usando DSLs.
Desenvolver
uma linguagem que expresse os conceitos de seus negócios exatamente
torna-se um ativo valioso para sua organização: você está capturando
maneiras importantes de solucionar problemas reais em uma linguagem
altamente adequada para o propósito.
Mesmo se sua organização não alternar para uma linguagem como Ruby ou
Groovy para a maior parte do desenvolvimento, você pode “inserir de
penetra” essas linguagens usando ferramentas implementadas nelas, como
RSpec e easyb.
Trazendo essas linguagens alternativas pela porta dos fundos, é
possível ajudar aqueles que são desnecessariamente cautelosos em
apresentar novas linguagens a entenderem que elas oferecem benefícios
significativos.
Recursos
Aprender
- The Productive Programmer (Neal Ford, O’Reilly Media, 2008): O livro mais recente de Neal Ford expande vários tópicos desta série.
- JRuby: JRuby é uma das melhores implementações de Ruby em qualquer plataforma.
- Ruby on Rails: Rails é uma plataforma de desenvolvimento da Web popular que usa muitas DSLs.
- RSpec: RSpec é uma estrutura de testes BDD escrita em Ruby que usa técnicas DSL.
- Rake: Rake é a ferramenta de desenvolvimento para a plataforma Ruby.
- Cucumber: Cucumber é uma estrutura de testes BDD poderosa que demonstra muitas técnicas poderosas de DSL em Ruby.
- easyb: easyb é uma estrutura BDD baseada em Groovy para a plataforma Java.
- Builder: Builder é uma biblioteca de Ruby que facilita gerar documentos XML programaticamente.
- zona de tecnologia Java do developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Discutir
- Participe da comunidade My developerWorks.
Entre em contato com outros usuários do developerWorks e explore os
blogs, fóruns, grupos e wikis voltados para desenvolvedores.
***
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.







