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

Шаблон дизайна переводчика в Java

1. обзор

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

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

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

2. Шаблон дизайна переводчика

Короче говоря, шаблонdefines the grammar of a particular language объектно-ориентированным способом, который может быть оценен самим интерпретатором.

Имея это в виду, технически мы могли бы создать собственное регулярное выражение, собственный интерпретатор DSL или проанализировать любой из человеческих языковbuild abstract syntax trees and then run the interpretation.

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

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

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

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

image

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

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

AbstractExpression  определяет один абстрактный метод, который принимает контекст  как параметр. Благодаря этомуeach expression will affect the context изменит свое состояние и либо продолжит интерпретацию, либо вернет сам результат.

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

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

СNonTerminalExpression может быть связано одно или несколько другихAbstractExpressions, поэтому его можно рекурсивно интерпретировать. В итогеthe process of interpretation has to finish with a TerminalExpression that will return the result.

Стоит отметить, чтоNonTerminalExpression - этоcomposite.

Наконец, роль клиента состоит в том, чтобы создать или использовать уже созданныйabstract syntax tree, который является не чем иным, какsentence defined in the created language.

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

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

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

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

List interpret(Context ctx);

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

class Select implements Expression {

    private String column;
    private From from;

    // constructor

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

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

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

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

Другое выражение - это классFrom:

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

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

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

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

class Where implements Expression {

    private Predicate filter;

    // constructor

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

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

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

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

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

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

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

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

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

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

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

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

6. Downsides

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

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

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

Отличный шаблон проектирования интерпретатораfor relatively simple grammar interpretation, который не требует значительного развития и расширения.

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

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

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