Padrão de Design de Intérpretes em Java
1. Visão geral
Neste tutorial, apresentaremos um dos padrões de design comportamental do GoF - o intérprete.
Inicialmente, forneceremos uma visão geral de seu objetivo e explicaremos o problema que ele tenta resolver.
Em seguida, veremos o diagrama UML do intérprete e a implementação do exemplo prático.
2. Padrão de Design de Intérpretes
Resumindo, o padrãodefines the grammar of a particular language de uma forma orientada a objetos que pode ser avaliada pelo próprio interpretador.
Tendo isso em mente, tecnicamente poderíamos construir nossa expressão regular customizada, um interpretador DSL customizado ou poderíamos analisar qualquer uma das linguagens humanas,build abstract syntax trees and then run the interpretation.
Esses são apenas alguns dos casos de uso em potencial, mas se pensarmos um pouco, poderíamos encontrar ainda mais usos, por exemplo em nossos IDEs, uma vez que eles interpretam continuamente o código que estamos escrevendo e, portanto, nos fornecem dicas inestimáveis.
O padrão do intérprete geralmente deve ser usado quando a gramática é relativamente simples.
Caso contrário, pode ficar difícil de manter.
3. Diagrama UML
O diagrama acima mostra duas entidades principais:ContexteExpression.
Agora, qualquer idioma precisa ser expresso de alguma maneira, e as palavras (expressões) terão algum significado com base no contexto fornecido.
AbstractExpression define um método abstrato que leva o contexto como um parâmetro. Graças a isso,each expression will affect the context, mude seu estado e continue a interpretação ou retorne o próprio resultado.
Portanto, o contexto vai ser o detentor do estado global de processamento, e vai ser reutilizado durante todo o processo de interpretação.
Então, qual é a diferença entreTerminalExpression eNonTerminalExpression?
UmNonTerminalExpression pode ter um ou mais outrosAbstractExpressions associados a ele, portanto, pode ser interpretado recursivamente. No final,the process of interpretation has to finish with a TerminalExpression that will return the result.
É importante notar queNonTerminalExpression é umcomposite.
Finalmente, a função do cliente é criar ou usar umabstract syntax tree já criado, que nada mais é do que umsentence defined in the created language.
4. Implementação
Para mostrar o padrão em ação, vamos construir uma sintaxe simples semelhante a SQL de uma forma orientada a objetos, que será então interpretada e nos retornará o resultado.
Primeiro, vamos definir as expressõesSelect, From,eWhere, construir uma árvore de sintaxe na classe do cliente e executar a interpretação.
A interfaceExpression terá o método interpretar:
List interpret(Context ctx);
A seguir, definimos a primeira expressão, a classeSelect:
class Select implements Expression {
private String column;
private From from;
// constructor
@Override
public List interpret(Context ctx) {
ctx.setColumn(column);
return from.interpret(ctx);
}
}
Ele obtém o nome da coluna a ser selecionado e outrosExpression concretos do tipoFrom como parâmetros no construtor.
Observe que no métodointerpret() sobrescrito, ele define o estado do contexto e passa a interpretação para outra expressão junto com o contexto.
Dessa forma, vemos que é umNonTerminalExpression.
Outra expressão é a classeFrom:
class From implements Expression {
private String table;
private Where where;
// constructors
@Override
public List interpret(Context ctx) {
ctx.setTable(table);
if (where == null) {
return ctx.search();
}
return where.interpret(ctx);
}
}
Agora, no SQL, a cláusula where é opcional, portanto, essa classe é uma expressão terminal ou não terminal.
Se o usuário decidir não usar uma cláusula where, a expressãoFrom será encerrada com a chamadactx.search() e retornará o resultado. Caso contrário, será mais interpretado.
AWhere expressão está novamente modificando o contexto, definindo o filtro necessário e termina a interpretação com a chamada de pesquisa:
class Where implements Expression {
private Predicate filter;
// constructor
@Override
public List interpret(Context ctx) {
ctx.setFilter(filter);
return ctx.search();
}
}
Para o exemplo, o sclassContext contém os dados que estão imitando a tabela do banco de dados.
Observe que ele possui três campos-chave que são modificados por cada subclasse deExpressione o método de pesquisa:
class Context {
private static Map> tables = new HashMap<>();
static {
List list = new ArrayList<>();
list.add(new Row("John", "Doe"));
list.add(new Row("Jan", "Kowalski"));
list.add(new Row("Dominic", "Doom"));
tables.put("people", list);
}
private String table;
private String column;
private Predicate whereFilter;
// ...
List search() {
List result = tables.entrySet()
.stream()
.filter(entry -> entry.getKey().equalsIgnoreCase(table))
.flatMap(entry -> Stream.of(entry.getValue()))
.flatMap(Collection::stream)
.map(Row::toString)
.flatMap(columnMapper)
.filter(whereFilter)
.collect(Collectors.toList());
clear();
return result;
}
}
Depois que a pesquisa é concluída, o contexto é limpo automaticamente, para que a coluna, a tabela e o filtro sejam definidos como padrões.
Dessa forma, cada interpretação não afetará a outra.
5. Teste
Para fins de teste, vamos dar uma olhada no sclassInterpreterDemo :
public class InterpreterDemo {
public static void main(String[] args) {
Expression query = new Select("name", new From("people"));
Context ctx = new Context();
List result = query.interpret(ctx);
System.out.println(result);
Expression query2 = new Select("*", new From("people"));
List result2 = query2.interpret(ctx);
System.out.println(result2);
Expression query3 = new Select("name",
new From("people",
new Where(name -> name.toLowerCase().startsWith("d"))));
List result3 = query3.interpret(ctx);
System.out.println(result3);
}
}
Primeiro, construímos uma árvore de sintaxe com expressões criadas, inicializamos o contexto e depois executamos a interpretação. O contexto é reutilizado, mas, como mostramos acima, ele se limpa após cada chamada de pesquisa.
Ao executar o programa, a saída deve ser a seguinte:
[John, Jan, Dominic]
[John Doe, Jan Kowalski, Dominic Doom]
[Dominic]
6. Desvantagens
Quando a gramática está ficando mais complexa, fica mais difícil de manter.
Isso pode ser visto no exemplo apresentado. Seria razoavelmente fácil adicionar outra expressão, comoLimit, mas não seria muito fácil de manter se decidirmos continuar estendendo-a com todas as outras expressões.
7. Conclusão
O padrão de design do interpretador é ótimofor relatively simple grammar interpretation, que não precisa evoluir e se estender muito.
No exemplo acima, mostramos que é possível criar uma consulta semelhante ao SQL de maneira orientada a objetos com a ajuda do padrão do interpretador.
Finalmente, você pode encontrar esse uso de padrão no JDK, particularmente emjava.util.Pattern,java.text.Format oujava.text.Normalizer.
Como de costume, o código completo está disponível emthe Github project.