«1. Обзор

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

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

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

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

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

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

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

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

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

3. Диаграмма UML

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

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

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);
    }
}

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

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

Таким образом, мы видим, что это 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.