Посетитель (Visitor) — различия между версиями

Материал из Вики ИТ мехмата ЮФУ
Перейти к: навигация, поиск
(Реализация)
(Достоинства и недостатки)
 
(не показано 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. Исходный код узлов полиморфной структуры остается при этом неизменным.

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

Использование

Паттерн Посетитель следует использовать когда

  • в структуре присутствуют объекты многих классов, и необходимо выполнять над ними операции в зависимости от типа этих классов;
  • нежелательно засорять объекты структуры кодом операций, которые необходимо над ними выполнить
  • классы структуры меняются редко, а новые операции над элементами структуры добавляются часто.

Реализация

Диаграмма классов

VisitorCommon.png

Участники

  • 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.