Сообщений 35    Оценка 120        Оценить  
Система Orphus

Паттерн Посетитель

Использование паттерна Посетитель в языке C#

Автор: Андрей Корявченко
The RSDN Group

Источник: RSDN Magazine #3-2006
Опубликовано: 06.12.2006
Исправлено: 10.12.2016
Версия текста: 1.0
Для чего это нужно
Паттерн Посетитель
Использование перегрузки методов
Передача параметров
Реализация посетителя при помощи функторов

Для чего это нужно

Очень часто в программах встречаются сложные структуры, представляющие собой дерево или граф, состоящий из разнотипных узлов. И, конечно же, при этом имеется необходимость обрабатывать этот граф. Самое очевидное решение - добавить в базовый класс виртуальный метод, который перекрыть в наследниках для выполнения нужного действия и осуществления дальнейшей навигации по дереву.

Пример1. Навигация по дереву при помощи специального виртуального метода

Однако у кода в этом примере есть серьезный недостаток: в нем структура данных оказывается увязанной с обрабатывающими ее алгоритмами. Если нам понадобится алгоритм, отличный от реализованного, то придется добавлять еще один виртуальный метод. Еще хуже, если классы, составляющие дерево, содержатся в недоступном для модификации коде.

Одним из вариантов решения проблемы высокой связности в данном случае является паттерн Посетитель.

Паттерн Посетитель

ПРИМЕЧАНИЕ

В объектно-ориентированном программировании паттерн Посетитель является способом разделения алгоритмов и объектных структур.

http://en.wikipedia.org/wiki/Visitor_pattern

Основная идея этого паттерна состоит в том, что каждый элемент объектной структуры содержит метод Accept, который принимает на вход в качестве аргумента специальный объект, Посетитель, реализующий заранее известный интерфейс. Этот интерфейс содержит по одному методу Visit для каждого типа узла. Метод Accept в каждом узле должен вызывать методы Visit для осуществления навигации по структуре.

Пример2. Простейший Посетитель.

Теперь для того, чтобы обеспечить работу алгоритма по объектной структуре, достаточно реализовать интерфейс IVisitor.

Пример3. Реализация простейшего посетителя.
      using System.IO;
using VisitorPatternDemo.Tree;

namespace VisitorPatternDemo
{
  publicclass PrintVisitor : IVisitor
  {
    privatereadonly TextWriter _writer;

    private PrintVisitor(TextWriter writer)
    {
      _writer = writer;
    }

    publicstaticvoid Print(TreeNodeBase tree, TextWriter writer)
    {
      tree.AcceptVisitor(new PrintVisitor(writer));
    }

    void IVisitor.VisitRoot(RootNode rootNode)
    {
      _writer.WriteLine(rootNode.Name + " : Root");
      foreach (Type1Node node in rootNode.Type1Nodes)
        node.AcceptVisitor(this);
      foreach (Type2Node node in rootNode.Type2Nodes)
        node.AcceptVisitor(this);
    }

    void IVisitor.VisitType1(Type1Node node)
    {
      _writer.WriteLine(node.Name + " : Type1");
    }

    void IVisitor.VisitType2(Type2Node node)
    {
      _writer.WriteLine(node.Name + " : Type2");
      foreach (Type3Node t3Node in node.Type3Nodes)
        t3Node.AcceptVisitor(this);
    }

    void IVisitor.VisitType3(Type3Node node)
    {
      _writer.WriteLine(node.Name + " : Type3");
    }
  }
}

Если алгоритм обхода структуры всегда один и тот же, код обхода дерева (foreach в примере) можно перенести в метод Accept. При этом в коде реализации посетителя осуществлять обход не нужно. Однако это снижает гибкость кода, так как другой способ обхода использовать уже не удастся.

Альтернативой может служить создание базовой реализации посетителя, осуществляющей только обход, без выполнения каких либо действий.

Пример 4. Реализация обхода в отдельном дереве.

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

В языках, поддерживающих перегрузку методов, ее можно использовать для упрощения реализации метода Accept .

Для использования этой техники необходимо, чтобы все методы посетителя назывались одинаково и различались лишь типом параметра. В этом случае код метода Accept будет одинаковым. Компилятор при компиляции сам выберет необходимый метод.

Пример 5. Посетитель с двойной диспетчеризацией.

Этот прием позволяет устранить ошибки, связанные с вызовом неверного метода в методе Accept.

Передача параметров

В некоторых случаях в методы посетителя требуется передать параметр. Для этого необходимо в методы Visit и AcceptVisitor добавить параметр – контекст вызова. Если язык поддерживает обобщенное программирование, то этот контекст можно сделать статически типизированным.

Пример 6. Посетитель с параметрами.

Реализация посетителя при помощи функторов

Однако даже при использовании перегрузки написание кучи методов Accept весьма утомительно. Полностью избавиться от этих методов можно, если язык программирования поддерживает функторы (функциональные объекты).

ПРИМЕЧАНИЕ

Функциональный объект, часто называемый функтором, - это конструкция, позволяющая объекту быть вызванным, как если бы он был простой функцией, обычно с тем же синтаксисом.

http://en.wikipedia.org/wiki/Function_object

В .NET имеется специализированный тип – делегат, реализующий функциональность функторов.

Идея использования функциональных объектов заключается в том, что мы во время выполнения строим карту (map) типов на эти объекты и затем, вместо вызова Accept, используем эту карту. С использованием механизма рефлексии такую карту можно построить автоматически, на основании интерфейса посетителя.

Пример 7. Посетитель с внешней реализацией метода Accept.

Для ускорения работы такого посетителя крайне рекомендуется кэшировать карты в статическом контексте. Еще один вариант повышения производительности – замена карты с делегатами на динамическую кодогенерацию.


Эта статья опубликована в журнале RSDN Magazine #3-2006. Информацию о журнале можно найти здесь
    Сообщений 35    Оценка 120        Оценить