Arquitetura evolucionária e design emergente: Interfaces fluentes

Construa DSLs internas para capturar padrões de domínio idiomático

Este capítulo de Arquitetura evolucionária e design emergente dá continuidade ao debate sobre técnicas de coleta de padrões idiomáticos no design emergente. Tendo identificado um padrão reutilizável, você deve capturá-lo de maneira a diferenciá-lo do restante do código. Domain-specific languages (DSLs) oferecem muitas técnicas de captura sucinta de dados e funcionalidade. Neste mês, Neal Ford mostra três maneiras de construir DSLs internas que capturam padrões de domínio idiomático.

Neal Ford, Application Architect, ThoughtWorks Inc.

Photo of Neal FordNeal 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.



30/Jul/2010

O capítulo anterior desta série introduziu o tema do uso de linguagens específicas de domínio (DSLs) para capturar padrões idiomáticos de domínio. Este capítulo prossegue nesse tópico, demonstrando várias técnicas de construção de DSL.

Em um livro prestes a ser publicado, Domain Specific Languages, Martin Fowler diferencia dois tipos de DSLs (consulte Recursos). As DSLs externas constroem uma nova gramática da linguagem, exigindo ferramentas como lexx e yacc ou Antlr. Uma DSL interna constrói uma nova linguagem sobre um idioma de base, cuja sintaxe toma emprestada e estiliza. Os exemplos neste capítulo constroem DSLs internas usando Java™ como idioma de base e desenvolvendo novas minilinguagens a partir de sua sintaxe.

Sobre esta série

Esta série visa prover uma nova perspectiva sobre conceitos de arquitetura e design de software frequentemente discutidos, mas elusivos. Por meio de exemplos concretos, Neal Ford oferece a você um embasamento sólido nas práticas ágeis da arquitetura evolucionária e do design emergente. O adiamento das decisões importantes sobre arquitetura e design até o último momento viável pode impedir que a complexidade desnecessária comprometa os projetos de software.

Um conceito subjacente a todas as técnicas de construção de DSLs descritas a seguir é o de contexto implícito. As DSLs (especialmente as internas) tentam eliminar a sintaxe dissonante por meio da criação de encapsulamentos contextuais em torno de elementos relacionados. Um bom exemplo desse conceito aparece em XML na forma de elementos pais e filhos, que fornecem um encapsulamento para itens relacionados. Você perceberá que muitas dessas técnicas de DSL obtêm esse mesmo efeito usando truques sintáticos da linguagem.

A facilidade de leitura é um dos benefícios do uso de uma DSL. Ao escrever código que pode ser lido por não desenvolvedores, você encurta o ciclo de feedback entre sua equipe e as pessoas que estão solicitando recursos. Um padrão comum de DSL identificado no livro de Fowler é a chamada interface fluente, que ele define como um comportamento capaz de retransmitir ou manter o contexto da instrução para uma série de chamadas de método. Mostrarei aqui vários tipos de interfaces fluentes, começando com o encadeamento de métodos.

Encadeamento de métodos

O encadeamento de métodos usa valores de retorno de métodos para retransmitir o contexto da instrução, que, neste caso, é a instância do objeto que faz a primeira solicitação de método. Isto soa muito mais complexo do que é; portanto, fornecerei um exemplo para esclarecer esse conceito.

Ao trabalhar com DSLs, é comum começar com a sintaxe de destino e usar engenharia reversa para descobrir como implementá-la. Começar pelo final faz sentido porque a facilidade de leitura é altamente valorizada nas DSLs. O exemplo que usarei é um pequeno aplicativo que rastreia as entradas em uma agenda. O aplicativo ilustra a sintaxe da DSL, como é mostrado na Listagem 1:

Listagem 1: Sintaxe de destino para uma DSL de agenda
 public class CalendarDemoChained {
public static void main(String[] args) {

new CalendarDemoChained(); 
} 

public CalendarDemoChained() {
 Calendar fourPM = Calendar.getInstance();
 fourPM.set(Calendar.HOUR_OF_DAY, 16); 
 Calendar fivePM = Calendar.getInstance();
 fivePM.set(Calendar.HOUR_OF_DAY, 17); 
 AppointmentCalendarChained calendar = new AppointmentCalendarChained();
 calendar.add("dentist"). from(fourPM). to(fivePM). at("123 main street");
 calendar.add("birthday party").at(fourPM);
 displayAppointments(calendar);
} 
private void displayAppointments(AppointmentCalendarChained calendar) {
 for (Appointment a : calendar.getAppointments()) 
    System.out.println(a.toString()); 
}
}

Após o código necessário no início, relacionado a agendas em Java, você pode ver a interface fluente com encadeamento de métodos em ação quando são adicionados valores às duas entradas da agenda. Observe que são usados espaços em branco para separar as partes do que é (do ponto de vista da sintaxe Java) uma única linha de código. É comum, nas DSLs internas, estilizar o uso do idioma de base para tornar a DSL mais legível.

A classe Appointment contendo a maioria dos métodos da interface fluente aparece na Listagem 2:

Listagem 2. Classe Appointment
 public class Appointment {
private String _name; 
private String _location; 
private Calendar _startTime; 
private Calendar _endTime; 
public Appointment(String name) {
this._name = name; 
} 
public Appointment() {
} 
public Appointment name(String name) {
_name = name; return this;
} 
public Appointment at(String location) {
_location = location; return this; 
}
public Appointment at(Calendar startTime) {
_startTime = startTime; return this; 
}
public Appointment from(Calendar startTime) {
_startTime = startTime; return this; 
}
public Appointment to(Calendar endTime) {
_endTime = endTime; return this; 
} 
public String toString() {
return "Appointment:"+ _name + (
(_location != null && _location.length() > 0)
? ", location:" + _location : "")
+ ", Start time:" + _startTime.get(Calendar.HOUR_OF_DAY) +
(_endTime != null? ", End time: " + _endTime.get(Calendar.HOUR_OF_DAY) : ""); 
}
}

Como você pode ver, construir interfaces fluentes é um processo simples. Para cada método mutador, você diverge da sintaxe JavaBean padrão escrevendo métodos setter que retornam ao objeto hospedeiro (this) e substituindo a convenção de nomenclatura set por algo mais legível. A definição geral no início desta seção agora deve estar clara. O contexto que está sendo retransmitido por meio do encadeamento de métodos é this, o que significa que é possível fazer uma série de chamadas de método de forma concisa.

No artigo "Leveraging reusable code, Part 2", mostrei uma definição de API para um vagão ferroviário, exibida na Listagem 3:

Listagem 3. Uma API para um vagão ferroviário
 Car2 car = new CarImpl();
MarketingDescription desc = new MarketingDescriptionImpl();
desc.setType("Box"); desc.setSubType("Insulated");
desc.setAttribute("length", "50.5");
desc.setAttribute("ladder", "yes");
desc.setAttribute("lining type", "cork");
car.setDescription(desc);

O domínio do problema para vagões ferroviários é complexo devido às normas regulamentares sobre conteúdo e histórico. No projeto que resultou nesse exemplo, tínhamos muitos cenários de teste complicados que exigiam dezenas de linhas de chamadas set, como as que são mostradas na Listagem 3. Tentamos convencer nossos analistas comerciais a verificar se tínhamos a combinação mágica correta de atributos, mas eles se recusaram porque encaravam isso como código Java, algo que não tinham interesse em ler. Em última análise, o efeito desse problema era exigir que um desenvolvedor traduzisse verbalmente os detalhes, o que, evidentemente, é uma atividade demorada e sujeita a erros.

Para solucionar o problema, convertemos nossa classe Car em uma interface fluente, de modo que o código da Listagem 3 transformou-se na interface fluente mostrada na Listagem 4:

Listagem 4. Interface fluente para vagões ferroviários
 Car car = Car.describedAs() 
.box() 
.length(50.5)
.type(Type.INSULATED)
.includes(Equipment.LADDER)
.lining(Lining.CORK);

Esse código era suficientemente declarativo e removia uma quantidade suficiente de ruído da versão em API Java para que nossos analistas comerciais se dispusessem a verificá-lo para nós.

Voltando ao exemplo da agenda, o último detalhe da implementação é a classe AppointmentCalendar, que aparece na Listagem 5:

Listagem 5. AppointmentCalendar
 public class AppointmentCalendarChained {
private List<Appointment> appointments;
public AppointmentCalendarChained() {
appointments = new ArrayList<Appointment>();
}
public List<Appointment> getAppointments() {
return appointments; 
} 
public Appointment add(String name) {
Appointment appt = new Appointment(name);
appointments.add(appt); return appt; 
} 
}

O método add():

  1. Inicia a cadeia de métodos criando uma nova instância de Appointment
  2. Adiciona a nova instância à lista de compromissos
  3. Finalmente, retorna a nova instância de compromisso, o que significa que as chamadas subsequentes do método serão invocadas no novo compromisso

Ao executar o aplicativo, você verá os detalhes dos seus compromissos configurados, como é mostrado na Figura 1:

Figura 1. Resultados da execução do aplicativo de agenda
output from demo application

Até agora, o encadeamento de métodos parece ser uma maneira simples de limpar uma sintaxe excessivamente detalhada, particularmente as chamadas de método, que são predominantemente declarativas. Isso funciona bem com padrões idiomáticos no design emergente porque os padrões de domínio frequentemente são declarativos.

Observe que, para usar encadeamento de métodos, é necessário violar as regras de sintaxe de JavaBeans, que insistem que métodos mutadores devem começar com set e retornar void. A construção de interfaces fluentes é um exemplo de saber quando faz sentido quebrar algumas regras. A especificação JavaBeans não lhe faz nenhum favor quando o força a escrever código incompreensível! Por outro lado, nada na criação ou no uso de interfaces fluentes impede o suporte tanto à interface fluente quanto a uma interface JavaBeans. Os métodos da interface fluente podem fazer o caminho inverso e chamar os métodos set padrão, o que lhe permite usar interfaces fluentes mesmo quando as estruturas insistem em interagir com suas classes como JavaBeans.


Solucionando o problema da conclusão

Uma armadilha inerente às interfaces fluentes, em certas circunstâncias, é conhecida como o problema da conclusão. Ilustrarei esse problema fazendo uma alteração na classe AppointmentCalendar da Listagem 5. Presumivelmente, você quer fazer mais do que simplesmente exibir os compromissos, como armazená-los em um banco de dados ou em algum outro mecanismo de persistência. Onde é possível adicionar o código para salvar o compromisso concluído no armazenamento? Você pode tentar fazer isso no método add() de AppointmentCalendar, imediatamente antes de retornar o compromisso. A Listagem 6 mostra uma tentativa de acessar o compromisso em questão para fazer algo tão simples como imprimi-lo:

Listagem 6. Adicionando a impressão
 public Appointment add(String name) {
Appointment appt = new Appointment(name);
appointments.add(appt);
System.out.println(appt.toString());
return appt;
}

A execução do código da Listagem 6 produz os lamentáveis resultados ilustrados na Figura 2:

Figura 2. Saída de erro após o acréscimo a AppointmentCalendar
Output from changes to AppointmentCalendar

O erro exibido é uma NullPointerException que ocorre no método toString() da classe Appointment. A razão para essa queixa, embora o método tenha funcionado corretamente, é a essência do problema da conclusão.

O erro ocorre porque estou tentando chamar o método toString() na instância do compromisso antes que os métodos setter restantes da interface fluente sejam chamados. O código para tentar imprimir o compromisso aparece no método que cria a instância do compromisso e inicia a cadeia. Eu poderia criar um método save() ou finished() que seria chamado obrigatoriamente como o último método da cadeia, mas prefiro não impor uma regra fácil de esquecer aos usuários da minha DSL. De fato, prefiro não impor qualquer semântica de ordenamento aos métodos em minha interface fluente.

O problema real é que estou sendo muito agressivo com a técnica de encadeamento de métodos. O encadeamento de métodos funciona melhor para a criação de objetos de dados simples, mas aqui está sendo usado tanto para os métodos setter em Appointment como, em AppointmentCalendar, para iniciar a cadeia de métodos.

Posso corrigir o problema da conclusão delimitando totalmente a criação do compromisso com os parênteses do método add() da agenda, como é mostrado na Listagem 7:

Listagem 7. Delimitando via parâmetro
 AppointmentCalendar calendar = new AppointmentCalendar();
calendar.add( new Appointment("Dentist"). at(fourPM));
calendar.add( new Appointment("Conference Call").
from(fourPM). to(fivePM). at("555-123-4321"));
calendar.add( new Appointment("birthday party"). 
from(fourPM). to(fivePM)).
add( new Appointment("Doctor"). at("123 Main St"));
calendar.add( new Appointment("No Fluff, Just Stuff"). at(fourPM));
displayAppointments(calendar);

Na Listagem 7, os parênteses do método add() encapsulam todo o uso da interface fluente Appointment, permitindo que o método add() lide com qualquer comportamento adicional desejado (impressão, persistência, e assim por diante). De fato, não pude resistir à adição de um pouco de interface fluente ao próprio AppointmentCalendar: agora é possível encadear os métodos add(), como é mostrado na Listagem 7 e implementado na Listagem 8:

Listagem 8. O encapsulamento de parâmetros AppointmentCalendar
 public class AppointmentCalendar {
private List<Appointment> appointments;
public AppointmentCalendar() {
 appointments = new ArrayList<Appointment>();
}
public AppointmentCalendar add(Appointment appt) {
 appointments.add(appt); return this; 
} 
public List<Appointment> getAppointments() {
 return appointments; 
}
}

O problema da conclusão pode surgir sempre que classes de interface fluente são combinadas. Ele surgiu no exemplo porque usei a agenda para iniciar a cadeia de métodos, combinando os comportamentos de construção e encapsulamento. Ao delegar a construção e a inicialização à classe Appointment, facilito a separação de comportamentos adicionais de encapsulamento (como a persistência).


Encapsulamento via sequência funcional

Até aqui, mostrei duas das três técnicas de passagem de contexto para DSLs de interface fluente. A terceira — sequência funcional — usa herança e classes internas anônimas para criar um wrapper de contexto. O aplicativo de agenda reescrito usando a sequência funcional é mostrado na Listagem 9:

Listagem 9. Encapsulamento via sequência funcional
 calendar.add(new Appointment() {
 { 
  name("dentist");
  from(fourPM); 
  to(fivePM);
  at("123 main street");
 }
});
calendar.add(new Appointment(){
 {
  name("birthday party");
  at(fourPM); 
 }
});

A Listagem 9 mostra um padrão que introduzi em "Leveraging reusable code, Part 2" com o objetivo de remover a duplicação estrutural. A sintaxe parece estranha devido às chaves duplas {{. O primeiro par de chaves delimita a construção de uma classe interna anônima e o segundo delimita o inicializador de instância para a classe interna anônima. (Se isso lhe parece um tanto confuso, consulte "Leveraging Reusable Code, Part 2" para obter uma explicação detalhada dessa expressão idiomática em Java.)

A principal vantagem desse estilo de interface fluente é sua adaptabilidade. A única coisa de que uma classe precisa para ser usada dessa maneira é um construtor padrão (o que permite criar uma instância da classe interna anônima que herda dessa classe). Isso significa que é possível adicionar facilmente métodos de interface fluente a APIs Java existentes sem alterar qualquer aspecto da semântica de chamada atual. Com isso, as APIs existentes podem ser "fluentizadas" gradualmente.


Conclusão

As DSLs capturam padrões de domínio idiomático de forma concisa e eficaz. As interfaces fluentes oferecem uma maneira simples de alterar a forma como você escreve código de modo a ver mais rapidamente os padrões idiomáticos que está tentando identificar. Além disso, elas o obrigam a alterar ligeiramente sua perspectiva do código: não basta que ele seja funcional, é necessário também que seja legível, particularmente quando não desenvolvedores devem consumir qualquer aspecto desse código. As interfaces fluentes removem o ruído desnecessário da sua sintaxe, resultando em um código mais legível. Em estruturas declarativas, as ideias podem ser expressas mais claramente com menos esforço.

No próximo capítulo, continuarei a discutir as técnicas de DSL como um mecanismo para coletar padrões idiomáticos no design emergente.

Recursos

Aprender

Obter produtos e tecnologias

  • Antlr: Antlr é uma poderosa ferramenta de design de linguagem que permite criar novos analisadores e ferramentas léxicas para linguagens customizadas.
  • Avalie os produtos da IBM® da forma que melhor lhe convém: Faça o download de uma versão de teste de produto, experimente um produto on-line, use um produto em um ambiente de nuvem ou passe algumas horas no SOA Sandbox aprendendo como implementar a Arquitetura Orientada a Serviços de forma eficiente.

Discutir

  • Envolva-se na comunidade My developerWorks. Entre em contato com outros usuários do developerWorks enquanto explora os blogs, fóruns, grupos e wikis dos desenvolvedores.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=502942
ArticleTitle=Arquitetura evolucionária e design emergente: Interfaces fluentes
publish-date=07302010