Перейти к основному содержимому

Шаблон проектирования интерпретатора в Java

· 5 мин. чтения

1. Обзор

В этом уроке мы познакомимся с одним из поведенческих шаблонов проектирования GoF — интерпретатором.

Сначала мы дадим обзор его назначения и объясним проблему, которую он пытается решить.

Затем мы рассмотрим UML-диаграмму интерпретатора и реализацию практического примера.

2. Шаблон проектирования интерпретатора

Короче говоря, шаблон определяет грамматику конкретного языка объектно-ориентированным способом, который может быть оценен самим интерпретатором.

Имея это в виду, технически мы могли бы создать наше собственное регулярное выражение, собственный интерпретатор DSL или мы могли бы проанализировать любой из человеческих языков, построить абстрактные синтаксические деревья, а затем запустить интерпретацию.

Это лишь некоторые из потенциальных вариантов использования, но если мы немного подумаем, мы могли бы найти еще больше вариантов его использования, например, в наших IDE, поскольку они постоянно интерпретируют код, который мы пишем, и, таким образом, предоставляют нам бесценные подсказки.

Шаблон интерпретатора обычно следует использовать, когда грамматика относительно проста.

В противном случае его будет сложно поддерживать.

3. UML-диаграмма

./6ef5f87a5234c1b82354ed60c4cb754e.png

На приведенной выше диаграмме показаны две основные сущности: контекст и выражение .

Теперь любой язык должен быть каким-то образом выражен, и слова (выражения) будут иметь какое-то значение в зависимости от данного контекста.

AbstractExpression определяет один абстрактный метод, который принимает контекст `` в качестве параметра. Благодаря этому каждое выражение будет влиять на контекст , менять свое состояние и либо продолжать интерпретацию, либо возвращать само результат.

Следовательно, контекст будет держателем глобального состояния обработки и будет повторно использоваться в течение всего процесса интерпретации.

Итак, в чем разница между TerminalExpression и NonTerminalExpression ?

Выражение NonTerminalExpression может иметь одно или несколько других связанных с ним абстрактных выражений , поэтому его можно интерпретировать рекурсивно. В конце концов, процесс интерпретации должен завершиться выражением TerminalExpression , которое вернет результат.

Стоит отметить, что NonTerminalExpression является составным .

Наконец, роль клиента заключается в создании или использовании уже созданного абстрактного синтаксического дерева , которое представляет собой не что иное, как предложение, определенное в созданном языке.

4. Реализация

Чтобы показать шаблон в действии, мы создадим простой объектно-ориентированный синтаксис, подобный SQL, который затем будет интерпретирован и вернет нам результат.

Сначала мы определим выражения Select, From и Where , построим синтаксическое дерево в клиентском классе и запустим интерпретацию.

Интерфейс Expression будет иметь метод интерпретации:

List<String> interpret(Context ctx);

Далее мы определяем первое выражение, класс Select :

class Select implements Expression {

private String column;
private From from;

// constructor

@Override
public List<String> interpret(Context ctx) {
ctx.setColumn(column);
return from.interpret(ctx);
}
}

Он получает имя столбца, которое нужно выбрать, и другое конкретное выражение типа From в качестве параметров в конструкторе.

Обратите внимание, что в переопределенном методе интерпретация() он устанавливает состояние контекста и передает интерпретацию дальше другому выражению вместе с контекстом.

Таким образом, мы видим, что это NonTerminalExpression.

Другим выражением является класс From :

class From implements Expression {

private String table;
private Where where;

// constructors

@Override
public List<String> interpret(Context ctx) {
ctx.setTable(table);
if (where == null) {
return ctx.search();
}
return where.interpret(ctx);
}
}

Теперь в SQL предложение where является необязательным, поэтому этот класс является либо терминальным, либо нетерминальным выражением.

Если пользователь решит не использовать предложение where, выражение From будет завершено вызовом ctx.search() и возвратит результат. В противном случае это будет интерпретироваться дополнительно.

Выражение Where снова изменяет контекст, устанавливая необходимый фильтр, и завершает интерпретацию поисковым вызовом:

class Where implements Expression {

private Predicate<String> filter;

// constructor

@Override
public List<String> interpret(Context ctx) {
ctx.setFilter(filter);
return ctx.search();
}
}

Например, класс Context содержит данные, имитирующие таблицу базы данных.

Обратите внимание, что у него есть три ключевых поля, которые изменяются каждым подклассом Expression и методом поиска:

class Context {

private static Map<String, List<Row>> tables = new HashMap<>();

static {
List<Row> 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<String> whereFilter;

// ...

List<String> search() {

List<String> 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;
}
}

После завершения поиска контекст очищается, поэтому для столбца, таблицы и фильтра устанавливаются значения по умолчанию.

Таким образом, каждая интерпретация не повлияет на другую.

5. Тестирование

В целях тестирования давайте взглянем на класс InterpreterDemo :

public class InterpreterDemo {
public static void main(String[] args) {

Expression query = new Select("name", new From("people"));
Context ctx = new Context();
List<String> result = query.interpret(ctx);
System.out.println(result);

Expression query2 = new Select("*", new From("people"));
List<String> result2 = query2.interpret(ctx);
System.out.println(result2);

Expression query3 = new Select("name",
new From("people",
new Where(name -> name.toLowerCase().startsWith("d"))));
List<String> result3 = query3.interpret(ctx);
System.out.println(result3);
}
}

Сначала мы строим синтаксическое дерево с созданными выражениями, инициализируем контекст и затем запускаем интерпретацию. Контекст используется повторно, но, как мы показали выше, очищается после каждого поискового вызова.

Запустив программу, вывод должен быть следующим:

[John, Jan, Dominic]
[John Doe, Jan Kowalski, Dominic Doom]
[Dominic]

6. Недостатки

Когда грамматика становится более сложной, ее становится труднее поддерживать.

Это видно на представленном примере. Было бы достаточно легко добавить еще одно выражение, такое как Limit , но его будет не так просто поддерживать, если мы решим продолжать расширять его всеми другими выражениями.

7. Заключение

Шаблон проектирования интерпретатора отлично подходит для относительно простой интерпретации грамматики , которую не нужно сильно развивать и расширять.

В приведенном выше примере мы показали, что можно построить SQL-подобный запрос объектно-ориентированным способом с помощью шаблона интерпретатора.

Наконец, вы можете найти использование этого шаблона в JDK, в частности, в java.util.Pattern , java.text.Format или java.text.Normalizer .

Как обычно, полный код доступен на проекте Github .