Dev (Back & Front)ARTIGO

Arquitetura evolucionária e design emergente: interfaces fluentes

Neste artigo sobre Arquitetura evolucionária e design emergente, vamos promover o 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 artigo, o autor mostra três
maneiras de construir DSLs internas que capturam padrões de domínio
idiomático.

Introdução às técnicas do DSL

Em um livro prestes a ser publicado, Domain Specific Languages, Martin
Fowler diferencia dois tipos de DSLs (ao final do artigo, consulte maiores informações em 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 artigo constroem DSLs internas usando Java como idioma de
base e desenvolvendo novas minilinguagens a partir de sua sintaxe.

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 {<br />public static void main(String[] args) {<br /><br />new CalendarDemoChained(); <br />} <br /><br />public CalendarDemoChained() {<br /> Calendar fourPM = Calendar.getInstance();<br /> fourPM.set(Calendar.HOUR_OF_DAY, 16); <br /> Calendar fivePM = Calendar.getInstance();<br /> fivePM.set(Calendar.HOUR_OF_DAY, 17); <br /> AppointmentCalendarChained calendar = new AppointmentCalendarChained();<br /> calendar.add("dentist"). from(fourPM). to(fivePM). at("123 main street");<br /> calendar.add("birthday party").at(fourPM);<br /> displayAppointments(calendar);<br />} <br />private void displayAppointments(AppointmentCalendarChained calendar) {<br /> for (Appointment a : calendar.getAppointments()) <br />    System.out.println(a.toString()); <br />}<br />} 

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 {<br />private String _name; <br />private String _location; <br />private Calendar _startTime; <br />private Calendar _endTime; <br />public Appointment(String name) {<br />this._name = name; <br />} <br />public Appointment() {<br />} <br />public Appointment name(String name) {<br />_name = name; return this;<br />} <br />public Appointment at(String location) {<br />_location = location; return this; <br />}<br />public Appointment at(Calendar startTime) {<br />_startTime = startTime; return this; <br />}<br />public Appointment from(Calendar startTime) {<br />_startTime = startTime; return this; <br />}<br />public Appointment to(Calendar endTime) {<br />_endTime = endTime; return this; <br />} <br />public String toString() {<br />return "Appointment:"+ _name + (<br />(_location != null && _location.length() > 0)<br />? ", location:" + _location : "")<br />+ ", Start time:" + _startTime.get(Calendar.HOUR_OF_DAY) +<br />(_endTime != null? ", End time: " + _endTime.get(Calendar.HOUR_OF_DAY) : ""); <br />}<br />} 

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“, existe 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();<br />MarketingDescription desc = new MarketingDescriptionImpl();<br />desc.setType("Box"); desc.setSubType("Insulated");<br />desc.setAttribute("length", "50.5");<br />desc.setAttribute("ladder", "yes");<br />desc.setAttribute("lining type", "cork");<br />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() <br />.box() <br />.length(50.5)<br />.type(Type.INSULATED)<br />.includes(Equipment.LADDER)<br />.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 {<br />private List<Appointment> appointments;<br />public AppointmentCalendarChained() {<br />appointments = new ArrayList<Appointment>();<br />}<br />public List<Appointment> getAppointments() {<br />return appointments; <br />} <br />public Appointment add(String name) {<br />Appointment appt = new Appointment(name);<br />appointments.add(appt); return appt; <br />} <br />} 

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

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) {<br />Appointment appt = new Appointment(name);<br />appointments.add(appt);<br />System.out.println(appt.toString());<br />return appt;<br />} 

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

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();<br />calendar.add( new Appointment("Dentist"). at(fourPM));<br />calendar.add( new Appointment("Conference Call").<br />from(fourPM). to(fivePM). at("555-123-4321"));<br />calendar.add( new Appointment("birthday party"). <br />from(fourPM). to(fivePM)).<br />add( new Appointment("Doctor"). at("123 Main St"));<br />calendar.add( new Appointment("No Fluff, Just Stuff"). at(fourPM));<br />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 {<br />private List<Appointment> appointments;<br />public AppointmentCalendar() {<br /> appointments = new ArrayList<Appointment>();<br />}<br />public AppointmentCalendar add(Appointment appt) {<br /> appointments.add(appt); return this; <br />} <br />public List<Appointment> getAppointments() {<br /> return appointments; <br />}<br />} 

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() {<br /> { <br />  name("dentist");<br />  from(fourPM); <br />  to(fivePM);<br />  at("123 main street");<br /> }<br />});<br />calendar.add(new Appointment(){<br /> {<br />  name("birthday party");<br />  at(fourPM); <br /> }<br />}); 

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. 

Aguarde o próximo capítulo para continuarmos 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.

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.

é o portal de tecnologia da IBM para profissionais de TI de todo o mundo. Colabore, aprenda a criar aplicativos inovadores e compreenda tecnologias avançadas. Acesse mais artigos e tutoriais em www.ibm.com/developerworks/br

Ver perfil