Відвідувач (шаблон проєктування)
Відвідувач (Visitor) — шаблон проєктування, який дозволяє відділити певний алгоритм від елементів, на яких алгоритм має бути виконаний, таким чином можливо легко додати або ж змінити алгоритм без зміни елементів системи. Практичним результатом є можливість додавання нових операцій в існуючі структури об'єкта без зміни цих структур.
Відвідувач дозволяє додавати нові віртуальні функції в родинні класи без зміни самих класів, натомість, один відвідувач створює клас, який реалізує всі відповідні спеціалізації віртуальної функції. Відвідувач приймає посилання на елемент й реалізується шляхом подвійної диспетчеризації.
Проблема, яку вирішує
Над кожним об'єктом деякої структури виконується одна або більше операцій. Визначити нову операцію, не змінюючи класи об'єктів.
Випадки застосування
Шаблон Відвідувач використовується, коли:
- існує багато об'єктів різних класів з різними інтерфейсами, і потрібно виконати операцію над цими об'єктами, залежно від їх типу.
- потрібно виконувати над об'єктами багато не пов'язаних між собою операцій, але не хочеться "забруднювати" цими операціями класи об'єктів. При цьому, якщо ці об'єкти використовуються у декількох проєктах, з'являється можливість включати операції лише в ті проєкти, де вони необхідні.
- класи, які визначають структуру даних, змінюються рідко, але є потреба часто визначати нові операції над цією структурою. Якщо ж класи, які визначають структуру даних, змінюються часто, краще визначити операції в цих класах, адже при зміні інтерфейсів у класі даних необхідно змінювати і класи, які реалізують операції над цією структурою.
- треба визначити операцію над деякою структурою, не змінюючи клас цієї структури.
Опис шаблону
Основним призначенням шаблону Відвідувач є введення абстрактної функціональності для сукупної ієрархічної структури об'єктів «елемент», а саме, шаблон Відвідувач дає змогу, не змінюючи класи елементів, додавати в них нові операції. Для цього вся обробна функціональність переноситься з самих класів елементів в ієрархію спадкування Відвідувача.
Шаблон відвідувач дає змогу легко додавати нові операції — потрібно просто додати новий похідний від відвідувача клас. Однак, шаблон Відвідувач слід використовувати тільки в тому випадку, якщо підкласи елементів сукупної ієрархічної структури залишаються стабільними (незмінними). В іншому випадку, потрібно докласти значних зусиль на оновлення всієї ієрархії.
Іноді наводяться заперечення з приводу використання шаблону Відвідувач, оскільки він розділяє дані та алгоритми, що суперечить концепції об'єктно-орієнтованого програмування. Однак успішний досвід застосування STL, де поділ даних і алгоритмів покладено в основу, доводить можливість використання Відвідувача.
Реалізація
Особливості шаблону
- Сукупна структура об'єктів елементу може визначатися за допомогою патерну Компонувальник (Composite).
- Для обходу може використовуватися Ітератор (Iterator).
- Шаблон Відвідувач демонструє класичний прийом відновлення інформації про втрачені типи, не вдаючись до понижуючого приведення типів (динамічне приведення).
- Використовує механізм багаторазової диспетчеризації.
Частини шаблону
- Відвідувач (зазвичай, абстрактний клас чи інтерфейс)
- визначає дію над кожним класом конкретних елементів. Ім'я та сигнатура операції мають визначати конкретний клас даних, елемент якого треба відвідати. Це дає можливість відвідувачу доступатися до елементів через інтерфейс конкретного класу.
- Конкретний відвідувач (конкретний клас, що наслідує Відвідувач)
- реалізує чи перевизначає операції, визначені в базовому класі. Містить алгоритми, які виконуватимуться над об'єктами відповідного класу. Також даний клас утримує локальний стан алгоритмів (цей стан, зазвичай, утримує проміжні результати під час обходу структури, та ін.).
- Елемент (зазвичай, абстрактний клас чи інтерфейс)
- визначає операцію, яка приймає об'єкт відвідувача як аргумент.
- Конкретний елемент (конкретний клас, що наслідує Елемент)
- визначає операцію, що приймає об'єкт відвідувача як аргумент.
- Структура елементів (клас, що реалізує структуру елементів)
- може перераховувати елементи, які містить.
- надає високорівневий інтерфейс, що дозволяє відвідувачу виконувати елементи.
- може бути Компонувальником чи колекцією (як список або черга).
Взаємодія
- Клієнт, що використовує Відвідувача, має створити об'єкт Конкретного відвідувача та обійти потрібні елементи, відвідуючи кожен з них цим об'єктом.
- Коли елемент відвідується, він викликає операцію Відвідувача, що відповідає класу елементу. Елемент передає себе як аргумент в цю операцію щоб дозволити Відвідувачу доступ до його стану, якщо це необхідно.
Переваги
- спрощується додавання нових операцій;
- об'єднання споріднених операції в класі Visitor;
- клас Visitor може запам’ятовувати в собі якийсь стан під час обходу контейнера.
Недоліки
- важке додавання нових класів, оскільки потрібно оновлювати ієрархію відвідувачів.
Приклади реалізації
C++
#include <iostream>
#include <string>
using namespace std;
struct Foo;
struct Bar;
struct Baz;
// Абстракція відвідувача
struct Visitor
{
virtual void visit(Foo &ref) = 0;
virtual void visit(Bar &ref) = 0;
virtual void visit(Baz &ref) = 0;
virtual ~Visitor() = default;
};
// Абстракція елемента
struct Element
{
virtual void accept(Visitor &v) = 0;
virtual ~Element() = default;
};
// Конкретні елементи
struct Foo : public Element
{
void accept(Visitor &v) override
{
v.visit(*this);
}
};
struct Bar : public Element
{
void accept(Visitor &v) override
{
v.visit(*this);
}
};
struct Baz : public Element
{
void accept(Visitor &v) override
{
v.visit(*this);
}
};
// Конкретний відвідувач
struct GetType : public Visitor
{
string value;
void visit(Foo &ref) override
{
value = "Foo";
}
void visit(Bar &ref) override
{
value = "Bar";
}
void visit(Baz &ref) override
{
value = "Baz";
}
};
void main()
{
Foo foo;
Bar bar;
Baz baz;
Element *elements[] = { &foo, &bar, &baz };
GetType visitor;
for (auto elem : elements)
{
elem->accept(visitor);
cout << visitor.value << endl;
}
}
С#
namespace VisitorPattern
{
// Абстракція відвідувача
interface IGeometryVisitor
{
double Visit(Square square);
double Visit(Circle circle);
}
// Абстракція елемента
interface IGeometryElement
{
double Accept(IGeometryVisitor visitor);
}
// Конкретні елементи
class Square : IGeometryElement
{
public double Side { get; internal set; }
public double Accept(IGeometryVisitor visitor)
{
return visitor.Visit(this);
}
}
class Circle : IGeometryElement
{
public double Radius { get; internal set; }
public double Accept(IGeometryVisitor visitor)
{
return visitor.Visit(this);
}
}
// Конкретні відвідувачі
class GetAreaVisitor : IGeometryVisitor
{
public double Visit(Square square)
{
return square.Side * square.Side;
}
public double Visit(Circle circle)
{
return Math.PI * circle.Radius * circle.Radius;
}
}
class GetPerimeterVisitor : IGeometryVisitor
{
public double Visit(Square square)
{
return 4 * square.Side;
}
public double Visit(Circle circle)
{
return 2 * Math.PI * circle.Radius;
}
}
class Program
{
static void Main(string[] args)
{
IGeometryElement[] elements = new IGeometryElement[]
{
new Square
{
Side = 20,
},
new Circle
{
Radius = 15,
}
};
IGeometryVisitor geometryVisitor = new GetAreaVisitor();
foreach (var element in elements)
{
var area = element.Accept(geometryVisitor);
System.Console.WriteLine($"Area of {element.GetType().Name} equals {area}");
}
}
}
}
Java
public class Program {
public static void main (String [] args) {
Point p = new Point2d(1, 2);
Visitor v = new ChebyshevMetric();
double metric = p.accept(v);
System.out.println(metric);
}
}
interface Visitor {
public double visit (Point2d p);
public double visit (Point3d p);
}
abstract class Point {
public abstract double accept (Visitor v);
}
class Point2d extends Point {
public Point2d (double x, double y) {
this.x = x;
this.y = y;
}
public double accept (Visitor v) {
return v.visit(this);
}
private double x;
public double getX () { return x; }
private double y;
public double getY () { return y; }
}
class Point3d extends Point {
public Point3d (double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public double accept (Visitor v) {
return v.visit(this);
}
private double x;
public double getX () { return x; }
private double y;
public double getY () { return y; }
private double z;
public double getZ () { return z; }
}
// конкретні операції
class EuclidMetric implements Visitor {
public double visit (Point2d p) {
double x2 = p.getX() * p.getX();
double y2 = p.getY() * p.getY();
return Math.sqrt(x2 + y2);
}
public double visit (Point3d p) {
double x2 = p.getX() * p.getX();
double y2 = p.getY() * p.getY();
double z2 = p.getZ() * p.getZ();
return Math.sqrt(x2 + y2 + z2);
}
}
class ChebyshevMetric implements Visitor {
public double visit (Point2d p) {
double ax = Math.abs(p.getX());
double ay = Math.abs(p.getY());
return Math.max(ax, ay);
}
public double visit (Point3d p) {
double ax = Math.abs(p.getX());
double ay = Math.abs(p.getY());
double az = Math.abs(p.getZ());
return Math.max(Math.max(ax, ay), az);
}
}
class ManhattanMetric implements Visitor {
public double visit (Point2d p) {
double ax = Math.abs(p.getX());
double ay = Math.abs(p.getY());
return ax + ay;
}
public double visit (Point3d p) {
double ax = Math.abs(p.getX());
double ay = Math.abs(p.getY());
double az = Math.abs(p.getZ());
return ax + ay + az;
}
}