Книга доктора Яна Хонга «JAVA и шаблоны» начинается с описания шаблона «Посетитель»:
Паттерн посетителя — это шаблон поведения объектов. Целью шаблона посетителя является инкапсуляция некоторых операций, которые применяются к определенным элементам структуры данных. Если эти операции необходимо изменить, структура данных, принимающая эту операцию, может остаться неизменной.
Понятие о рассылке
Тип, при котором объявляется переменная, называется статическим типом переменной (статический тип), а некоторые люди называют статический тип видимым типом (видимый тип), а реальный тип объекта, на который ссылается переменная, также называется типом; фактический тип переменной (Actual Type). например:
Скопируйте код кода следующим образом:
Список список = ноль;
список = новый ArrayList();
Объявлен список переменных, его статический тип (также называемый очевидным типом) — List, а его фактический тип — ArrayList.
Выбор методов в зависимости от типа объекта диспетчеризации делится на два типа: статическую отправку и динамическую отправку.
Статическая отправка происходит во время компиляции, а отправка происходит на основе информации о статическом типе. Статическая диспетчеризация нам не незнакома. Перегрузка методов — это статическая диспетчеризация.
Динамическая отправка происходит во время выполнения, и динамическая отправка динамически заменяет метод.
статическая отправка
Java поддерживает статическую отправку посредством перегрузки методов. Используя в качестве примера историю о Мози, едущем на лошади, Мози мог ездить на белой или черной лошади. Диаграмма классов Мози и белой лошади, черной лошади и лошади выглядит следующим образом:
В этой системе Mozi представлен классом Mozi. Код выглядит следующим образом:
общественный класс Мози {
общественная недействительная поездка (лошадь ч) {
System.out.println("верховая езда");
}
общественная недействительная поездка (WhiteHorse WH) {
System.out.println("Верхом на белом коне");
}
общественная недействительная поездка (BlackHorse bh) {
System.out.println("Оседлать на темной лошадке");
}
public static void main(String[] args) {
Лошадь WH = новый WhiteHorse();
Лошадь bh = новый BlackHorse();
Мози Мози = новый Мози ();
mozi.ride(wh);
mozi.ride(бх);
}
}
Очевидно, что метод ride() класса Mozi перегружен тремя методами. Эти три метода принимают параметры типов Horse, WhiteHorse, BlackHorse и других соответственно.
Итак, какие результаты программа выведет при запуске? В результате программа печатает одни и те же две строки «верхом». Другими словами, Мози обнаружил, что все, на чем он ездил, — это лошади.
Почему? Два вызова метода ride() передают разные параметры, а именно wh и bh. Хотя они имеют разные реальные типы, их статические типы одинаковы — это типы Horse.
Отправка перегруженных методов основана на статических типах, и этот процесс отправки завершается во время компиляции.
динамическая отправка
Java поддерживает динамическую отправку посредством переопределения метода. На примере истории о лошади, которая ест траву, код выглядит следующим образом:
Скопируйте код кода следующим образом:
общественный класс Лошадь {
общественная недействительность есть () {
System.out.println("Лошадь ест траву");
}
}
Скопируйте код кода следующим образом:
публичный класс BlackHorse расширяет Horse {
@Override
общественный недействительный съесть () {
System.out.println("Темная лошадка ест траву");
}
}
Скопируйте код кода следующим образом:
Клиент публичного класса {
public static void main(String[] args) {
Лошадь h = новый BlackHorse();
нагревать();
}
}
Статический тип переменной h — Horse, а реальный тип — BlackHorse. Если метод eat() в последней строке выше вызывает метод eat() класса BlackHorse, то выше будет напечатано «Черная лошадь ест траву», наоборот, если метод eat(), указанный выше, вызывает метод eat(; ) метода класса Horse, то выводится «лошадь ест траву».
Следовательно, суть проблемы в том, что компилятор Java не всегда знает, какой код будет выполнен во время компиляции, поскольку компилятор знает только статический тип объекта, но не знает реальный тип объекта и метода; вызов основан на реальных типах объекта, а не на статических типах. Таким образом, метод eat() в последней строке выше вызывает метод eat() класса BlackHorse и печатает «черная лошадь ест траву».
тип отправки
Объект, которому принадлежит метод, называется получателем метода. Получатель метода, а параметры метода вместе называются объемом метода. Например, код копирования класса Test в примере ниже выглядит следующим образом:
тест публичного класса {
общественная недействительная печать (String str) {
System.out.println(str);
}
}
В приведенном выше классе метод print() принадлежит объекту Test, поэтому его получателем также является объект Test. Метод print() имеет параметр str, его тип — String.
В зависимости от того, на скольких типах диспетчеризации величин может базироваться объектно-ориентированные языки, их можно разделить на языки единой диспетчеризации (Uni-Dispatch) и многодиспетчерские языки (Multi-Dispatch). Языки с одной диспетчеризацией выбирают методы на основе типа одного экземпляра, тогда как языки с несколькими диспетчеризациями выбирают методы на основе типа более чем одного экземпляра.
И C++, и Java являются языками с единой диспетчеризацией, а примеры языков с несколькими диспетчеризациями включают CLOS и Cecil. В соответствии с этим различием Java является динамическим языком с одной диспетчеризацией, поскольку при динамической отправке этого языка учитывается только тип получателя метода, и это статический язык с несколькими диспетчеризациями, поскольку этот язык отправляет перегруженные методы. учитываются тип приемника метода и типы всех параметров метода.
В языке, поддерживающем динамическую однократную отправку, есть два условия, определяющие, какую операцию вызовет запрос: первое — это имя запроса и реальный тип получателя. Единая отправка ограничивает процесс выбора метода, поэтому можно рассматривать только один экземпляр, которым обычно является получатель метода. В языке Java, если операция выполняется над объектом неизвестного типа, проверка реального типа объекта происходит только один раз. Это характеристика динамической одиночной отправки.
двойная отправка
Метод решает выполнить другой код в зависимости от типов двух переменных. Это «двойная диспетчеризация». Язык Java не поддерживает динамическую множественную отправку, а это означает, что Java не поддерживает динамическую двойную отправку. Но с помощью шаблонов проектирования динамическую двойную диспетчеризацию можно реализовать и на языке Java.
В Java две диспетчеризации могут быть достигнуты посредством двух вызовов методов. Диаграмма классов выглядит следующим образом:
На картинке два объекта: тот, что слева, называется Запад, а тот, что справа, называется Восток. Теперь объект West сначала вызывает метод goeast() объекта East, передавая ему себя. Когда вызывается объект East, он сразу же узнает, кто вызывающий объект, на основе переданных параметров, поэтому по очереди вызывается метод goWest() объекта «вызывающий». Посредством двух вызовов управление программой передается поочередно двум объектам. Схема последовательности действий выглядит следующим образом:
Таким образом, происходит два вызова метода. Программное управление передается между двумя объектами. Сначала оно передается от объекта West к объекту East, а затем передается обратно к объекту West.
Но простой возврат мяча не решает проблему двойной раздачи. Ключевым моментом является то, как использовать эти два вызова и функцию динамической одиночной диспетчеризации языка Java для запуска двух одиночных отправок во время этого процесса передачи.
Динамическая одиночная диспетчеризация в языке Java происходит, когда подкласс переопределяет метод родительского класса. Другими словами, и Запад, и Восток должны быть помещены в свою собственную иерархию типов, как показано ниже:
исходный код
Код копирования класса West выглядит следующим образом:
публичный абстрактный класс West {
общественная абстрактная пустота goWest1 (Subeast1 восток);
общественная абстрактная пустота goWest2 (Subeast2 восток);
}
Код кода копии класса SubWest1 выглядит следующим образом:
публичный класс SubWest1 расширяет West{
@Override
общественная недействительность goWest1 (Subeast1 восток) {
System.out.println("SubWest1 + " +east.myName1());
}
@Override
общественная недействительность goWest2 (Subeast2 восток) {
System.out.println("SubWest1 + " +east.myName2());
}
}
Субвест Класс 2
Скопируйте код кода следующим образом:
публичный класс SubWest2 расширяет West{
@Override
общественная недействительность goWest1 (Subeast1 восток) {
System.out.println("SubWest2 + " +east.myName1());
}
@Override
общественная недействительность goWest2 (Subeast2 восток) {
System.out.println("SubWest2 + " +east.myName2());
}
}
Код копирования восточного класса выглядит следующим образом:
публичный абстрактный класс Восток {
общественная абстрактная пустота go East (Запад-Запад);
}
Код копии кода класса Subeast1 следующий:
общественный класс Subeast1 расширяет East {
@Override
общественная недействительность go East (Запад-Запад) {
запад.goWest1(это);
}
публичная строка myName1(){
вернуть «СубВосток1»;
}
}
Код копии кода класса Subeast2 выглядит следующим образом:
публичный класс Subeast2 расширяет East{
@Override
общественная недействительность go East (Запад-Запад) {
запад.goWest2(это);
}
публичная строка myName2(){
вернуть «СубВосток2»;
}
}
Код копирования клиентского класса выглядит следующим образом:
Клиент публичного класса {
public static void main(String[] args) {
//комбинация 1
Восток-восток = новый Subeast1();
Запад-запад = новый SubWest1();
восток.goВосточный (запад);
//комбинация 2
восток = новый Subeast1();
запад = новый SubWest2 ();
восток.goВосточный (запад);
}
}
Результаты работы следующие: Скопируйте код. Код выглядит следующим образом:
СубЗапад1 + СубВосток1
СубЗапад2 + СубВосток1
Когда система работает, сначала создаются объекты SubWest1 и Subeast1, а затем клиент вызывает метод goeast() Subeast1 и передает объект SubWest1. Поскольку объект Subeast1 переопределяет метод goeast() своего суперкласса East, в этот момент происходит динамическая одиночная диспетчеризация. Когда объект Subeast1 получает вызов, он получает объект SubWest1 из параметра, поэтому он немедленно вызывает метод goWest1() этого объекта и передает себя. Поскольку объект Subeast1 имеет право выбирать, какой объект вызывать, в это время выполняется еще одна динамическая отправка метода.
В это время объект SubWest1 получил объект Subeast1. Вызвав метод myName1() этого объекта, вы можете распечатать свое имя и имя объекта Subeast. Диаграмма последовательности выглядит следующим образом:
Поскольку одно из этих двух имен происходит из восточной иерархии, а другое — из западной иерархии, их сочетание определяется динамически. Это механизм реализации динамической двойной диспетчеризации.
Структура шаблона посетителя
Шаблон посетителя подходит для систем с относительно неопределенными структурами данных. Он отделяет связь между структурой данных и операциями, которые действуют на структуру, позволяя набору операций развиваться относительно свободно. Упрощенная схема шаблона посетителя показана ниже:
Каждый узел структуры данных может принимать вызов от посетителя. Этот узел передает объект узла объекту посетителя, а объект посетителя, в свою очередь, выполняет операции объекта узла. Этот процесс называется «двойной отправкой». Узел вызывает посетителя, передавая себя, и посетитель выполняет алгоритм против этого узла. Схематическая диаграмма классов для шаблона Посетитель показана ниже:
В режиме посетителя задействованы следующие роли:
● Роль абстрактного посетителя (посетителя) : объявляет одну или несколько операций метода для формирования интерфейса, который должны реализовать все конкретные роли посетителя.
● Роль конкретного посетителя (ConcreteVisitor) : реализует интерфейс, объявленный абстрактным посетителем, то есть каждую операцию доступа, объявленную абстрактным посетителем.
● Роль абстрактного узла (Node) : объявляет операцию принятия и принимает объект посетителя в качестве параметра.
● Роль ConcreteNode : реализует операцию принятия, указанную абстрактным узлом.
● Роль объекта структуры (ObjectStructure) : имеет следующие обязанности, при необходимости может перемещаться по всем элементам структуры, предоставлять высокоуровневый интерфейс, чтобы объекты посетителей могли получить доступ к каждому элементу при необходимости, могут быть спроектированы как составной объект или; Коллекция, например List или Set.
исходный код
Как видите, роль абстрактного посетителя подготавливает операцию доступа для каждого конкретного узла. Поскольку имеется два узла, существуют две соответствующие операции доступа.
Скопируйте код кода следующим образом:
Публичный интерфейс Посетитель {
/**
* Соответствует операции доступа NodeA.
*/
публичное недействительное посещение (узел NodeA);
/**
* Соответствует операции доступа NodeB.
*/
публичное недействительное посещение (узел NodeB);
}
Конкретный код копии класса посетителя A выглядит следующим образом:
публичный класс VisitorA реализует Visitor {
/**
* Соответствует операции доступа NodeA.
*/
@Override
общественное недействительное посещение (узел NodeA) {
System.out.println(node.operationA());
}
/**
* Соответствует операции доступа NodeB.
*/
@Override
общественное недействительное посещение (узел NodeB) {
System.out.println(node.operationB());
}
}
Код копии класса конкретного посетителя VisitorB выглядит следующим образом:
публичный класс VisitorB реализует Visitor {
/**
* Соответствует операции доступа NodeA.
*/
@Override
общественное недействительное посещение (узел NodeA) {
System.out.println(node.operationA());
}
/**
* Соответствует операции доступа NodeB.
*/
@Override
общественное недействительное посещение (узел NodeB) {
System.out.println(node.operationB());
}
}
Код кода копирования класса абстрактного узла выглядит следующим образом:
публичный абстрактный класс Node {
/**
* Принять операцию
*/
public Abstract void Accept (Посетитель);
}
Конкретный класс узла NodeA
Скопируйте код кода следующим образом:
публичный класс NodeA расширяет Node{
/**
* Принять операцию
*/
@Override
public void Accept (Посетитель) {
посетитель.посещение(это);
}
/**
*Метод, специфичный для NodeA.
*/
публичная строковая операцияA(){
вернуть «УзелА»;
}
}
Конкретный класс узла NodeB
Скопируйте код кода следующим образом:
публичный класс NodeB расширяет Node{
/**
*Принять метод
*/
@Override
public void Accept (Посетитель) {
посетитель.посещение(это);
}
/**
*Методы, специфичные для NodeB.
*/
публичная строковая операцияB(){
вернуть «NodeB»;
}
}
Класс роли структурного объекта. Эта роль структурного объекта содержит коллекцию и предоставляет метод add() внешнему миру в качестве операции управления коллекцией. Вызвав этот метод, можно динамически добавить новый узел.
Скопируйте код кода следующим образом:
общественный класс ObjectStructure {
частные узлы List<Node> = новый ArrayList<Node>();
/**
* Выполнить операцию метода
*/
публичное недействительное действие (посетитель) {
for(Узел-узел: узлы)
{
node.accept(посетитель);
}
}
/**
* Добавить новый элемент
*/
public void add(Node node){
nodes.add(узел);
}
}
Код копирования клиентского класса выглядит следующим образом:
Клиент публичного класса {
public static void main(String[] args) {
//Создаем объект структуры
ObjectStructure os = новая ObjectStructure();
//Добавляем узел в структуру
os.add(новый NodeA());
//Добавляем узел в структуру
os.add(новый NodeB());
//Создаем посетителя
Посетитель посетитель = новый ПосетительA();
os.action(посетитель);
}
}
Хотя сложная древовидная структура объектов с несколькими узлами ветвей не показана в этой схематической реализации, в реальных системах шаблон посетителя обычно используется для обработки сложных древовидных структур объектов, а шаблон посетителя может использоваться для решения проблем древовидной структуры, охватывающих несколько иерархий. . Именно здесь структура посетителей имеет такое большое значение.
Схема процесса подготовки
Сначала этот иллюстративный клиент создает объект структуры, а затем передает новый объект NodeA и новый объект NodeB.
Во-вторых, клиент создает объект VisitorA и передает этот объект объекту структуры.
Затем клиент вызывает метод управления агрегацией объектов структуры, чтобы добавить узлы NodeA и NodeB к объекту структуры.
Наконец, клиент вызывает метод действия action() объекта структуры, чтобы начать процесс доступа.
Схема последовательности процесса доступа
Объект структуры будет проходить через все узлы в сохраненной им коллекции, которыми в этой системе являются узлы NodeA и NodeB. Сначала будет осуществлен доступ к NodeA. Этот доступ состоит из следующих операций:
(1) Вызывается метод Accept() объекта NodeA и передается сам объект VisitorA;
(2) Объект NodeA, в свою очередь, вызывает метод доступа объекта VisitorA и передает сам объект NodeA;
(3) Объект VisitorA вызывает уникальный метод OperationA() объекта NodeA.
Таким образом, процесс двойной диспетчеризации завершен. Затем будет осуществлен доступ к узлу B. Процесс доступа аналогичен процессу доступа к узлу A, который здесь не описывается.
Преимущества шаблона «Посетитель»
● Хорошая расширяемость позволяет добавлять новые функции к элементам структуры объекта без изменения элементов в структуре объекта.
● Хорошая возможность повторного использования позволяет посетителям определять функции, общие для всей структуры объекта, тем самым повышая степень возможности повторного использования.
● Разделение нерелевантного поведения. Вы можете использовать посетителей для разделения нерелевантного поведения и инкапсулировать связанное поведение вместе, чтобы сформировать посетителя, чтобы функция каждого посетителя была относительно единой.
Недостатки шаблона «Посетитель»
● Сложно изменить структуру объекта. Это не подходит для ситуаций, когда классы в структуре объекта часто меняются. Поскольку структура объекта меняется, интерфейс посетителя и его реализация должны меняться соответствующим образом, что является слишком дорогостоящим.
● Нарушение шаблона «Посетитель инкапсуляции» обычно требует, чтобы структура объекта открывала внутренние данные посетителям и ObjectStructrue, что нарушает инкапсуляцию объекта.