Посетитель (Visitor) — различия между версиями
Admin (обсуждение | вклад) (→Реализация) |
Admin (обсуждение | вклад) (→Достоинства и недостатки) |
||
(не показано 17 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
[[Страница_курса_Паттерны_проектирования| К основной странице курса]] | [[Страница_курса_Паттерны_проектирования| К основной странице курса]] | ||
__NOTOC__ | __NOTOC__ | ||
− | |||
=== Назначение === | === Назначение === | ||
Позволяет выполнить над каждым объектом некоторой структуры операцию, не загрязняя код класса этого объекта и не используя определение типа для каждого объекта. | Позволяет выполнить над каждым объектом некоторой структуры операцию, не загрязняя код класса этого объекта и не используя определение типа для каждого объекта. | ||
=== Описание === | === Описание === | ||
+ | Когда есть некоторая полиморфная структура данных (связный список, дерево), и требуется ее обойти, выполнив для каждого узла структуры действие в зависимости от типа узла, то обычной практикой в ООП является создание виртуального метода для этого действия и переопределение его в потомках. Однако, данный подход имеет ряд недостатков. При наличии нескольких типов действий для каждого из них необходимо делать виртуальный метод, что захламляет интерфейс класса. | ||
+ | |||
+ | Одно из решений данной проблемы состоит в том чтобы вынести действие (точнее, группу полиморфных действий) в отдельный объект, называемый '''Посетителем''', и передавать его элементам полиморфной структуры по мере ее обхода. "Принимая" посетителя, элемент посылает ему запрос, соответствующий типу этого элемента. Кроме того, в этом запросе в качестве параметра передается сам элемент. | ||
+ | |||
+ | Таким образом, действия, производимые над объектами полиморфной структуры, являются внешними по отношению к самой этой структуре. Это позволяет, в частности, легко добавлять новые действия за счет реализации подклассов класса Visitor. Исходный код узлов полиморфной структуры остается при этом неизменным. | ||
+ | |||
+ | Паттерн Посетитель является одним из решений так называемой задачи '''двойной диспетчеризации'''. В этой задаче требуется выполнять определенное действие от двух параметров: типа объекта и типа операции. По-существу, возникает матрица действий с индексами, являющимися типами: первый индекс определяет тип объекта структуры, второй - тип действия. | ||
+ | |||
+ | ===Использование === | ||
+ | Паттерн Посетитель следует использовать когда | ||
+ | *в структуре присутствуют объекты многих классов, и необходимо выполнять над ними операции в зависимости от типа этих классов; | ||
+ | *нежелательно засорять объекты структуры кодом операций, которые необходимо над ними выполнить | ||
+ | *классы структуры меняются редко, а новые операции над элементами структуры добавляются часто. | ||
=== Реализация === | === Реализация === | ||
Строка 12: | Строка 24: | ||
==== Участники==== | ==== Участники==== | ||
− | * | + | * '''Visitor''' - посетитель |
+ | Объявляет метод Visit... для каждого класса в иерархии объектов с базовым классом Element. Например, для элемента ElementA объявляется метод VisitElementA с параметром типа ElementA. Для языков, в которых возможна перегрузка методов, для методов можно оставить одно имя Visit - они будут отличаться типом параметра: | ||
+ | |||
+ | <source lang="Csharp"> | ||
+ | void Visit(ElementA a); | ||
+ | void Visit(ElementB b); | ||
+ | </source> | ||
+ | |||
+ | * '''ConcreteVisitor''' - конкретный посетитель | ||
+ | Реализует все операции Visit, объявленные в классе Visitor. Каждая операция реализует действие для объекта определенного типа, производного от Element. | ||
+ | |||
+ | Класс ConcreteVisitor реализует также контекст для посетителя и сохраняет в нем состояние между вызовами Visit | ||
+ | |||
+ | * '''Node''' - элемент | ||
+ | Объявляет операцию Accept, принимающую посетителя в качестве аргумента | ||
+ | |||
+ | * '''ConcreteNode''' - конкретный элемент | ||
+ | Реализует операцию Accept. Для древовидной структуры, помимо вызова метода Visit, может вызывать Accept своих потомков, обеспечивая тем самым обход дерева. | ||
=== Пример === | === Пример === | ||
+ | Визитор CountIdVisitor считает количество идентификаторов в дереве выражения. | ||
+ | |||
+ | Здесь следует обратить внимание на то, что для нелистовых узлов в методе Visit требуется вызывать методы Accept своих узлов-потомков, реализуя обход всего дерева. | ||
+ | |||
+ | Следует обратить также внимание, что визитор при своей работе может пользоваться глобальными по отношению к методу Visit переменными - полями класса ConcreteVisitor. | ||
<source lang="Csharp"> | <source lang="Csharp"> | ||
− | </source> | + | using System; |
+ | |||
+ | abstract class Node | ||
+ | { | ||
+ | public abstract void Accept(Visitor v); | ||
+ | } | ||
+ | |||
+ | class NumNode: Node | ||
+ | { | ||
+ | public NumNode(int n) | ||
+ | { | ||
+ | Number = n; | ||
+ | } | ||
+ | public int Number { get; set; } | ||
+ | public override void Accept(Visitor v) | ||
+ | { | ||
+ | v.Visit(this); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | class IdNode: Node | ||
+ | { | ||
+ | public IdNode(string id) | ||
+ | { | ||
+ | Ident = id; | ||
+ | } | ||
+ | public string Ident { get; set; } | ||
+ | public override void Accept(Visitor v) | ||
+ | { | ||
+ | v.Visit(this); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | class BinOpNode: Node | ||
+ | { | ||
+ | public BinOpNode(Node left, Node right, char op) | ||
+ | { | ||
+ | Left = left; Right = right; | ||
+ | Op = op; | ||
+ | } | ||
+ | public Node Left { get; set; } | ||
+ | public Node Right { get; set; } | ||
+ | public char Op { get; set; } | ||
+ | public override void Accept(Visitor v) | ||
+ | { | ||
+ | Left.Accept(v); | ||
+ | Right.Accept(v); | ||
+ | v.Visit(this); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | interface Visitor | ||
+ | { | ||
+ | void Visit(IdNode id); | ||
+ | void Visit(NumNode num); | ||
+ | void Visit(BinOpNode binop); | ||
+ | } | ||
+ | |||
+ | class CountIdVisitor: Visitor | ||
+ | { | ||
+ | public int Count { get; set; } | ||
+ | public void Visit(IdNode id) | ||
+ | { | ||
+ | Count += 1; | ||
+ | } | ||
+ | public void Visit(NumNode num) | ||
+ | { | ||
+ | } | ||
+ | public void Visit(BinOpNode binop) | ||
+ | { | ||
+ | } | ||
+ | } | ||
+ | |||
+ | class My | ||
+ | { | ||
+ | static void Main() | ||
+ | { | ||
+ | var id1 = new IdNode("a1"); | ||
+ | var id2 = new IdNode("a2"); | ||
+ | var num = new NumNode(25); | ||
+ | var expr = new BinOpNode(id1,num,'+'); | ||
+ | var expr1 = new BinOpNode(expr,id2,'*'); | ||
+ | var v = new CountIdVisitor(); | ||
+ | expr1.Accept(v); | ||
+ | Console.WriteLine(v.Count); | ||
+ | } | ||
+ | }</source> | ||
=== Достоинства и недостатки === | === Достоинства и недостатки === | ||
− | * | + | * Упрощает добавление новых операций. Не требует менять классы узлов. |
− | + | * Объединяет родственные операции в одном классе посетителя. | |
− | + | * Недостаток: добавление новых элементов Node затруднено - в каждом посетителе необходимо добавить соответствующий метод. Поэтому при решении вопроса о том, следует ли использовать паттерн Посетитель, необходимо проанализировать, что будет добавляться чаще - алгоритмы для обработки всех элементов или новые типы элементов. | |
− | * | + | * Посетитель, в отличие от итератора, может обходить узлы, не связанные родственными соотношениями. |
+ | * Посетители могут аккумулировать информацию о состоянии при посещении объектов структуры. | ||
+ | * Недостаток: для реализации паттерна Посетитель приходится предоставлять открытые операции для доступа к внутреннему состоянию элементов, что является нарушением инкапсуляции. | ||
+ | * Недостаток: необходимо переопределять все методы Visit, даже не задействованные в данном визиторе. Здесь можно сделать прослойку в виде абстрактного класса AbstractVisitor, предлагающего пустую реализацию всех медтодов Visit. |
Текущая версия на 18:27, 31 июля 2014
Назначение
Позволяет выполнить над каждым объектом некоторой структуры операцию, не загрязняя код класса этого объекта и не используя определение типа для каждого объекта.
Описание
Когда есть некоторая полиморфная структура данных (связный список, дерево), и требуется ее обойти, выполнив для каждого узла структуры действие в зависимости от типа узла, то обычной практикой в ООП является создание виртуального метода для этого действия и переопределение его в потомках. Однако, данный подход имеет ряд недостатков. При наличии нескольких типов действий для каждого из них необходимо делать виртуальный метод, что захламляет интерфейс класса.
Одно из решений данной проблемы состоит в том чтобы вынести действие (точнее, группу полиморфных действий) в отдельный объект, называемый Посетителем, и передавать его элементам полиморфной структуры по мере ее обхода. "Принимая" посетителя, элемент посылает ему запрос, соответствующий типу этого элемента. Кроме того, в этом запросе в качестве параметра передается сам элемент.
Таким образом, действия, производимые над объектами полиморфной структуры, являются внешними по отношению к самой этой структуре. Это позволяет, в частности, легко добавлять новые действия за счет реализации подклассов класса Visitor. Исходный код узлов полиморфной структуры остается при этом неизменным.
Паттерн Посетитель является одним из решений так называемой задачи двойной диспетчеризации. В этой задаче требуется выполнять определенное действие от двух параметров: типа объекта и типа операции. По-существу, возникает матрица действий с индексами, являющимися типами: первый индекс определяет тип объекта структуры, второй - тип действия.
Использование
Паттерн Посетитель следует использовать когда
- в структуре присутствуют объекты многих классов, и необходимо выполнять над ними операции в зависимости от типа этих классов;
- нежелательно засорять объекты структуры кодом операций, которые необходимо над ними выполнить
- классы структуры меняются редко, а новые операции над элементами структуры добавляются часто.
Реализация
Диаграмма классов
Участники
- Visitor - посетитель
Объявляет метод Visit... для каждого класса в иерархии объектов с базовым классом Element. Например, для элемента ElementA объявляется метод VisitElementA с параметром типа ElementA. Для языков, в которых возможна перегрузка методов, для методов можно оставить одно имя Visit - они будут отличаться типом параметра:
void Visit(ElementA a);
void Visit(ElementB b);
- ConcreteVisitor - конкретный посетитель
Реализует все операции Visit, объявленные в классе Visitor. Каждая операция реализует действие для объекта определенного типа, производного от Element.
Класс ConcreteVisitor реализует также контекст для посетителя и сохраняет в нем состояние между вызовами Visit
- Node - элемент
Объявляет операцию Accept, принимающую посетителя в качестве аргумента
- ConcreteNode - конкретный элемент
Реализует операцию Accept. Для древовидной структуры, помимо вызова метода Visit, может вызывать Accept своих потомков, обеспечивая тем самым обход дерева.
Пример
Визитор CountIdVisitor считает количество идентификаторов в дереве выражения.
Здесь следует обратить внимание на то, что для нелистовых узлов в методе Visit требуется вызывать методы Accept своих узлов-потомков, реализуя обход всего дерева.
Следует обратить также внимание, что визитор при своей работе может пользоваться глобальными по отношению к методу Visit переменными - полями класса ConcreteVisitor.
using System;
abstract class Node
{
public abstract void Accept(Visitor v);
}
class NumNode: Node
{
public NumNode(int n)
{
Number = n;
}
public int Number { get; set; }
public override void Accept(Visitor v)
{
v.Visit(this);
}
}
class IdNode: Node
{
public IdNode(string id)
{
Ident = id;
}
public string Ident { get; set; }
public override void Accept(Visitor v)
{
v.Visit(this);
}
}
class BinOpNode: Node
{
public BinOpNode(Node left, Node right, char op)
{
Left = left; Right = right;
Op = op;
}
public Node Left { get; set; }
public Node Right { get; set; }
public char Op { get; set; }
public override void Accept(Visitor v)
{
Left.Accept(v);
Right.Accept(v);
v.Visit(this);
}
}
interface Visitor
{
void Visit(IdNode id);
void Visit(NumNode num);
void Visit(BinOpNode binop);
}
class CountIdVisitor: Visitor
{
public int Count { get; set; }
public void Visit(IdNode id)
{
Count += 1;
}
public void Visit(NumNode num)
{
}
public void Visit(BinOpNode binop)
{
}
}
class My
{
static void Main()
{
var id1 = new IdNode("a1");
var id2 = new IdNode("a2");
var num = new NumNode(25);
var expr = new BinOpNode(id1,num,'+');
var expr1 = new BinOpNode(expr,id2,'*');
var v = new CountIdVisitor();
expr1.Accept(v);
Console.WriteLine(v.Count);
}
}
Достоинства и недостатки
- Упрощает добавление новых операций. Не требует менять классы узлов.
- Объединяет родственные операции в одном классе посетителя.
- Недостаток: добавление новых элементов Node затруднено - в каждом посетителе необходимо добавить соответствующий метод. Поэтому при решении вопроса о том, следует ли использовать паттерн Посетитель, необходимо проанализировать, что будет добавляться чаще - алгоритмы для обработки всех элементов или новые типы элементов.
- Посетитель, в отличие от итератора, может обходить узлы, не связанные родственными соотношениями.
- Посетители могут аккумулировать информацию о состоянии при посещении объектов структуры.
- Недостаток: для реализации паттерна Посетитель приходится предоставлять открытые операции для доступа к внутреннему состоянию элементов, что является нарушением инкапсуляции.
- Недостаток: необходимо переопределять все методы Visit, даже не задействованные в данном визиторе. Здесь можно сделать прослойку в виде абстрактного класса AbstractVisitor, предлагающего пустую реализацию всех медтодов Visit.