Desmistificando a programação generativa

Daniel Lucredio
b2w engineering
Published in
27 min readNov 11, 2020

Programar é um processo majoritariamente criativo, onde o programador expressa estruturas, algoritmos e cálculos para resolver um problema computacional.

Mas a parte criativa é quase sempre acompanhada de um esforço mais repetitivo, um trabalho braçal, às vezes pouco gratificante, mas igualmente necessário para transformar grandes ideias em realidade.

Muito desse trabalho pode ser automatizado por meio da geração de código. Existem muitas ferramentas que ajudam nesse processo, mas e naquelas situações onde a ferramenta não gera código exatamente como você precisa?

O que fazer? Preencher um chamado e implorar para que os fabricantes da ferramenta acrescentem uma feature para você? Ou colocar a mão na massa você mesmo e entender a ferramenta? E se a ferramenta não for aberta?

Neste artigo, será apresentado como a programação generativa pode ser incorporada em qualquer projeto, de forma que qualquer desenvolvedor possa criar um gerador especializado para sua situação e colocar a máquina para fazer um pouco daquele trabalho repetitivo para você.

Requisitos para a leitura

Para entender os conceitos e ideias fundamentais do artigo, você vai precisar de apenas um ingrediente:

  • Um cérebro com conhecimentos básicos de programação e com espaço para novas ideias.

Se quiser reproduzir os exemplos feitos ao longo do texto, você vai precisar de:

- Java Development Kit (testado na versão 1.8)

- Apache Maven (testado na versão 3.6.3)

- Um editor de código (testado no Visual Studio Code na versão 1.49.2)

Uma breve contextualização

A programação generativa se refere ao uso de geração de código como auxílio ao processo de programar. O objetivo quase sempre é evitar esforço repetitivo. Por exemplo, o projeto Lombok permite que você escreva algo como isto aqui:

// Você, programador, escreve isso aqui...
@Getter @Setter private boolean employed = true;
@Setter(AccessLevel.PROTECTED) private String name;

ao invés disso aqui:

// O Lombok irá gerar automaticamente o seguinte código...
private boolean employed = true;
private String name;

public boolean isEmployed() {
return employed;
}

public void setEmployed(final boolean employed) {
this.employed = employed;
}

protected void setName(final String name) {
this.name = name;
}

Isso obviamente deixa o código mais simples, mais fácil de entender, evitando uma série de batidas de dedo no teclado e salvando um tempo precioso.

Mas, além de reduzir a necessidade de digitar todo esse código, a programação generativa pode incrementar a semântica da linguagem. Isso significa que você pode ter novas palavras-chave, por exemplo, que trazem um significado novo e um comportamento adicional no código gerado.

Ainda seguindo o exemplo do Lombok, veja o código a seguir, com atenção especial à anotação "NonNull". Neste caso a anotação "NonNull" tem um significado (ou semântica) particular: o atributo "members" não pode ser nulo.

@Getter @Setter @NonNull
private List<Person> members;

Para garantir que esse significado seja respeitado, o Lombok irá gerar o seguinte código, que verifica se a instância de "members" é nula, lançando uma exceção em caso positivo, dando assim maior segurança ao programador:

@NonNull
private List<Person> members;

public Family(@NonNull final List<Person> members) {
if (members == null) throw new java.lang.NullPointerException("members");
this.members = members;
}

@NonNull
public List<Person> getMembers() {
return members;
}

public void setMembers(@NonNull final List<Person> members) {
if (members == null) throw new java.lang.NullPointerException("members");
this.members = members;
}

O Lombok tem outras anotações diversas que podem ajudar o programador. Se isso estiver de bom tamanho para você, vai lá aprender como instalar e usar, e pode colher os benefícios agora mesmo!

Agora, se você precisa que o código gerado seja um pouco diferente, você pode tentar contribuir com o Lombok. Se a sua demanda for por mudanças muito radicais, ou se necessitar de outra linguagem completamente diferente, pode ser que o Lombok não sirva para você. Você pode tentar pesquisar outras ferramentas…

… ou pode continuar lendo este artigo e aprender …

…como construir uma linguagem e gerador próprios

Antes de colocar a mão na massa, vale a pena discutir o que é necessário para construir uma solução de programação generativa. Existem muitas formas de se empregá-la. Essencialmente, duas coisas são necessárias: uma entrada e um gerador.

A entrada

A entrada é o conjunto de informações usadas pelo gerador. Nos exemplos da seção anterior, a entrada consiste na definição de atributos com tipo, visibilidade e anotações adicionais indicando se deveriam ser gerados setters, getters e código para teste de valores nulos.

Outros exemplos de entrada para um gerador são:

  • Um arquivo .yaml que define as colunas de um arquivo .CSV, servindo de entrada para um gerador que produz código que lê as colunas certas do arquivo .CSV;
  • Um arquivo .xml que representa uma ontologia, servindo de entrada para um gerador que irá produzir código de acesso aos elementos da ontologia;
  • Um arquivo .json que representa uma sequência de mensagens de comunicação, servindo de entrada para um gerador que produz código que implementa um protocolo de rede;
  • um modelo de classes UML, servindo de entrada para um gerador que produz código da camada Model, incluindo classes de entidade, código CRUD (Create, Retrieve, Update, Delete) e scripts SQL para a criação das tabelas correspondentes;
  • entre outros exemplos.

Como pode-se notar, a entrada pode ser tão simples ou complexa quanto se deseja. Quanto mais simples o formato da entrada, mais fácil será de implementar, porém o resultado será um menor poder expressivo.

Por exemplo: um arquivo .json é simples de utilizar e seria suficiente para definir uma estrutura de um objeto. Mas para representar um conceito como herança entre classes ou relacionamento entre tabelas, seria necessário algum truque com as strings, deixando o resultado pouco natural.

Quanto mais complexo o formato da entrada, maior o seu poder expressivo, porém mais difícil será de implementar. Um modelo UML é bastante expressivo, permitindo expressar praticamente tudo o que se deseja relacionado ao software, mas seu metamodelo (ou estrutura interna), por ser genérico (ou “unificado”, na terminologia OMG) é particularmente difícil de ser digerido por um gerador.

Assim, um meio-termo aceitável e muito utilizado na programação generativa é utilizar uma DSL (Domain-Specific Language ou Linguagem de Domínio Específico). Uma DSL é uma linguagem que foi especificamente projetada para um propósito, como realizar consultas em bancos de dados relacionais.

Já com propósito definido, a DSL tem um poder expressivo alto dentro daquele domínio, porém como ela não tem necessidade de ser genérica, sua estrutura interna é mais simples e fácil de ser digerida por um gerador.

Existem diferentes formas de se implementar uma DSL. Uma delas é estender uma linguagem existente por meio de suas facilidades sintáticas e mecanismos de extensão. Neste caso, diz-se que a DSL é interna, ou embutida.

No seguinte exemplo de uma DSL em Kotlin, é possível representar um treinamento que tem horário de início e término:

training {
starts at 15..30
ends at 17..15
}

O resultado é bastante impressionante: fica bastante intuitivo o que está sendo representado! A vantagem é reaproveitar uma sintaxe já consagrada, além de poder reutilizar um compilador/interpretador já pronto. Porém, a expressividade da linguagem pode ficar um pouco prejudicada. Além disso, a solução fica restrita à linguagem e ambiente escolhidos.

Clique aqui e confira o texto sobre DSL em Kotlin, publicado anteriormente.

Outro caminho é a criação de uma linguagem nova, com sintaxe própria, o que é conhecido como uma DSL externa. Tem-se assim, total liberdade para moldar a linguagem como desejado. Revisitando o exemplo anterior, o seguinte resultado poderia ser alcançado com uma DSL externa:

training "Aprendendo programação generativa":
from 15h30 to 17h15
number of seats 45
registration from 10/12/2020 to 20/12/2020 with training@company.com

Como pode-se notar, dependendo das informações necessárias, o poder expressivo pode ficar ainda maior. Neste exemplo, há uma representação de horário que parece mais natural e informações como o título do treinamento, o número de assentos e os dados para inscrição.

A principal desvantagem é a necessidade de se construir um interpretador para essa DSL a partir do zero. Isso pode ser uma grande barreira para muitos programadores. Mas não para você, leitor, que continuará este artigo até o fim!

O gerador

Agora que já decidimos criar nossa própria DSL, resta falar do gerador. Um gerador nada mais é do que um programa que lê a entrada (qualquer que seja seu formato) e produz código que normalmente é um arquivo texto.

Pode-se implementar um gerador utilizando uma série de técnicas, mas há muito tempo se sabe que a técnica baseada em templates e visitantes é uma das mais adequadas para a tarefa.

Um visitante é um padrão de projeto que facilita a tarefa de se realizar ações, à medida que uma estrutura de objetos (como a estrutura de um programa escrito em uma DSL) é percorrida.

Já um template dispensa apresentações. Quem conhece PHP, JSP, ASP e outras tecnologias parecidas, conhece a essência de um template: um arquivo textual instrumentado com comandos lógicos, aritméticos, controle de fluxo e substituição de valores.

Templates e visitantes se complementam muito bem na geração de código, como começaremos a ver (finalmente) na próxima seção.

Colocando a mão na massa

A partir de agora, o artigo descreve uma solução baseada em tecnologias Java, mas os conceitos são plenamente transportáveis para outras linguagens, bastando para isso encontrar componentes equivalentes. Para se ter uma ideia, iremos construir:

  • Um analisador sintático utilizando um gerador de parsers chamado ANTLR, que tem suporte para Java, mas também outras linguagens, como C#, Python, Go, C++, Swift e PHP. Caso o ANTLR não tenha suporte para alguma linguagem, existem VÁRIOS outros geradores de parsers para escolher.
  • Um mecanismo de templates chamado Apache FreeMarker, que funciona em Java. Existem processadores de templates para muitas linguagens, como Python e C#.
  • Um plugin do Maven para facilitar a execução do analisador sintático em seus projetos. Qualquer gerenciador de tarefas pode ser utilizado para esta tarefa, mas até mesmo um bom e velho Shell Script pode quebrar o galho.

Passo 1: criando o projeto de plugin do Maven

Começaremos criando um novo projeto no Maven. Depois de configurar o Maven corretamente, basta executar o seguinte comando no terminal (lembre-se de executar esse comando dentro de uma pasta de sua escolha, por exemplo "/Users/daniellucredio/projects/", pois o projeto será criado ali dentro):

mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-plugin -DarchetypeVersion=1.4

O Maven irá solicitar algumas informações. As seguintes sugestões serão seguidas ao longo do artigo (mas fique à vontade para escolher os valores que quiser, só tome o cuidado de ajustar sempre que algum desses valores for referenciado posteriormente):

Define value for property 'groupId': com.medium.generative          
Define value for property 'artifactId': pojo-generator-maven-plugin
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' com.medium.generative: : com.medium.generative.pojogeneratorplugin

O leitor mais atento já deve ter adivinhado que faremos um gerador de POJO. É um exemplo simples, bom para começar!

Antes de mais nada, precisamos atualizar a versão Java no arquivo "pom.xml" do projeto. Utilizaremos alguns recursos que só existem na versão 1.8 em diante, porém o projeto criado vem pré-configurado para a versão 1.7. Basta alterar as linhas "<maven.compiler.source>" e "<maven.compiler.target>" no arquivo "pom.xml":

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.version>3.3.9</maven.version>
</properties>

Para usar o ANTLR, basta adicionar a seguinte dependência do Maven no arquivo "pom.xml":

<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>4.8-1</version>
<classifier>complete</classifier>
</dependency>

Vamos também adicionar no "pom.xml" uma referência para o plugin do ANTLR para o Maven. Essa configuração serve para que não seja preciso rodar o ANTLR manualmente:

<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>4.8-1</version>
<configuration>
<visitor>true</visitor>
</configuration>
<executions>
<execution>
<id>antlr</id>
<goals>
<goal>antlr4</goal>
</goals>
</execution>
</executions>
</plugin>

Note a configuração "<visitor>true</visitor>". Sim, é o mesmo padrão de projeto mencionado anteriormente.

Dica importante: programadores inexperientes frequentemente colocam as configurações de dependências e plugins no local errado do arquivo ```pom.xml```.

Se estiver encontrando problemas, certifique-se de que as dependências e o plugin estão no local correto dentro da estrutura XML do arquivo POM.

Existem dois locais onde configurar plugins: dentro de "<build><pluginManagement><plugins>" e diretamente em "<build><plugins>". Neste exemplo, o correto é a segunda opção:

  <build>
<pluginManagement>
<plugins>
<!-- NÃO É AQUI -->
</plugins>
</pluginManagement>
<plugins>
<!-- ESTE É O LOCAL CORRETO -->
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>4.8-1</version>
<configuration>
<visitor>true</visitor>
</configuration>
<executions>
<execution>
<id>antlr</id>
<goals>
<goal>antlr4</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Passo 2: definindo a sintaxe da nossa DSL

A sintaxe é definida na forma de uma Gramática Livre de Contexto.

No ANTLR, a gramática deve ser escrita em um arquivo textual. Como estamos usando o plugin do ANTLR para o Maven, esse arquivo textual deve estar em um local bem específico dentro do projeto.

Neste exemplo (e seguindo as sugestões fornecidas ao criar o projeto do Maven), o caminho deve ser o seguinte:

pojo-generator-maven-plugin/src/main/antlr4/com/medium/generative/pojogeneratorplugin/Pojo.g4

O conteúdo do arquivo é o seguinte:

// O arquivo começa com o nome da gramática
// O nome deve ser idêntico ao nome do arquivo
// sem a extensão ".g4"
grammar Pojo;
// Agora vem as regras léxicas e sintáticas
// As regras léxicas são as "palavras" da nossa linguagem
// E são definidas com a primeira letra maiúscula
// As regras sintáticas são as "frases" da nossa linguagem
// E são definidas com a primeira letra minúscula
// Primeiro, temos os identificadores da nossa linguagem
// O nome da regra léxica deve começar com letra maiúscula
// Neste exemplo, temos uma expressão regular que diz que
// um identificador (ID) deve ser uma letra ou sublinhado [a-zA-Z_]
// seguido por letra ou sublinhado ou dígito [a-zA-Z0-9_] repetido
// zero ou mais vezes (símbolo "*")
ID: [a-zA-Z_][a-zA-Z0-9_]*;
// Outras "palavras" da nossa linguagem são os espaços em branco,
// tabulações, mudança de linha, etc [ \r\n\t] repetido uma ou
// mais vezes (símbolo "+")
// Mas na nossa linguagem, essas "palavras" não servem para nada
// então devem ser descartadas (-> skip)
WS: [ \r\n\t]+ -> skip;
// Um "program" na nossa linguagem consiste de zero ou mais "pojo"
program: pojo*;
// Um "pojo" consiste de:
// - um ID (que vamos chamar de "name" para facilitar o acesso)
// - o símbolo "{"
// - zero ou mais "attribute"
// - o símbolo "}"
pojo: name=ID '{' attribute* '}';
// Um "attribute" consiste de:
// - um ID (que vamos chamar de "name" para facilitar o acesso)
// - o símbolo ":"
// - um ID (que vamos chamar de "type" para facilitar o acesso)
attribute: name=ID ':' type=ID;
// E é isso!

A gramática está explicada em forma de comentários. Estes devem dar uma boa ideia de cada regra e da estrutura final da entrada para o gerador, que ficará parecida com isso aqui:

Cliente {
nome: String
cpf: String
telefone: String
}
Produto {
id: int
descricao: String
}

Sim, por enquanto parece com JSON, mas não se engane. Em breve faremos algo um pouco mais elaborado!

Mas a boa notícia é que a primeira parte da DSL já está pronta. Basta gerar o analisador sintático. Como estamos usando o plugin do ANTLR para o Maven, basta navegar até a raiz do projeto (onde tem o "pom.xml") e executar:

mvn compile

Na pasta "target/generated-sources",serão geradas algumas classes, mas você não precisa se preocupar com elas, daqui a pouco vamos usá-las e é bem simples. Basta saber que foram gerados:

  • Um analisador léxico para a nossa linguagem;
  • Um analisador sintático para a nossa linguagem;
  • Um visitante “padrão” para podermos customizar a visitação e geração de código;
  • Outras classes auxiliares.

Outra coisa importante de saber é que como utilizamos aquela estrutura de diretórios para o arquivo "Pojo.g4", o código gerado terá os mesmos pacotes Java que a nossa classe principal, facilitando assim seu uso.

Passo 3: implementando o gerador

Um gerador é essencialmente um visitante. Uma classe visitante “padrão” foi gerada especificamente para a nossa DSL no passo anterior. Trata-se da classe "PojoBaseVisitor". Essa classe tem um método para cada regra sintática da gramática.

Então, nosso gerador precisa estender aquela classe e sobrescrever os métodos onde queremos acrescentar alguma coisa para gerar código. Neste exemplo, vamos sobrescrever apenas o método "visitPojo", que será chamado sempre que uma ocorrência de “Pojo” (da gramática) aparecer.

Vamos ao código!

O caminho do arquivo a seguir é:

src/java/com/medium/generative/pojogeneratorplugin/Generator.java

Primeiro, temos a definição de pacote, importações e definição da classe como filha de "PojoBaseVisitor". Definimos que nossos métodos retornarão booleano, indicando se a execução foi bem-sucedida ou não. Isso é feito por meio do parâmetro genérico "<Boolean>" na definição da classe.

package com.medium.generative.pojogeneratorplugin;import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import com.medium.generative.pojogeneratorplugin.PojoParser.AttributeContext;
import com.medium.generative.pojogeneratorplugin.PojoParser.PojoContext;
public class Generator extends PojoBaseVisitor<Boolean> {

Agora vamos aos atributos do gerador. Temos apenas três, conforme comentários a seguir. O construtor apenas inicializa as variáveis.

// Indica em qual pacote devemos gerar as classes
String packageName;
// Indica a pasta onde gerar o código
File outputDir;
// Lista de erros que podem ter acontecido no processo
StringBuilder errors;
// O construtor apenas inicializa as variáveis
public Generator(String packageName, File outputDir) {
this.packageName = packageName;
this.outputDir = outputDir;
errors = new StringBuilder();
}

Agora, vamos sobrescrever um método do visitante. Neste caso, vamos sobrescrever apenas o método "visitPojo", que será chamado sempre que na árvore aparecer a regra “Pojo”.

O método tem um único parâmetro "ctx" que contém todos os valores definidos na regra sintática. Vamos ao corpo do método para entender melhor essa relação.

@Override
public Boolean visitPojo(PojoContext ctx) {
// Vamos criar os diretórios de saída
if (!outputDir.exists()) {
outputDir.mkdirs();
}
// Agora vamos obter os elementos de um POJO
// O parâmetro "ctx" é um espelho da regra "pojo", assim:
// Regra da gramática:
// pojo: name=ID '{' attribute* '}';
// Portanto podemos "pegar" o nome e a lista de atributos, assim:
String pojoName = ctx.name.getText();
List<AttributeContext> pojoAttributes = ctx.attribute();
// Note como "name=ID" da gramática foi acessado via ctx.name.getText(),
// que retorna uma String
// Note como attribute* da gramática foi acessado via ctx.attribute(),
// que retorna uma lista de AttributeContext
// É simples assim! ctx é um espelho da regra "pojo" // Agora vamos começar a gerar o código. Para isso, vamos criar um
// objeto "writer" simples, que aponta para um arquivo, na pasta
// de saída, com a mesma estrutura de pastas que os pacotes
// e cujo nome é o mesmo que o nome do POJO, com extensão ".java"
try (FileWriter writer = new FileWriter(new File(outputDir, pojoName + ".java"))) {

A partir de agora, já podemos começar a escrever no arquivo e gerar o código.

            // Imprimindo o pacote
writer.append("package "+packageName+";\n\n");

// Imprimindo a definição da classe
writer.append("public class "+ctx.name.getText()+" {\n");
// Vamos gerar os atributos do POJO, todos "private"
for (AttributeContext attr : pojoAttributes) {
// Para cada atributo, podemos também obter seus detalhes como
// definido na gramática.
// Regra da gramática:
// attribute: name=ID ':' type=ID;
// Um atributo tem "name" e "type", então podemos "pegar"
// esses valores assim:
String attributeName = attr.name.getText();
String attributeType = attr.type.getText();
// Note como "name=ID" e "type=ID" da gramática foram acessados
// via attr.name.getText() e attr.type.getText()
// É simples assim!
// A regra "attribute" na gramática tem seu espelho aqui no código
// Agora é só gerar o código dos atributos
writer.append(" private "+attributeType+" "+attributeName+";\n");
}
// Da mesma forma, podemos gerar getters e setters
for (AttributeContext attr : pojoAttributes) {
writer.append(" public "+attr.type.getText()+" get"+firstUpper(attr.name.getText())+"() {\n");
writer.append(" return this."+attr.name.getText()+";\n");
writer.append(" }\n");
writer.append(" public void set"+firstUpper(attr.name.getText())+"("+attr.type.getText()+" "+attr.name.getText()+") {\n");
writer.append(" this."+attr.name.getText()+" = "+attr.name.getText()+";\n");
writer.append(" }\n");
}
// Vamos adicionar uma quebra de linha
writer.append("}\n");
// E pronto
return true;
} catch (IOException ioe) {
errors.append(ioe.getMessage());
return false;
}
}
// Método auxiliar para ajudar na escrita do nome dos setters e getters
private String firstUpper(String text) {
return text.substring(0, 1).toUpperCase() + text.substring(1);
}
}

Já com o gerador, basta integrar tudo para completar o plugin. O projeto já tem uma classe chamada "MyMojo.java" criada. Trata-se do “main” do plugin que estamos construindo. Substitua o código dessa classe pelo seguinte (primeiro, definições de pacotes, importação e classe):

package com.medium.generative.pojogeneratorplugin;import java.io.File;
import java.io.IOException;
import com.medium.generative.pojogeneratorplugin.PojoParser.ProgramContext;import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
// Esse plugin irá executar na fase GENERATE_SOURCES do ciclo de vida do Maven
@Mojo(name = "pojo", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class MyMojo extends AbstractMojo {

A seguir, temos os parâmetros para a execução do nosso gerador. Os valores serão definidos pelo usuário do plugin, no arquivo "pom.xml" do projeto de teste posteriormente. Aqui no plugin, isso é configurado da seguinte maneira:

    // Esse parâmetro define em qual diretório iremos
// gerar o código para os POJO
@Parameter(property = "outputDir", required = true)
private File outputDir;
// Esse parâmetro define em qual diretório estarão
// os arquivos de entrada
@Parameter(property = "inputDir", required = true)
private File inputDir;
// Permite configurar qual o pacote para as classes
// geradas
@Parameter(property = "packageName", required = true)
private String packageName;

Vamos ao método principal deste plugin do Maven. É aqui que ocorre a chamada do parser e do gerador.

    public void execute() throws MojoExecutionException {
// Vamos primeiro imprimir um log, apenas para depuração
getLog().info("Reading dir:" +outputDir.getAbsolutePath());
// Primeiro vamos pegar todos os arquivos, na
// pasta de entrada, que tem a extensão .pojo
File[] inputFiles = inputDir.listFiles((dir, name) -> {
return name.endsWith(".pojo");
});
// Agora vamos percorrer os arquivos encontrados
for (File f : inputFiles) {
// Para cada arquivo, vamos chamar o parser

A seguir, temos a chamada do parser para cada arquivo “.pojo” encontrado. O código a seguir é praticamente o padrão do ANTLR.

            // CharStream é uma classe auxiliar do ANTLR
CharStream cs;
try {
// Vamos ler o arquivo
cs = CharStreams.fromFileName(f.getAbsolutePath());
// Para cada arquivo, vamos instanciar o analisador léxico...
PojoLexer lexer = new PojoLexer(cs);

// Passar o resultado do léxico para o analisador sintático...
CommonTokenStream tokens = new CommonTokenStream(lexer);
PojoParser parser = new PojoParser(tokens);
// E vamos chamar o analisador sintático.
ProgramContext tree = parser.program();

O resultado da chamada no parser, do método "program()" (que é a primeira regra sintática da gramática) será uma árvore que representa todo o programa escrito na DSL.

Basta agora repassar essa árvore ao gerador para que ele possa visitá-la e gerar o código. Ao criar o gerador, aproveitamos e passamos os parâmetros configurados ("packageName" e "outputDir").

                Generator g = new Generator(packageName, outputDir);
boolean success = g.visit(tree);
// Se deu tudo certo, nada a fazer. Se deu alguma coisa errada
// vamos avisar o Maven que deu algum erro
if (!success) {
throw new MojoExecutionException("Error generating POJO: " + g.errors.toString());
}
} catch (IOException e) {
throw new MojoExecutionException("Error reading file : " + f.getAbsolutePath(), e);
}
}
}
}

Passo 4: testando o gerador

Para testar o gerador, primeiro precisamos instalar nosso plugin no ambiente Maven. Para facilitar o processo, vamos primeiro remover o caso de teste que já vem configurado no projeto (pasta "test/java/com/medium/generative/pojogeneratorplugin"). Em seguida, basta executar o comando:

mvn clean install

Agora qualquer novo projeto Maven criado na mesma máquina pode utilizar o gerador de código. Também é possível compartilhar o plugin com outros desenvolvedores.

Para criar um novo projeto, basta executar o seguinte comando no terminal (Lembre-se de executar esse comando dentro de uma pasta de sua escolha, por exemplo "/Users/daniellucredio/projects/", pois o projeto será criado ali dentro):

mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4

O Maven irá solicitar algumas informações. As seguintes sugestões serão seguidas ao longo do artigo (mas fique à vontade para escolher os valores que quiser):

Define value for property 'groupId': com.exemplo         
Define value for property 'artifactId': projeto-teste
Define value for property 'version' 1.0-SNAPSHOT:
Define value for property 'package' com.exemplo:

Adicione o nosso recém-instalado plugin (que já está no ambiente) ao arquivo "pom.xml" do projeto recém-criado (cuidado para inserir no local correto, em "<build><plugins>"):

      <plugin>
<groupId>com.medium.generative</groupId>
<artifactId>pojo-generator-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<configuration>
<outputDir>src/main/java/com/exemplo/gerado</outputDir>
<inputDir>src/main/pojo</inputDir>
<packageName>com.exemplo.gerado</packageName>
</configuration>
<executions>
<execution>
<goals>
<goal>pojo</goal>
</goals>
</execution>
</executions>
</plugin>

Já é possível criar nossos POJOs e gerar o código. O plugin foi configurado para olhar a pasta "src/main/pojo" em busca da entrada, portanto é lá que temos que colocar nossos arquivos. Por exemplo, vamos colocar o seguinte conteúdo no arquivo "src/main/pojo/ecommerce.pojo":

Cliente {
nome: String
cpf: String
telefone: String
}
Produto {
id: int
descricao: String
}

O plugin já foi configurado para ser executado na fase "GENERATE_SOURCES". Não viu? Volte lá ao passo 3 e veja na criação do MOJO. Essa fase é automaticamente executada na compilação, portanto um "mvn compile" irá gerar o código antes de compilar. Mas se quiser apenas gerar o código sem compilar, execute:

mvn generate-sources

Faça o teste! Execute esse comando e veja o código sendo gerado na pasta de saída que especificamos ("src/main/java/com/exemplo/gerado")!

  • Classe "Cliente.java":
package com.exemplo.gerado;public class Cliente {
private String nome;
private String cpf;
private String telefone;
public String getNome() {
return this.nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getCpf() {
return this.cpf;
}
public void setCpf(String cpf) {
this.cpf = cpf;
}
public String getTelefone() {
return this.telefone;
}
public void setTelefone(String telefone) {
this.telefone = telefone;
}
}
  • Classe "Produto.java":
package com.exemplo.gerado;public class Produto {
private int id;
private String descricao;
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
public String getDescricao() {
return this.descricao;
}
public void setDescricao(String descricao) {
this.descricao = descricao;
}
}

Experimente modificar o conteúdo do arquivo “.pojo” ou adicionar novos arquivos “.pojo” e executar o comando "mvn generate-sources" novamente. Veja o resultado e o novo código sendo gerado.

Neste caso, o código está sendo gerado na pasta "src/main" para facilitar a configuração para você ver os resultados mais facilmente. O mais apropriado seria gerar o código na pasta "target/generated-sources/".

Desta forma, não correríamos o risco de modificar o código gerado acidentalmente, o que não é uma prática muito recomendada, uma vez que ao gerá-lo novamente perderia todas as alterações feitas.

Passo 5: utilizando templates

O exemplo funcionou, mas tem um problema. Veja só esse trecho de código do gerador:

writer.append("package "+packageName+";\n\n");
writer.append("public class "+ctx.name.getText()+" {\n");

Está vendo esse "writer", "append", esses ”\n”, esses ”+ e +”? Difícil enxergar o que estamos gerando! Deve existir um jeito melhor!

E existe! Como discutido no início deste artigo, o uso de templates é a abordagem escolhida para realizar essa tarefa! Faremos isso neste passo.

Utilizaremos o Apache FreeMarker, um processador de templates de código aberto para Java.

Para utilizar o FreeMarker no projeto do plugin, primeiro é preciso adicionar sua dependência no "pom.xml":

    <dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>

Em seguida, vamos modificar o gerador para utilizar o FreeMarker. Para manter as duas versões e poder comparar depois, vamos criar uma cópia do arquivo "Generator.java". Vamos chamar a nova classe de "TemplateBasedGenerator.java".

Nesta classe, vamos modificar o construtor para inicializar o FreeMarker:

public class TemplateBasedGenerator extends PojoBaseVisitor<Boolean> {
String packageName;
File outputDir;
StringBuilder errors;
+ // Objeto de configuração do freeMarker
+ Configuration freeMarkerCfg;
public TemplateBasedGenerator(String packageName, File outputDir) {
this.packageName = packageName;
this.outputDir = outputDir;
errors = new StringBuilder();
+ // Inicializando o FreeMarker
+
+ // Inicializando a configuração. A versão garante que os problemas de incompatibilidade
+ // sejam minimizados
+ freeMarkerCfg = new Configuration(Configuration.VERSION_2_3_30);
+ // Informando ao FreeMarker onde procurar os templates. Neste caso, estamos dizendo
+ // para o FreeMarker olhar no classpath a partir desta classe (this.getClass).
+ // Isso inclui todas as classes do classpath, e no Maven, a pasta "src/main/resources"
+ // também. Estamos dizendo que queremos olhar dentro da pasta "templates", ali dentro
+ freeMarkerCfg.setClassForTemplateLoading(this.getClass(), "/templates/");
+ // Por último, vamos dizer que estamos escrevendo os templates em UTF-8
+ freeMarkerCfg.setDefaultEncoding("UTF-8");
+
+ // As seguintes linhas são opcionais no FreeMarker, pois muitos projetos em
+ // Java não utilizam campos públicos, e sim getters e setters, que funcionam por
+ // default no FreeMarker.
+ // Mas como o ANTLR gera campos públicos sem setters e getters, é preciso dizer
+ // ao FreeMarker que vamos utilizá-los
+ DefaultObjectWrapper ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_30);
+ ow.setExposeFields(true);
+ freeMarkerCfg.setObjectWrapper(ow);

}

Agora e hora de modificar o método "visitPojo" para utilizar o template:

@Override
public Boolean visitPojo(PojoContext ctx) {
if (!outputDir.exists()) {
outputDir.mkdirs();
}
String pojoName = ctx.name.getText();
List<AttributeContext> pojoAttributes = ctx.attribute();
+ // Vamos inicializar um mapa para passar os
+ // elementos do "Pojo" para o template
+ Map<String, Object> context = new HashMap<>();
+ context.put("packageName", packageName);
+ context.put("pojoName", pojoName);
+ context.put("pojoAttributes", pojoAttributes);
try (FileWriter writer = new FileWriter(new File(outputDir, pojoName + ".java"))) {
+ // Podemos agora apagar todo o restante do conteúdo e substituir
+ // pela chamada ao template
+ Template temp = freeMarkerCfg.getTemplate("pojotemplate.ftl");
+ temp.process(context, writer);
return true;
} catch (Exception ioe) {
errors.append(ioe.getMessage());
return false;
}
}

Note que não há mais nada referente ao código a ser gerado aqui! Tudo foi para o template. Vamos então criá-lo. Para este exemplo, o nome do arquivo deve ser "src/main/resources/templates/pojotemplate.ftl". O conteúdo é o seguinte:

package ${packageName};// Classe gerada automaticamente. Não modificar!
public class ${pojoName} {
// Atributos de ${pojoName}
<#list pojoAttributes as attr>
private ${attr.type.text} ${attr.name.text};
</#list>
// Setters e Getters de ${pojoName}
<#list pojoAttributes as attr>
public ${attr.type.text} get${attr.name.text?cap_first}() {
return this.${attr.name.text};
}
public void set${attr.name.text?cap_first}(${attr.type.text} ${attr.name.text}) {
this.${attr.name.text} = ${attr.name.text};
}
</#list>
}

Note como, no template, o código a ser gerado aparece claramente, e não dentro de strings em métodos do tipo “append”, como antes. Esse código será copiado, de acordo com a lógica de processamento definida em marcações especiais (delimitadas por $ e #, neste exemplo). Isso torna a tarefa de gerar código mais fácil e direta.

E já que estamos vendo muito melhor o código a ser gerado, podemos aproveitar para escrever outras coisas também. Neste exemplo, estamos colocando alguns comentários para avisar ao programador que o código é gerado, para não mexer. Também há comentários para descrever partes do código sendo gerado.

Agora, vamos às marcações. Neste template, estamos utilizando dois tipos de marcações. As marcações com "${}" são uma substituição simples. Por exemplo:

package ${packageName};

Neste exemplo, "${packageName}" irá substituir o valor que foi armazenado anteriormente (lá no visitante) com esse mesmo nome:

context.put("packageName", packageName);

Neste template também temos uma marcação "<#list>". Esta marcação serve para percorrer uma lista, neste caso, a lista de atributos do “Pojo”. No exemplo a seguir, estamos percorrendo a lista de atributos e gerando um atributo "private" para cada um.

<#list pojoAttributes as attr>
private ${attr.type.text} ${attr.name.text};
</#list>

Veja também como acessamos os campos do atributo, exatamente como gerado pelo ANTLR. "${attr.name.text}" e "${attr.type.text}" apontam diretamente para os respectivos lexemas definidos na seguinte regra sintática:

attribute: name=ID ':' type=ID;

Por último, veja que utilizamos uma diretiva "?cap_first" para criar uma nova string onde apenas o primeiro caracter é capitalizado. O FreeMarker tem uma série de funções úteis como esta!

Vamos atualizar nosso plugin agora. Na classe "MyMojo.java", localize o seguinte trecho e substitua o antigo objeto visitante pelo novo que acabamos de criar:

// Substituir a seguinte linha pela próxima
//Generator g = new Generator(packageName, outputDir);
TemplateBasedGenerator g = new TemplateBasedGenerator(packageName, outputDir);

Agora é só instalar o plugin novamente. Na raiz do projeto "pojo-generator-maven-plugin", execute:

mvn clean install

Agora vamos para a raiz do projeto "projeto-teste" novamente e executar:

mvn generate-sources

Se tudo der certo, o novo código será gerado corretamente. Exceto pelos novos comentários, nada mudou. Mas por baixo dos panos, agora estamos usando templates para gerar o código de um jeito muito melhor.

Passo 6: incrementando um pouco nossa solução

Vamos modificar um pouco a gramática para acrescentar duas coisas: a possibilidade de definir os atributos como somente leitura ou leitura/escrita e a possibilidade de incluir atributos multivalorados. Para isso, vamos modificar levemente nossa gramática, na regra "attribute":

attribute: (readonly=’-’)? name=ID ‘:’ type=ID (multivalued=’*’)?;

O símbolo "-" no início indica que um atributo é somente leitura, e o símbolo "*" no final indica que o atributo é multivalorado. Ambos os símbolos são opcionais (note o "?" após a definição destes símbolos). Assim, poderíamos definir o seguinte “Pojo”, onde o "cpf" é somente leitura e há múltiplos telefones:

Cliente {
nome: String
-cpf: String
telefones: String*
}

Para gerar o novo parser, basta executar:

mvn compile

Agora precisamos atualizar o template. As mudanças são poucas, e a esta altura não devem ser difíceis de compreender:

package ${packageName};// Classe gerada automaticamente. Não modificar!
public class ${pojoName} {
// Atributos de ${pojoName}
<#list pojoAttributes as attr>
+ <#if !attr.multivalued?exists>
private ${attr.type.text} ${attr.name.text};
+ <#else>
+ private java.util.List<${attr.type.text}> ${attr.name.text} = new java.util.ArrayList<>();
+ </#if>
</#list>
// Setters e Getters de ${pojoName}
<#list pojoAttributes as attr>
+ <#if !attr.multivalued?exists>
public ${attr.type.text} get${attr.name.text?cap_first}() {
return this.${attr.name.text};
}
+ <#if !attr.readonly?exists>
public void set${attr.name.text?cap_first}(${attr.type.text} ${attr.name.text}) {
this.${attr.name.text} = ${attr.name.text};
}
+ </#if>
+ <#else>
+ public ${attr.type.text} get${attr.name.text?cap_first}(int index) {
+ return this.${attr.name.text}.get(index);
+ }
+ <#if !attr.readonly?exists>
+ public void addElementTo${attr.name.text?cap_first}(${attr.type.text} ${attr.name.text}) {
+ this.${attr.name.text}.add(${attr.name.text});
+ }
+ </#if>
+ </#if>

</#list>
}

Neste novo template, note como utilizamos as marcações "<#if>", "<#else>" e "?exists" para gerar o código condicionalmente: para atributos multivalorados gera-se uma lista, com "get" e "add" ao invés de "get" e "set"; e o "setter"/"adder" só é gerado para atributos que não são somente leitura. Teste e veja você mesmo o resultado.

Passo 7: incrementando ainda mais nossa solução

Vamos agora gerar uma definição de serviços de uma API HTTP para finalizar este artigo com algo um pouco mais interessante. O primeiro passo é modificar a gramática. Ela não muda muito. Vamos apenas acrescentar a possibilidade de definir serviços, além dos “Pojos”:

grammar Pojo;ID: [a-zA-Z_][a-zA-Z0-9_]*; 
WS: [ \r\n\t]+ -> skip;
+ program: (pojo | service)*;pojo: name=ID '{' attribute* '}';
attribute: (readonly='-')? name=ID ':' type=ID (multivalued='*')?;
+ service: name=ID '(' (parameter (',' parameter)*)? ')' ':' returnType=ID;
+ parameter: name=ID ':' type=ID;

Agora um “program” é composto de uma repetição de zero ou mais “pojos” ou “services” (o símbolo “|” significa alternativa). Um “service” é como uma definição de uma função, com nome, lista de “parameter” separados por vírgula e um tipo de retorno. Veja que o ANTLR irá juntar todas as ocorrências de “parameter” em uma única lista, facilitando assim seu acesso no gerador. Por fim, um “parameter” é um par nome/tipo, separados por “:”.

Nosso visitante também vai sofrer algumas mudanças. Primeiro, vamos adicionar um novo método de visitação: "visitProgram".

...
@Override
public Boolean visitProgram(ProgramContext ctx) {
// Esta é a regra raiz da nossa gramática, portanto como estamos sobrescrevendo-a,
// precisamos começar chamando nosso visitante já existente (visitPojo)
// Antes não precisava disso pois esse já é o comportamento padrão do BaseVisitor
for (PojoContext pojo : ctx.pojo()) {
if (!visitPojo(pojo)) {
return false;
}
}
// Em seguida, vamos criar uma classe com todos os serviços. Veja como a estrutura
// é a mesma que já criamos anteriormente
try (FileWriter writer = new FileWriter(new File(outputDir, "HttpServices.java"))) {
Template temp = freeMarkerCfg.getTemplate("httpservicestemplate.ftl");
Map<String, Object> context = new HashMap<>();
context.put("packageName", packageName);
// Desta vez vamos passar o objeto inteiro do ANTLR para o FreeMarker
context.put("services", ctx.service());
temp.process(context, writer);
// Em seguida vamos visitar os serviços, para poder criar classes para os
// requests
for (ServiceContext service : ctx.service()) {
if (!visitService(service)) {
return false;
}
}
return true;
} catch (Exception ioe) {
errors.append(ioe.getMessage());
return false;
}
}

O método "visitPojo" não muda, então vamos deixar ele como está:

@Override
public Boolean visitPojo(PojoContext ctx) { ... }

Por fim, vamos sobrescrever o método "visitService":

// Para cada serviço, vamos gerar uma classe que encapsula os parâmetros de entrada
// Fazemos isso a seguir. Novamente, nenhuma novidade aqui além de instanciar e
// chamar o template
@Override
public Boolean visitService(ServiceContext ctx) {
String requestClassName = firstUpper(ctx.name.getText() + “Request”);
try (FileWriter writer = new FileWriter(new File(outputDir, requestClassName + “.java”))) {
Template temp = freeMarkerCfg.getTemplate(“servicerequesttemplate.ftl”);
Map<String, Object> context = new HashMap<>();
context.put(“packageName”, packageName);
context.put(“service”, ctx);
temp.process(context, writer);
return true;
} catch (Exception ioe) {
errors.append(ioe.getMessage());
return false;
}
}
// Precisamos voltar a ter um método para capitalizar a primeira letra, pois
// precisamos disso para o nome das classes, antes de chamar o template
private String firstUpper(String text) {
return text.substring(0, 1).toUpperCase() + text.substring(1);
}
}

A verdadeira “mágica” acontece nos templates. Primeiro, vamos criar o template para a classe de serviços. O arquivo é "src/main/resources/templates/httpservicestemplate.ftl":

package ${packageName};import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path(“services”)
public class HttpServices {
<#list services as service>
@POST
@Path(“/${service.name.text}”)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response ${service.name.text}(${service.name.text?cap_first}Request request) {
// Get parameters
<#list service.parameter() as p>
final ${p.type.text} ${p.name.text} = request.${p.name.text};
</#list>
// TODO: insert service logic here

return Response.ok().build();
}
</#list>
}

Este template produz uma lista de métodos (um para cada serviço). Para cada serviço, inicializa os parâmetros e constrói uma resposta vazia. O completo entendimento desse template fica como aprendizado para o leitor. Mas note que não estamos utilizando nenhuma diretriz nova aqui. Um detalhe importante é a chamada "service.parameter()". Normalmente, não precisaríamos chamar o método com "()" explícito, mas como o ANTLR gera duas versões do método "parameter", precisamos deixar claro em qual versão estamos interessados.

Outro template que precisaremos serve para criar as classes que encapsulam os parâmetros dos requests, uma para cada serviço. O arquivo é "src/main/resources/templates/servicerequesttemplate.ftl":

package ${packageName};public class ${service.name.text?cap_first}Request {
<#list service.parameter() as p>
public ${p.type.text} ${p.name.text};
</#list>
}

Novamente, nenhum segredo aqui! Essa classe irá encapsular o request de um serviço específico.

Já estamos prontos para os testes. Vamos instalar a nova versão do plugin:

mvn clean install

No "projeto-teste" vamos criar uma nova versão do arquivo de entrada:

Cliente {
nome: String
-cpf: String
telefones: String*
}
Produto {
id: int
descricao: String
}
Fatura {
preco: double
produto: Produto
}
consultarCliente(cpf: String) : Cliente
inserirProduto(id: int, descricao: String) : Produto

Foram definidos dois serviços, um para consultar um cliente e um para inserir um produto. Antes de gerar o código, acrescente a dependência no "pom.xml" à biblioteca "JAX-RS", pois o código gerado utiliza esta biblioteca.

<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>2.1.1</version>
</dependency>

Para gerar o código:

mvn generate-sources

O resultado é uma classe com os serviços, anotados no padrão "JAX-RS" e uma classe nova para cada serviço, encapsulando os parâmetros de entrada:

package com.exemplo.gerado;import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path(“services”)
public class HttpServices {
@POST
@Path(“/consultarCliente”)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response consultarCliente(ConsultarClienteRequest request) {
// Get parameters
final String cpf = request.cpf;
// TODO: insert service logic here

return Response.ok().build();
}
@POST
@Path(“/inserirProduto”)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response inserirProduto(InserirProdutoRequest request) {
// Get parameters
final int id = request.id;
final String descricao = request.descricao;
// TODO: insert service logic here

return Response.ok().build();
}
}
public class ConsultarClienteRequest {
public String cpf;
}
public class InserirProdutoRequest {
public int id;
public String descricao;
}

Indo além

O exemplo apresentado mostra que ainda há muito a ser feito, mas já demonstra o grande potencial da programação generativa. Como estudos, deixo aqui as seguintes sugestões de extensão:

  • O exemplo não lida com os retornos dos serviços. Poderia haver um template que gera uma classe que encapsula a resposta em JSON, por exemplo;
  • O exemplo não permite a separação dos serviços em diferentes rotas. Poderia haver uma configuração na gramática para agrupar os serviços em recursos, cada um com sua própria rota;
  • O exemplo não permite a especificação do método HTTP dos serviços (GET, POST, DELETE, etc.). Isso poderia ser configurado na gramática e nos templates;
  • O exemplo não permite a inserção do corpo (implementação) de cada serviço. Isso poderia ser feito por meio de injeção de dependência (para evitar que o programador tenha que modificar o código gerado).

Se tiver seguido os exemplos passo-a-passo, você já está pronto para começar a tentar implementar essas extensões. Caso contrário, eu deixei todo o código feito até aqui no GitHub. Fique à vontade para baixar e utilizar à vontade.

A partir de agora, você já sabe o passo-a-passo básico de como colocar a programação generativa em prática. Daqui para frente, para se aprofundar no assunto, são duas as frentes de estudo.

A primeira é estudar construção de compiladores. Não é necessário estudar de maneira exaustiva para conseguir fazer coisas bem legais. Com pouco tempo e conhecendo apenas uns poucos operadores das gramáticas livres de contexto, já dá para fazer quase tudo, exceto as construções mais complicadas de uma linguagem. Mas conhecer os fundamentos exige sim um pouco de estudo. Eu deixo duas recomendações de material:

  • O livro do autor do ANTLR é muito bom. Completo, didático e com um preço relativamente acessível, é uma leitura obrigatória para quem quiser conhecer o ANTLR e aprender a construir linguagens mais complexas com uma boa fundamentação;
  • Também tenho um curso online completo, com vídeo-aulas, exemplos de código e exercícios. É completamente gratuito e disponível para qualquer interessado em aprender.

A segunda frente de estudo é conhecer melhor as marcações do FreeMarker, para conseguir inserir corretamente a lógica dos templates. O guia oficial é a referência completa do assunto.

E depois dessas duas frentes, quando já estiver trabalhando com programação generativa e a solução aqui apresentada estiver ficando complicada demais, com muitos templates conversando entre si, indico estudar alguma language workbench.

Minha recomendação é a excelente Eclipse Xtext que é baseada no Eclipse, mas tem suporte para outros ambientes. Xtext possibilita a criação de linguagens muito mais robustas, com editores com auto-completing, syntax highlighting, uma camada orientada a objetos como alternativa à árvore sintática do ANTLR e uma linguagem de templates que facilita o gerenciamento de muitos templates.

Conclusão

A programação generativa pode ser uma poderosa aliada para o engenheiro de software. Normalmente as tecnologias envolvidas, principalmente as dos compiladores, são vistas com receio pelo programador comum.

Mas a verdade é que, apesar de ser de fato um assunto complexo, é possível criar coisas muito interessantes sem precisar se aprofundar demasiadamente no assunto. Espero ter removido um pouco desse receio com este artigo, dando os passos iniciais para que o leitor possa seguir seu caminho.

Por fim, apesar de todos os exemplos utilizarem a linguagem Java, reforço que há tecnologias equivalentes em várias linguagens, como já comentei no início. Deixo aqui o convite para, caso consiga reproduzir os exemplos aqui em outra linguagem, que também publique este conteúdo para compartilhar o conhecimento com a comunidade.

Se você busca uma oportunidade de desenvolvimento, trabalhando com inovação em um negócio de alto impacto, acesse o portal B2W Carreiras! Nele, você consegue acessar todas as vagas disponíveis. Venha fazer parte do nosso time!

--

--