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);
}
}
Он получает имя столбца, которое нужно выбрать, и другое конкретное выражение
типа 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 .