Os padrões idiomáticos podem ser técnicos ou de domínio. Os padrões técnicos representam soluções para os problemas comuns de software técnico, como a manipulação de dados de validação, de segurança e transacionais no seu aplicativo (ou conjunto de aplicativos). Os artigos anteriores focaram na coleta de padrões idiomáticos técnicos usando técnicas como a metaprogramação.Os padrões de domínio consideram como simplificar os problemas comuns de negócios. Enquanto os padrões técnicos estão presentes em praticamente todos os tipos de software, os padrões de domínio são distintos, assim como os negócios. No entanto, existe um rico conjunto de técnicas para coletá-los, que são o assunto deste e dos próximos artigos desta série.
Este artigo fornece motivação para uso das técnicas de DSL como um estilo de abstração para coleta de padrões de domínio. As DSLs oferecem uma diversidade de opções, incluindo a própria nomenclatura padrão. O livro mais recente de Martin Fowler é uma investigação profunda das técnicas de DSL (consulte a seção Recursos). Eu irei usar muitos dos nomes de padrão dele e uma combinação dos meus exemplos e dos exemplos dele nos artigos subsequentes ao mostrar técnicas específicas.
Por que criar uma DSL somente para coletar padrões idiomáticos? Conforme mencionei no artigo "Aproveitando códigos reutilizáveis, Parte 2", uma das melhores formas de distinguir um padrão idiomático do resto do seu código é mudando sua aparência. Essa diferenciação visual é uma pista imediata de que você não está olhando para uma API regular. De forma similar, um dos objetivos do uso de DSLs é escrever códigos que se parecem menos com o código de origem e mais com o problema que você está tentando resolver. Se for possível atingir esse objetivo (ou mesmo chegar mais perto do objetivo do que você está agora), você preencherá uma lacuna importante da maioria dos projetos de software: a comunicação entre os desenvolvedores e as partes interessadas de negócios. Permitir aos usuários a leitura do seu código é um excelente benefício, pois elimina a necessidade de conversão do seu código para o idioma local, uma tarefa que induz ao erro. Ao tornar o seu código legível para pessoas não técnicas que conhecem a finalidade do software, é possível conversar mais claramente com eles.
Com o objetivo de motivar o uso desta técnica, irei usar um dos exemplos do Fowler do seu livro sobre DSL (consulte a seção Recursos). Digamos que eu trabalhe para uma empresa que cria compartimentos secretos controlados por software (pense no James Bond). Um dos clientes da minha empresa, a Sra. H., deseja a instalação de um compartimento secreto no seu quarto. No entanto, minha empresa usa torradeiras equipadas com Java™, que sobraram do fiasco da comercialização on-line, para executar o software. Apesar das torradeiras serem baratas, a colocação do software nelas é cara. Então, eu preciso criar o código básico do compartimento secreto e colocá-lo permanentemente nas torradeiras, e descobrir uma maneira de configurar as necessidades do compartimento secreto de cada cliente individualmente. Como você pode ver, esse é um problema comum no mundo de software moderno: comportamentos gerais que não são alterados com frequência, complementados com configurações que são alteradas para circunstâncias individuais.
A Sra. H. deseja um compartimento secreto que abre quando a porta do quarto é fechada pela primeira vez, e em seguida abre a segunda gaveta da penteadeira e, por fim, acende a luz da cabeceira da cama. Essas atividades devem acontecer na sequência, e se alguma coisa quebrar a sequência, será necessário reiniciar do começo. É possível imaginar o software que controla o compartimento secreto da Sra. H. como a máquina de estado ilustrada na Figura 1:
Figura 1. Compartimento secreto da Sra. H. como uma máquina de estado
A API da máquina de estado subjacente é simples. Eu criei uma classe de eventos abstratos, mostrada na Listagem 1, que manipula os eventos e os comandos na máquina de estado:
Listagem 1. Eventos abstratos para máquina de estado
public class AbstractEvent {
private String name, code;
public AbstractEvent(String name, String code) {
this.name = name;
this.code = code;
}
public String getCode() { return code;}
public String getName() { return name;}
|
É possível modelar os estados na máquina de estado com outra classe simples chamada States, mostrada na Listagem 2:
Listagem 2. O início da classe da máquina de estado
public class States {
private State content;
private List<TransitionBuilder> transitions = new ArrayList<TransitionBuilder>();
private List<Commands> commands = new ArrayList<Commands>();
public States(String name, StateMachineBuilder builder) {
super(name, builder);
content = new State(name);
}
State getState() {
return content;
}
public States actions(Commands... identifiers) {
builder.definingState(this);
commands.addAll(Arrays.asList(identifiers));
return this;
}
public TransitionBuilder transition(Events identifier) {
builder.definingState(this);
return new TransitionBuilder(this, identifier);
}
void addTransition(TransitionBuilder arg) {
transitions.add(arg);
}
void produce() {
for (Commands c : commands)
content.addAction(c.getCommand());
for (TransitionBuilder t : transitions)
t.produce();
}
}
|
A Listagem 1 e a Listagem 2 estão aqui somente para referência. O problema interessante a ser resolvido é a representação da configuração da máquina de estado. Essa representação é um padrão idiomático para o negócio de instalação de compartimentos secretos. A Listagem 3 mostra uma configuração baseada em Java para a máquina de estado:
Listagem 3. Uma opção de configuração: Código Java
Event doorClosed = new Event("doorClosed", "D1CL");
Event drawerOpened = new Event("drawerOpened", "D2OP");
Event lightOn = new Event("lightOn", "L1ON");
Event doorOpened = new Event("doorOpened", "D1OP");
Event panelClosed = new Event("panelClosed", "PNCL");
Command unlockPanelCmd = new Command("unlockPanel", "PNUL");
Command lockPanelCmd = new Command("lockPanel", "PNLK");
Command lockDoorCmd = new Command("lockDoor", "D1LK");
Command unlockDoorCmd = new Command("unlockDoor", "D1UL");
State idle = new State("idle");
State activeState = new State("active");
State waitingForLightState = new State("waitingForLight");
State waitingForDrawerState = new State("waitingForDrawer");
State unlockedPanelState = new State("unlockedPanel");
StateMachine machine = new StateMachine(idle);
idle.addTransition(doorClosed, activeState);
idle.addAction(unlockDoorCmd);
idle.addAction(lockPanelCmd);
activeState.addTransition(drawerOpened, waitingForLightState);
activeState.addTransition(lightOn, waitingForDrawerState);
waitingForLightState.addTransition(lightOn, unlockedPanelState);
waitingForDrawerState.addTransition(drawerOpened, unlockedPanelState);
unlockedPanelState.addAction(unlockPanelCmd);
unlockedPanelState.addAction(lockDoorCmd);
unlockedPanelState.addTransition(panelClosed, idle);
machine.addResetEvents(doorOpened);
|
A Listagem 3 destaca diversos problemas com o uso do código Java para a configuração da máquina de estado. Em primeiro lugar, não fica óbvio na leitura que essa é a configuração de uma máquina de estado. Como ocorre com muitas APIs Java, é uma pilha de códigos que não são diferenciados. Em segundo lugar, ela é muito detalhada e repetitiva. Por exemplo: os nomes das variáveis são usados repetidamente à medida que defino diversos estados e transições para cada parte da máquina de estado. Toda essa duplicação dificulta a leitura do código. Em terceiro lugar, esse código não atinge o objetivo original de configuração dos compartimentos secretos sem a recompilação do código.
Na verdade, quase não se vê códigos como esse no mundo Java, que prefere usar XML para o código de configuração. A composição da configuração em XML é simples, conforme mostrado na Listagem 4:
Listagem 4. Configuração da máquina de estado em XML
<stateMachine start = "idle">
<event name="doorClosed" code="D1CL"/>
<event name="drawerOpened" code="D2OP"/>
<event name="lightOn" code="L1ON"/>
<event name="doorOpened" code="D1OP"/>
<event name="panelClosed" code="PNCL"/>
<command name="unlockPanel" code="PNUL"/>
<command name="lockPanel" code="PNLK"/>
<command name="lockDoor" code="D1LK"/>
<command name="unlockDoor" code="D1UL"/>
<state name="idle">
<transition event="doorClosed" target="active"/>
<action command="unlockDoor"/>
<action command="lockPanel"/>
</state>
<state name="active">
<transition event="drawerOpened" target="waitingForLight"/>
<transition event="lightOn" target="waitingForDrawer"/>
</state>
<state name="waitingForLight">
<transition event="lightOn" target="unlockedPanel"/>
</state>
<state name="waitingForDrawer">
<transition event="drawerOpened" target="unlockedPanel"/>
</state>
<state name="unlockedPanel">
<action command="unlockPanel"/>
<action command="lockDoor"/>
<transition event="panelClosed" target="idle"/>
</state>
<resetEvent name = "doorOpened"/>
</stateMachine>
|
O código na Listagem 4 possui diversas vantagens em relação à versão Java. Em primeiro lugar, existe a ligação tardia, o que significa que é possível fazer alterações no código de configuração e colocá-las na torradeira, permitindo a um analisador XML a leitura da nova configuração. Em segundo lugar, para esse problema específico, esse código é muito mais expressivo, pois o XML inclui o conceito de contêiner: os estados incluem suas configurações como elementos filho. Isso ajuda a remover a redundância irritante que está presente na versão Java. Em terceiro lugar, esse código é naturalmente declarativo. Muitas vezes, o código declarativo é mais fácil de ler se estiver somente fazendo instruções e for utilizar as sintaxes if e while .
Retroceda um pouco e pense nas implicações. A externalização da configuração é um padrão tão comum no mundo Java moderno que nem pensamos nela mais como uma entidade distinta. No entanto, esse é um recurso de praticamente todas as estruturas Java. A configuração é um padrão idiomático e precisamos buscar formas de capturá-lo separando-o e diferenciando-o do comportamento geral da estrutura circundante. Ao usar o XML para a configuração, estou escrevendo o código em uma DSL externa (a sintaxe é XML e a gramática é definida pelo esquema associado a esse documento XML), então não é necessário recompilar o meu código de estrutura para alterá-lo.
Não é necessário ir até o fim em XML para obter as vantagens do XML. Considere a versão do código de configuração mostrado na Listagem 5:
Listagem 5. Uma configuração de máquina de estado com gramática customizada
events
doorClosed D1CL
drawerOpened D2OP
lightOn L1ON
doorOpened D1OP
panelClosed PNCL
end
resetEvents
doorOpened
end
commands
unlockPanel PNUL
lockPanel PNLK
lockDoor D1LK
unlockDoor D1UL
end
state idle
actions {unlockDoor lockPanel}
doorClosed => active
end
state active
drawerOpened => waitingForLight
lightOn => waitingForDrawer
end
state waitingForLight
lightOn => unlockedPanel
end
state waitingForDrawer
drawerOpened => unlockedPanel
end
state unlockedPanel
actions {unlockPanel lockDoor}
panelClosed => idle
end
|
Essa versão do código possui muitos benefícios da versão XML: é declarativa, possui o conceito de contêiner e é concisa. Ela é mais vantajosa em comparação as versões XML e Java, pois possui menos caracteres de ruído (como < e >) que são necessários para a implementação técnica, mas prejudicam a capacidade de leitura.
Essa versão do código de configuração é uma DSL externa customizada escrita usando ANTLR, uma ferramenta de software livre que facilita a composição de idiomas customizados (consulte a seção Recursos). Aqueles que ainda têm pesadelos com a classe de compilador apresentada na universidade (incluindo ferramentas clássicas como Lex e YACC) ficarão felizes de saber que as ferramentas foram muito aprimoradas. Esse exemplo é do livro do Fowler e, segundo ele, o desenvolvimento da versão XML e o desenvolvimento da versão da linguagem customizada demoram praticamente a mesma quantidade de tempo.
A Listagem 6 contém outra alternativa, escrita em Ruby:
Listagem 6. Configuração da máquina de estado em JRuby
event :doorClosed, "D1CL"
event :drawerOpened, "D2OP"
event :lightOn, "L1ON"
event :doorOpened, "D1OP"
event :panelClosed, "PNCL"
command :unlockPanel, "PNUL"
command :lockPanel, "PNLK"
command :lockDoor, "D1LK"
command :unlockDoor, "D1UL"
resetEvents :doorOpened
state :idle do
actions :unlockDoor, :lockPanel
transitions :doorClosed => :active
end
state :active do
transitions :drawerOpened => :waitingForLight,
:lightOn => :waitingForDrawer
end
state :waitingForLight do
transitions :lightOn => :unlockedPanel
end
state :waitingForDrawer do
transitions :drawerOpened => :unlockedPanel
end
state :unlockedPanel do
actions :unlockPanel, :lockDoor
transitions :panelClosed => :idle
end
|
Esse é um bom exemplo de uma DSL interna : uma DSL que usa a sintaxe da linguagem de base, o que significa que essa DSL deve ser um código Ruby válido, no que diz respeito à sintaxe. (Pois é escrito em Ruby, é possível executá-lo através do JRuby, o que significa que todas as suas torradeiras precisam ter o arquivo JRuby JAR.)
A Listagem 6 possui muitas vantagens da linguagem customizada. Observe o uso intenso de blocos de Ruby para agir como contêineres, o que fornece o mesmo tipo de contêiner semântico do XML e das versões de linguagens customizadas. Ele usa mais caracteres de ruído do que a linguagem customizada. Por exemplo: o prefixo : no Ruby indica um símbolo, que nesse caso é basicamente uma cadeia de caractere imutável usada como um identificador.
A implementação desse tipo de DSL em Ruby é bem simples, conforme mostrado na Listagem 7:
Listagem 7. Definição de classe parcial para a DSL JRuby
class StateMachineBuilder
attr_reader :machine, :events, :states, :commands
def initialize
@events = {}
@states = {}
@state_blocks = {}
@commands = {}
end
def event name, code
@events[name] = Event.new(name.to_s, code)
end
def state name, &block
@states[name] = State.new(name.to_s)
@state_blocks[name] = block
@start_state ||= @states[name]
end
def command name, code
@commands[name] = Command.new(name.to_s, code)
end
|
O Ruby possui regras flexíveis sobre sintaxe, o que o torna apropriado para esse tipo de DSL. Por exemplo: ao declarar um evento, não é necessário incluir os parênteses como parte da chamada de método. Nessa versão, não é necessário escrever sua própria linguagem ou se prejudicar com sinais de maior e menor. Isso ajuda a ilustrar porque essa abordagem é tão popular no mundo Ruby.
A DSL oferece uma boa sintaxe alternativa para coleta de padrões idiomáticos. De acordo com a definição de Martin Fowler, as DSLs possuem cinco características principais.
Linguagem de programação de computador
Para ser uma DSL, uma linguagem precisa ser uma linguagem de programação de computador. Sem essa restrição em vigor, é fácil chegar a um caminho perigoso no qual todos os itens encontrados são uma DSL. Se o termo DSL for definido dessa forma abrangente, todas as conversas contextualizadas seriam DSLs. Por exemplo: eu possuo colegas que são fãs de críquete. Quando eu passo por eles quando eles estão conversando sobre críquete, não consigo entender o que eles estão falando, apesar deles estarem usando palavras em inglês. Eu não possuo o contexto apropriado para entender a forma que eles estão usando as palavras. Então, seria possível argumentar que críquete e outros esportes possuem DSLs nas suas terminologias. Mas com essa definição ampla fica difícil limitá-la para restrições úteis — por isso a insistência do Fowler com a restrição para linguagens de programação de computador.
O segundo critério do Fowler para as DSLs é que elas tenham uma "natureza de linguagem", o que significa que a DSL deve ser vagamente legível por indivíduos que não são programadores. Essa natureza da linguagem pode ter várias formas, muitas das quais mostrarei nos próximos artigos, nos quais continuarei investigando o uso das DSLs como uma forma de capturar padrões idiomáticos.
Para ser uma DSL adequada, a linguagem deve ser focada de forma restrita em um domínio de problema particular. Um dos riscos da tentativa de criação de DSLs é torná-las muito amplas. As DSLs são um mecanismo de abstração e a criação de uma abstração muito ampla diminui os benefícios da abstração.
A expressividade limitada também é uma característica típica das DSLs. Por exemplo: é muito raro encontrar uma DSL que inclua estruturas de controle como looping e decisões. A DSL deve focar de forma especial e exclusiva no domínio que está tentando descrever. Como resultado, a maioria das DSLs é declarativa e não imperativa.
Os dois critérios anteriores sugerem essa característica, mas irei formalizá-la aqui. A DSL não deve ser Turing completa (consulte a seção Recursos). Na verdade, a transformação acidental para o padrão Turing completo é considerada um antipadrão nas DSLs. Por exemplo: o arquivo clássico de configuração do UNIX® sendmail é acidentalmente Turing completo. Seria possível escrever um sistema operacional no arquivo de configuração sendmail se desejar e tiver bastante tempo livre.
É surpreendentemente fácil se tornar Turing completo acidentalmente. Algumas ferramentas de infraestrutura familiar fizeram essa transição acidentalmente — o XSLT, por exemplo. Ocasionalmente, a determinação de uma linguagem como DSL depende do contexto no qual a linguagem está sendo usada. Ao usar XSLT para transformar uma versão de texto em outra versão de texto, a XSLT é usada como uma DSL. Ao usar XSLT para resolver o problema das Torres de Hanói, a XSLT é usada como uma linguagem Turing completa (e você provavelmente deveria encontrar um novo hobby).
Neste artigo, eu lancei a base para o uso de DSLs como um mecanismo de extração para coleta de padrões idiomáticos. As DSLs funcionam muito bem para esse propósito, pois são facilmente diferenciadas das APIs regulares, tendem a ser de natureza declarativa e melhoram o loop de feedback de comunicação entre os desenvolvedores e não desenvolvedores nos projetos. Nos próximos artigos, irei investigar diversas técnicas para desenvolvimento de DSLs. Nos próximos artigos, demonstrarei também diversas técnicas de DSL que poderão ser aproveitadas na sua busca para descoberta e design no código.
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.
-
ANTLR: ANTLR é uma ferramenta poderosa de software livre para desenvolvimento de linguagens e gramáticas.
-
Domain Specific Languages (Martin Fowler, Addison-Wesley, 2010): O novo livro de Fowler está disponível na versão beta.
-
Turing completa: Leia o artigo da Wikipédia sobre esse conceito.
-
Navegue na livraria de tecnologia para obter livros sobre estes e outros tópicos técnicos.
-
Zona de tecnologia Java do developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Discutir
- Participe da comunidade My developerWorks.

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.