Criando uma DSL

A um tempo atrás escrevi um post sobre Fluent Interface e surgiram algumas discussões sobre a fluência, como por exemplo:

  • Restrições de tecnologia (no caso o Java)
  • Um trabalho que pode ser chato de implementar
  • O código demonstrado no post ainda não parecia ser muito fluente.

Concordo com essas questões e realmente tem como resolver esses problemas. Caso eu precisar dessa fluência no meu projeto eu poderia utilizar de recursos como Groovy e Clousures deixando o código muito mais fluente e intuitivo. Mas antes de chegar nesse ponto é interessante entender o que é uma DSL. É interessante notar que algumas DSLs nós já usamos a muito tempo e nem percebemos!

Para quem já trabalhou com aplicações a chance de ter deparado com uma DSL é grande. Um arquivo de configuração é uma DSL, um makefile ou um build que especifica regras e dependências do projeto é uma DSL. Se você já escreveu algum desses arquivos você já teve seus primeiros passos com DSL.

DSL, portanto, é uma linguagem de domínio de fácil entendimento por quem conhece esse domínio.

Uma pessoa que é familiar à uma linguagem de domínio deve entendê-la facilmente. Por exemplo, se você está criando uma DSL para atores do seu sistema expressarem uma regra de negócio, você não vai querer que eles gastem tempo para entender a complexidade da linguagem. Você quer que eles foquem em detalhar as regras de negócio. A DSL que você criar para eles teve ter o vocabulário e os termos usados por esses atores no dia-dia, sem se preocuparem com a sintaxe de uma linguagem. Por exemplo, para alguém que entende de multiplicação de matrizes, matrixA.multiply(matrixB) é menos expressivo e conciso do que simplesmente matrixA * matrixB. O ganho do uso de uma DSL é prover uma interface gráfica ou textual que permitam usuários interagirem com sua aplicação. O resultado disso é uma maior produtividade e flexibilidade no sistema.

DSL Interna e Externa

Uma DSL pode ser classificada como interna ou externa, dependendo de como ela é desenhada e implementada.

Uma DSL externa é independente de uma linguagem de programação em particular. Por exemplo ANTLR.

Uma DSL interna, por outro lado, depende da linguagem de programação. A boa notícia é que você não precisa se preocupar com a gramática, parser ou alguma ferramenta. Porém a DSL é limitada por restrições da linguagem. Outro problema é que dependendo da gramática você terá que criar um compilador para parsear, processar a sintaxe e mapear na semântica que você espera, o que pode ser um pouco trabalhoso.

Exemplos de DSL:

 

  • Em CSS 
A:hover { color: #FF0000; text-decoration: none; }

  • Em makefile
GCC=cxx -I.
all : clean compile
compile : myprog
myprog: MyProg.cc Util.o
    $(GCC) -o myprog MyProg.cc Util.o
Util.o : Util.h Util.cc
    $(GCC) -c Util.cc
clean :
    /bin/rm -f myprog Util.o

  • Em ANT
<project name="AnExampleProject" default="jarit" basedir=".">
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="distrib" location="distrib"/>
<target name="compile" description="compile your Java code from src into build" >
<javac srcdir="${src}" destdir="${build}"/>
</target>
<target name="jarit" depends="compile" description="jar it up" >
<jar jarfile="${distrib}/AnExampleProject.jar" basedir="${build}"/>
</target>
</project>

  • Em Rake (Makefile no Ruby)
ORIGINAL = 'input.dat'
BACKUP = 'input.dat.bak'
task :default => BACKUP
file BACKUP => ORIGINAL do |task|
cp task.prerequisites[0], task.name
end

  • Em Gant (Make no Groovy)
#slightly modified version of example from http://gant.codehaus.org/
includeTargets << gant.targets.Clean
cleanPattern << [ '**/*' , '**/*.bak' ]
cleanDirectory << 'build'
target (stuff : 'A target to do some stuff') {
println 'Stuff'
depends clean
echo message : 'A default message from Ant'
otherStuff()
}
target (otherStuff : 'A target to do some other stuff') {
println 'OtherStuff'
echo message : 'Another message from Ant'
clean()
}
setDefaultTarget stuff


Usando DSL para Validação.
ActiveRecord em Rails:

class Person
validates_presence_of :first_name, :last_name, :ssn
validates_uniqueness_of :ssn
end

GORM em Grails:

class State
{
String twoLetterCode
static constrains = {
twoLetterCode size: 2..2, blank: false, unique: true }
}

 

 

 

O ganho de linguagens dinâmicas como Ruby e Groovy, podendo ainda utilizar frameworks como Rails e Grails, torna a linguagem muito mais expressiva e clara do que linguagens mais burocráticas como o Java e C#.

easyb é um framework orientado ao comportamento do software (escrevi sobre isso nesse post) que permite usuários escrever histórias que expressem e validem o comportamento do software. Validação no easyb é baseado em history DSL. Exemplo:

//transferMoney.story
scenario 'transfer money', {
given 'account numbers 123456789 and 123456788'
when 'transfer $50 from 123456789 to 123456788'
then 'balance of 123456789 is $50 less'
and
then 'balance of 123456788 is $50 more'
and
then 'transaction has been logged...'
}

Esse código acima é uma documentação executável e pode ser executada com o seguinte comando:

java -classpath ... org.disco.easyb.BehaviorRunner transferMoney.story

Isso vai produzir o seguinte resultado:

Running transfer money story (transferMoney.story)
Scenarios run: 1, Failures: 0, Pending: 1, Time Elapsed: 0.431 sec
1 behavior run with no failures

Martin Fowler e Eric Evans descrevem uma Fluent Interface como uma interface bem desenhada para o uso humano.  Um exemplo de Fluent Interface é o EasyMock. Com ele podemos fluentemente expecificar o retorno de um método:

expect(alarm.raise()).andReturn(true);
expect(alarm.raise()).andThrow(new InvalidStateException());


Outro exemplo é o JSONObject:


JSONObject json = new JSONObject();
json.accumulate("key1", "value1");
json.accumulate("key2", "value2");

Ou melhor que isso, sem precisar usar o 'json.' 

JSONObject json = new JSONObject().accumulate("key1", "value1").accumulate("key2", "value2");

No Guice, um framework de Injeção de Dependência, podemos fazer :

bind(Alerter.class)

.to(VisualAlerter.class) .in(Scopes.SINGLETON);

 



Um ArrayList em Java pode ser escrito assim: 

java.util.ArrayList<String> cart = new java.util.ArrayList<String>(); 

cart.add("Milk"); 

cart.add("Juice"); 

cart.add("Apple");   

System.out.printf("My cart has %d items.", cart.size());  

 

Já esse mesmo código, em Groovy:

cart = []

cart.with {
  add "Milk"
  add "Juice"
  add "Apple"

  println "My cart has $size items."
}


Agora em Javascript:

cart = new java.util.ArrayList()

with(cart)
{
  add("Milk")
  add("Juice")
  add("Apple")

  println("My cart has " + size() + " items.")
}


Em Java podemos escrever o código abaixo:

PizzaBuilder bldr = new PizzaBuilder();
    bldr.setDough();
    bldr.setSauce(2);
    bldr.setCheese(2, "Mozzarella");
    bldr.setToppings(new String[] {"Olives", "Tomatoes", "Bell Peppers"});
    bldr.bake();
    Pizza pizza = bldr.get();

Podemos melhorar esse código para:

Pizza pizza = new PizzaBuilder()
      .prepareDough()
      .addSauce(2)
      .addCheese(2, "Mozzarella")
      .addToppings(new String[] {"Olives", "Tomatoes", "Bell Peppers"})
      .bake()
      .get();

Esse mesmo código, se utilizarmos Groovy e clousures pode ser facilmente escrito assim:

Pizza pizza = PizzaBuilder.make {
        prepareDough()
        addSauce(2)
        addCheese(2, "Mozzarella")
        addToppings("Olives", "Tomatoes", "Bell Peppers")
        bake()

Ou mesmo sem os parênteses:


Pizza pizza = PizzaBuilder.make {
        prepareDough()
        addSauce 2
        addCheese 2, Mozzarella
        addToppings Olives, Tomatoes, Bell_Peppers
        bake()


A classe PizzaBuilder ficaria assim:

 

 

public class PizzaBuilder
{
  def Mozzarella = "Mozzarella"
  def Olives = "Olives"
  def Tomatoes = "Tomatoes"
  def Bell_Peppers = "Bell Peppers"


  void prepareDough() { }
  void addSauce(int amount) {  }
  void addCheese(int amount, String type) { }
  void addToppings(String[] toppings) { }
  void bake() { }
  Pizza get() { return null; }
static Pizza make(closure)   {     

PizzaBuilder bldr = new PizzaBuilder();

bldr.with closure 

return bldr.get();   

}

}

Para quem se interessar mais sobre o assunto as fontes são:

http://www.javaworld.com/javaworld/jw-06-2008/jw-06-dsls-in-java-1.html

http://www.javaworld.com/javaworld/jw-07-2008/jw-07-dsls-in-java-2.html



Publicado em DSL. 3 Comments »