Статьи

Шаблон посетителя повторно посещен

Шаблон посетителя — один из самых переоцененных и все же недооцененных шаблонов в объектно-ориентированном дизайне. Переоценен, потому что он часто выбирается слишком быстро ( возможно, астронавтом архитектуры ), а затем раздувается очень простой дизайн, если его добавить неправильно. Недооценивать, потому что это может быть очень мощным, если вы не последуете примеру учебника. Давайте посмотрим подробно.

Проблема № 1: Наименование

Самый большой недостаток (на мой взгляд) — это само название. Шаблон «посетитель». Когда мы гуглим его, мы, скорее всего, окажемся в соответствующей статье Википедии , показывающей забавные картинки, подобные этой:

Пример шаблона посетителя Википедии

Правильно. Для 98% из нас, которые думают о колесах, двигателях и кузовах в своей каждодневной работе по разработке программного обеспечения, это сразу становится ясно, потому что мы знаем, что механик, выставляющий нам несколько тысяч долларов за починку нашего автомобиля, сначала посетит колеса, а затем двигатель. прежде чем в конце концов посетить наш кошелек и принять наши деньги. Если нам не повезет, он также навестит нашу жену, пока мы на работе, но она никогда не примет эту верную душу.

Но как насчет 2%, которые решают другие проблемы в их трудовой жизни? Например, когда мы кодируем сложные структуры данных для систем электронного банкинга, клиентов фондовых бирж, интранет-порталов и т. Д. И т. Д. Почему бы не применить шаблон посетителей к действительно иерархической структуре данных? Нравится папки и файлы? (хорошо, не так уж и сложно)

Итак, мы будем «посещать» папки, и каждая папка будет позволять своим файлам «принимать» «посетителя», а затем мы также разрешаем посетителю «посещать» файлы. Какая?? Автомобиль позволяет своим частям принять посетителя, а затем позволить посетителю посетить себя? Условия вводят в заблуждение. Они универсальны и хороши для дизайна. Но они убьют ваш реальный дизайн, потому что никто не думает с точки зрения «принятия» и «посещения», когда на самом деле вы читаете / пишете / удаляете / изменяете свою файловую систему.

Проблема № 2: полиморфизм

Это та часть, которая вызывает еще большую головную боль, чем присвоение имен, когда применяется в неправильной ситуации. С какой стати посетитель знает всех остальных? Зачем посетителю нужен метод для каждого задействованного элемента в иерархии? Полиморфизм и инкапсуляция утверждают, что реализация должна быть скрыта за API. API (нашей структуры данных), вероятно, каким-то образом реализует составной шаблон , то есть его части наследуются от общего интерфейса. ОК, конечно, колесо — это не машина, и моя жена не механик. Но когда мы берем структуру папок / файлов, разве они не все объекты java.util.File?

Понимание проблемы

Фактическая проблема заключается не в наименовании и ужасной многословности API посещения кода, а в неправильном понимании шаблона. Это не шаблон, который лучше всего подходит для посещения больших и сложных структур данных с большим количеством объектов разных типов. Это шаблон, который лучше всего подходит для посещения простых структур данных с несколькими различными типами, но посещение их с сотнями посетителей. Возьмите файлы и папки. Это простая структура данных. У вас есть два типа. Один может содержать другой, оба имеют некоторые общие свойства. Различные посетители могут быть:

  • CalculateSizeVisitor
  • FindOldestFileVisitor
  • DeleteAllVisitor
  • FindFilesByContentVisitor
  • ScanForVirusesVisitor
  • … ты называешь это

Я по-прежнему не люблю именование, но шаблон отлично работает в этой парадигме.

Так когда же шаблон посетителя «неправильный»?

Я хотел бы привести структуру jOOQ QueryPart в качестве примера. Их очень много, они моделируют различные конструкции SQL-запросов, позволяя jOOQ создавать и выполнять SQL-запросы произвольной сложности. Давайте назовем несколько примеров:

  • Состояние
    • CombinedCondition
    • NotCondition
    • В состоянии
    • BetweenCondition
  • поле
    • TableField
    • функция
    • агрегатная функция
    • BindValue
  • Список полей

Есть много других. Каждый из них должен иметь возможность выполнять два действия: отображать SQL и связывать переменные. Это сделало бы двух посетителей, каждый из которых знал бы больше … 40-50 типов …? Возможно, в далеком будущем jOOQ-запросы смогут отображать JPQL или какой-либо другой тип запроса. Это сделало бы 3 посетителей против 40-50 типов. Понятно, что здесь классический шаблон посетителей — плохой выбор. Но я все еще хочу «посетить» QueryParts, делегируя рендеринг и привязку к более низким уровням абстракции.

Как это реализовать, тогда?

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

Таким образом, интуитивно, шаг 1 будет таким

1
2
3
4
5
6
7
8
9
interface QueryPart {
  // Let the QueryPart return its SQL
  String getSQL();
 
  // Let the QueryPart bind variables to a prepared
  // statement, given the next bind index, returning
  // the last bind index
  int bind(PreparedStatement statement, int nextIndex);
}

С помощью этого API мы можем легко абстрагировать SQL-запрос и делегировать обязанности артефактам более низкого уровня. Между условиями, например. Он заботится о правильном упорядочении частей условия [field] BETWEEN [lower] и [upper], отображая синтаксически правильный SQL, делегируя части задач его дочернему QueryParts:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
class BetweenCondition {
  Field field;
  Field lower;
  Field upper;
 
  public String getSQL() {
    return field.getSQL() + ' between ' +
           lower.getSQL() + ' and ' +
           upper.getSQL();
  }
 
  public int bind(PreparedStatement statement, int nextIndex) {
    int result = nextIndex;
 
    result = field.bind(statement, result);
    result = lower.bind(statement, result);
    result = upper.bind(statement, result);
 
    return result;
  }
}

В то время как BindValue, с другой стороны, будет в основном заботиться о привязке переменных

01
02
03
04
05
06
07
08
09
10
11
12
class BindValue {
  Object value;
 
  public String getSQL() {
    return '?';
  }
 
  public int bind(PreparedStatement statement, int nextIndex) {
    statement.setObject(nextIndex, value);
    return nextIndex + 1;
  }
}

В совокупности теперь мы можем легко создать условия этой формы: МЕЖДУ? И ?. Когда будет реализовано больше QueryParts, мы также сможем представить такие вещи, как MY_TABLE.MY_FIELD BETWEEN? И (ВЫБРАТЬ? ОТ ДВОЙНОГО), когда доступны соответствующие реализации полей. Вот что делает составной шаблон таким мощным, общий API и множество компонентов, инкапсулирующих поведение, делегируя части поведения подкомпонентам.

Шаг 2 заботится об эволюции API

Составной шаблон, который мы видели до сих пор, довольно интуитивен, но все же очень силен. Но рано или поздно нам понадобятся дополнительные параметры, поскольку мы обнаружим, что хотим передать состояние от родительского QueryParts их дочерним элементам. Например, мы хотим иметь возможность встроить некоторые значения связывания для некоторых предложений. Возможно, некоторые диалекты SQL не допускают привязки значений в предложении BETWEEN. Как справиться с этим с текущим API? Расширить его, добавив параметр «логическое значение»? Нет! Это одна из причин, по которой был изобретен шаблон посетителей. Для простоты API элементов составной структуры (нужно только реализовать «принять»). Но в этом случае гораздо лучше, чем реализовать шаблон истинного посетителя, это заменить параметры «контекстом»:

1
2
3
4
5
6
7
interface QueryPart {
  // The QueryPart now renders its SQL to the context
  void toSQL(RenderContext context);
 
  // The QueryPart now binds its variables to the context
  void bind(BindContext context);
}

Вышеупомянутые контексты будут содержать свойства, подобные этим (методы setters и render возвращают сам контекст, чтобы обеспечить цепочку методов):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface RenderContext {
  // Whether we're inlining bind variables
  boolean inline();
  RenderContext inline(boolean inline);
 
  // Whether fields should be rendered as a field declaration
  // (as opposed to a field reference). This is used for aliased fields
  boolean declareFields();
  RenderContext declareFields(boolean declare);
 
  // Whether tables should be rendered as a table declaration
  // (as opposed to a table reference). This is used for aliased tables
  boolean declareTables();
  RenderContext declareTables(boolean declare);
 
  // Whether we should cast bind variables
  boolean cast();
 
  // Render methods
  RenderContext sql(String sql);
  RenderContext sql(char sql);
  RenderContext keyword(String keyword);
  RenderContext literal(String literal);
 
  // The context's 'visit' method
  RenderContext sql(QueryPart sql);
}

То же самое касается BindContext. Как видите, этот API довольно расширяемый, можно добавлять новые свойства, также можно добавлять другие распространенные средства рендеринга SQL. Но BetweenCondition не нужно отказываться от своих инкапсулированных знаний о том, как визуализировать свой SQL, и о том, разрешены ли переменные связывания или нет. Это будет держать это знание при себе:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class BetweenCondition {
  Field field;
  Field lower;
  Field upper;
 
  // The QueryPart now renders its SQL to the context
  public void toSQL(RenderContext context) {
    context.sql(field).keyword(' between ')
           .sql(lower).keyword(' and ')
           .sql(upper);
  }
 
  // The QueryPart now binds its variables to the context
  public void bind(BindContext context) {
    context.bind(field).bind(lower).bind(upper);
  }
}

В то время как BindValue, с другой стороны, будет в основном заботиться о привязке переменных

01
02
03
04
05
06
07
08
09
10
11
class BindValue {
  Object value;
 
  public void toSQL(RenderContext context) {
    context.sql('?');
  }
 
  public void bind(BindContext context) {
    context.statement().setObject(context.nextIndex(), value);
  }
}

Вывод: назовите его Context-Pattern, а не Visitor-Pattern

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

  1. Если у вас много посетителей и относительно простая структура данных (несколько типов), шаблон посетителей, вероятно, в порядке.
  2. Если у вас много типов и относительно небольшой набор посетителей (мало вариантов поведения), шаблон посетителя излишний, придерживайтесь составного шаблона
  3. Чтобы обеспечить простую эволюцию API, спроектируйте свои составные объекты, чтобы иметь методы, принимающие один параметр контекста.
  4. Внезапно, вы снова окажетесь с моделью «почти посетитель», где context = посетитель, «визит» и «принять» = »названия ваших собственных методов

«Шаблон контекста» в то же время интуитивно понятен, как «Составной шаблон», и мощен, как «Шаблон посетителя», сочетая в себе лучшее из обоих миров.

Ссылка: шаблон посетителя, повторно посещенный нашим партнером JCG Лукасом Эдером в блоге JAVA, SQL и AND JOOQ .