Статьи

Новая жизнь старого шаблона дизайна посетителя

Вступление

Посетитель [1, 2] — широко известная модель классического дизайна. Есть много ресурсов, которые объясняют это в деталях. Не углубляясь в реализацию, я кратко напомню идею шаблона, объясню его преимущества и недостатки и предложу некоторые улучшения, которые можно легко применить к нему с помощью языка программирования Java.

Классический Посетитель

[Посетитель] Позволяет применять одну или несколько операций к набору объектов во время выполнения, отделяя операции от структуры объекта. (Банда четырех книг)

Шаблон основан на интерфейсе, который обычно называется. Visitable который должен быть реализован классом модели и набором Visitors которые реализуют метод (алгоритм) для каждого соответствующего класса модели.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public interface Visitable {
  public void accept(Visitor visitor);
}
 
public class Book implements Visitable {
   .......
   @Override public void accept(Visitor visitor) {visitor.visit(this)};
   .......
}
 
public class Cd implements Visitable {
   .......
   @Override public void accept(Visitor visitor) {visitor.visit(this)};
   .......
}
 
interface Visitor {
   public void visit(Book book);
   public void visit(Magazine magazine);
   public void visit(Cd cd);
}

Теперь мы можем реализовать различных visitors , например,

  • PrintVisitor который печатает при условии Visitable
  • DbVisitor который хранит его в базе данных,
  • ShoppingCart который добавляет его в корзину

и т.п.

Недостатки модели посетителя

  1. Тип возврата методов visit() должен быть определен во время разработки. На самом деле в большинстве случаев эти методы являются void .
  2. Реализации метода accept() идентичны во всех классах. Очевидно, мы предпочитаем избегать дублирования кода.
  3. Каждый раз, когда добавляется новый класс модели, каждый visitor должен обновляться, поэтому обслуживание становится трудным.
  4. Невозможно иметь необязательные реализации для определенного класса модели у определенного visitor . Например, программное обеспечение может быть отправлено покупателю по электронной почте, а молоко не может быть отправлено. Тем не менее, оба могут быть доставлены с использованием традиционной почтой. Таким образом, EmailSendingVisitor не может реализовать метод visit(Milk) но может реализовать visit(Software) . Возможное решение — выбросить исключение UnsupportedOperationException но вызывающая сторона не может заранее знать, что это исключение будет сгенерировано до вызова метода.

Улучшения в классической модели Visitor

Возвращаемое значение

Сначала добавим возвращаемое значение в интерфейс Visitor . Общее определение может быть сделано с использованием дженериков.

01
02
03
04
05
06
07
08
09
10
public interface Visitable {
  public <R> R accept(Visitor<R> visitor);
}
 
 
interface Visitor<R> {
   public R visit(Book book);
   public R visit(Magazine magazine);
   public R visit(Cd cd);
}

Ну, это было легко. Теперь мы можем применять к нашей Книге любого Visitor который возвращает значение. Например, DbVisitor может возвращать количество измененных записей в БД (целое число), а посетитель ToJson может возвращать JSON-представление нашего объекта в виде String. (Вероятно, пример не слишком органичен, в реальной жизни мы обычно используем другие методы для сериализации объекта в JSON, но это достаточно хорошо для теоретически возможного использования шаблона Visitor ).

Реализация по умолчанию

Далее, давайте поблагодарим Java 8 за способность хранить реализации по умолчанию внутри интерфейса:

1
2
3
4
5
public interface Visitable<R> {
  default R accept(Visitor<R> visitor) {
      return visitor.visit(this);
  }
}

Теперь класс, который реализует Visitable , не должен сам реализовывать >visit() : реализация по умолчанию в большинстве случаев достаточно хороша.

Улучшения, предложенные выше, исправляют недостатки # 1 и # 2.

MonoVisitor

Попробуем применить дальнейшие улучшения. Во-первых, давайте определим интерфейс MonoVisitor следующим образом:

1
2
3
public interface MonoVisitor<T, R> {
    R visit(T t);
}

Имя Visitor было изменено на MonoVisitor чтобы избежать конфликта имен и возможной путаницы. По книге visitor определяет множество перегруженных методов visit() . Каждый из них принимает аргумент различного типа для каждого Visitable . Следовательно, Visitor по определению не может быть универсальным. Это должно быть определено и поддержано на уровне проекта. MonoVisitor определяет только один метод. Безопасность типов гарантируется генериками. Один класс не может реализовать один и тот же интерфейс несколько раз, даже с разными общими параметрами. Это означает, что нам придется хранить несколько отдельных реализаций MonoVisitor даже если они сгруппированы в один класс.

Ссылка на функцию вместо посетителя

Поскольку MonoVisitor имеет только один бизнес-метод, мы должны создать реализацию для каждого класса модели. Однако мы не хотим создавать отдельные классы верхнего уровня, а предпочитаем группировать их в один класс. Этот новый visitor хранит Map между различными классами Visitable и реализациями java.util.Function и отправляет вызов метода visit() для конкретной реализации.

Итак, давайте посмотрим на MapVisitor.

01
02
03
04
05
06
07
08
09
10
11
12
13
public class MapVisitor<R> implements
        Function<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> {
    private final Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors;
 
    MapVisitor(Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors) {
        this.visitors = visitors;
    }
 
    @Override
    public MonoVisitor apply(Class clazz) {
        return visitors.get(clazz);
    }
}

MapVisitor

  • Реализует Function

    для того, чтобы получить конкретную реализацию (полные обобщения здесь опущены для удобочитаемости; взгляните на фрагмент кода для подробного определения)

  • Получает отображение между классом и реализацией в карте
  • Получает конкретную реализацию, подходящую для данного класса

MapVisitor имеет частный конструктор пакетов. Инициализация MapVisitor с помощью специального компоновщика очень проста и гибка:

1
2
3
4
MapVisitor<Void> printVisitor = MapVisitor.builder(Void.class)
        .with(Book.class, book -> {System.out.println(book.getTitle()); return null;})
        .with(Magazine.class, magazine -> {System.out.println(magazine.getName()); return null;})
        .build();

Использование MapVisitor аналогично одному из традиционных Visitor :

1
2
someBook.accept(printVisitor);
someMagazine.accept(printVisitor);

У нашего MapVisitor есть еще одно преимущество. Все методы, объявленные в интерфейсе традиционного посетителя, должны быть реализованы. Однако часто некоторые методы не могут быть реализованы.

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

Вот список животных: Duck, Penguin, Wale, Ostrich
И это список действий: Walk, Fly, Swim.

Мы решили иметь посетителя на действие: WalkVisitor, FlyVisitor, SwimVisitor . Утка может выполнять все три действия, Пингвин не может летать, Уэйл может только плавать и
Страус может только ходить. Итак, мы решили выбросить исключение, если пользователь пытается заставить Уэйла ходить или Ostrich летать. Но такое поведение не удобно для пользователя. Действительно, пользователь получит сообщение об ошибке только тогда, когда он нажмет кнопку действия. Возможно, мы бы предпочли отключить нерелевантные кнопки. MapVisitor позволяет это без дополнительной структуры данных или дублирования кода. Нам даже не нужно определять новый или расширять любой другой интерфейс. Вместо этого мы предпочитаем использовать стандартный интерфейс java.util.Predicate :

01
02
03
04
05
06
07
08
09
10
public class MapVisitor<R> implements
        Function<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>>,
        Predicate<Class<? extends Visitable>> {
    private final Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors;
    ...............
    @Override
    public boolean test(Class<? extends Visitable> clazz) {
        return visitors.containsKey(clazz);
    }
}

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

Полный исходный код примеров, используемых здесь, доступен на github .

Выводы

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

  1. Методы Visitor метода Visitor описанные здесь, могут возвращать значения и, следовательно, могут быть реализованы как чистые функции [3], которые помогают комбинировать шаблон Visitor с парадигмой функционального программирования.
  2. Разбиение монолитного интерфейса Visitor на отдельные блоки делает его более гибким и упрощает обслуживание кода.
  3. MapVisitor можно настроить с помощью компоновщика во время выполнения, поэтому он может изменять свое поведение в зависимости от информации, известной только во время выполнения и недоступной во время разработки.
  4. Посетители с различным типом возврата могут быть применены к одним и тем же классам Visitable .
  5. Реализация по умолчанию методов, выполняемых в интерфейсах, удаляет большой объем кода, обычно используемого для типичной реализации Visitor .

Рекомендации

  1. Википедия
  2. DZone
  3. Определение чистой функции .

Опубликовано на Java Code Geeks с разрешения Александра Радзина, партнера нашей программы JCG . Смотреть оригинальную статью здесь: Новая жизнь старого шаблона дизайна посетителя

Мнения, высказанные участниками Java Code Geeks, являются их собственными.