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#.
O 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