Когда вы должны действительно использовать шаблон посетителя

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

Я наткнулся на этот пост: Когда следует использовать шаблон дизайна посетителя?

и пользователь, который написал первый ответ, говорит следующее:

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

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

Но он также говорит, что это прекрасно добавляет новый полиморфный метод в иерархию, пока это "простая" иерархия. Но мой вопрос заключается в том, когда вы настраиваете свою линию и решаете, что просто, а что нет.
Кроме того, что, если иерархия является сложной, но добавление нового метода просто делает полный смысл и метод экземпляра вместо того, чтобы иметь операцию в совершенно другом классе?

Поиск немного больше Я нашел эту статью, объясняя шаблон посетителя, и он использует http://butunclebob.com/ArticleS.UncleBob.IuseVisitor

Автор привел пример, где запись метода экземпляра делает объект связанным с чем-то и перемещение метода в другой класс (посетитель) нарушает развязку. Это имело для меня больше смысла, но я все еще не совсем уверен, когда этот шаблон действительно должен использоваться, первый аргумент "изменение времени иерархии каждый раз, когда вы хотите добавить новый полиморфный метод..." мне кажется оправдание, потому что, если метод, похоже, вписывается в иерархию, он должен быть там логически, предположив, что пример Animal был действительно сложной иерархией, и я бы решил добавить метод make sound, добавив, что метод экземпляра будет логичным выбором (в моем ум).
Но, может быть, я ошибаюсь, поэтому я прошу об этом более подробно, может быть, кто-то может просветить меня.

Ответы

Ответ 1

Шаблон посетителя подходит для случаев, когда нам нужна двойная отправка, а язык программирования не поддерживает его.

Я разработчик С#, а С# не поддерживает его напрямую. И я думаю, что ни С++. (хотя в более новой версии С#, post С# 4.0, есть ключевое слово dynamic, которое может сделать трюк).

Я приведу пример, чтобы показать ситуацию, когда нам нужна двойная отправка и как посетитель помогает нам в этом. (Поскольку я разработчик С#, моя база кода находится на С#, пожалуйста, несите меня, но я обещаю, что я старался сохранить ее как можно более нейтральной)

Пример:

Предположим, у меня есть 3 типа мобильных устройств - iPhone, Android, Windows Mobile.

Все эти три устройства имеют Bluetooth-радио, установленное в них.

Предположим, что радиостанция с синим зубом может быть от двух отдельных OEM-производителей - Intel и Broadcom.

Чтобы сделать пример подходящим для нашего обсуждения, давайте также предположим, что API-интерфейсы, предоставляемые радиостанцией Intel, отличаются от тех, которые выставлены радио Broadcom.

Вот как выглядят мои классы -

введите описание изображения здесь введите описание изображения здесь

Теперь я хотел бы ввести операцию "Включение Bluetooth на мобильном устройстве".

Его сигнатура функции должна выглядеть примерно так:

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

Итак, в зависимости от правильного типа устройства и В зависимости от правильного типа радиосвязи Bluetooth его можно включить с помощью вызова соответствующих шагов или алгоритма.

В принципе, он становится матрицей 3 x 2, где-в Im пытается вектор правильной операции в зависимости от правильного типа объектов.

Полиморфное поведение в зависимости от типа обоих аргументов.

введите описание изображения здесь

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

Двойная отправка необходима из-за матрицы 3x2

Представление шаблона посетителя в коде -

Сначала мне нужно принять решение, какая иерархия классов более стабильна (менее подвержена изменениям) - иерархия классов устройств или иерархия классов синего зуба. Еще одной стабильностью станут посещаемые классы, а менее стабильная станет классом посетителей. В этом примере Ill говорит, что класс устройства более стабилен.

Вот настройка

введите описание изображения здесь

Вот код клиента и тестовый код

 class Client
  {
      public void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothVisitor blueToothRadio) 
      {
          mobileDevice.TurnOn(blueToothRadio);        
      }
  }


 [TestClass]
public class VisitorPattern
{

    Client mClient = new Client();

    [TestMethod]
    public void AndroidOverBroadCom()
    {
        IMobileDevice device = new Android();
        IBlueToothVisitor btVisitor = new BroadComBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }

    [TestMethod]
    public void AndroidOverIntel()
    {
        IMobileDevice device = new Android();
        IBlueToothVisitor btVisitor = new IntelBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }

    [TestMethod]
    public void iPhoneOverBroadCom()
    {
        IMobileDevice device = new iPhone();
        IBlueToothVisitor btVisitor = new BroadComBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }

    [TestMethod]
    public void iPhoneOverIntel()
    {
        IMobileDevice device = new iPhone();
        IBlueToothVisitor btVisitor = new IntelBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }
}

Вот иерархия классов

     /// <summary>
        /// Visitable class interface 
        /// </summary>
       interface IMobileDevice
        {
           /// <summary>
           /// It is the 'Accept' method of visitable class
           /// </summary>
            /// <param name="blueToothVisitor">Visitor Visiting the class</param>
           void TurnOn(IBlueToothVisitor blueToothVisitor);
        }

       class iPhone : IMobileDevice
       {
           public void TurnOn(IBlueToothVisitor blueToothVisitor)
           {
               blueToothVisitor.SwitchOn(this);
           }
       }

       class Android : IMobileDevice
       {
           public void TurnOn(IBlueToothVisitor blueToothVisitor)
           {
               blueToothVisitor.SwitchOn(this);
           }
       }

       class WindowsMobile : IMobileDevice
       {
           public void TurnOn(IBlueToothVisitor blueToothVisitor)
           {
               blueToothVisitor.SwitchOn(this);
           }
       }

        interface IBlueToothRadio
        {

        }

        class BroadComBlueToothRadio : IBlueToothRadio
        {

        }

        class IntelBlueToothRadio : IBlueToothRadio
        {

        }

Последовали посетители -

/// <summary>
/// Wiki Page - The Visitor pattern encodes a logical operation on the whole hierarchy into a single class containing one method per type. 
/// </summary>
interface IBlueToothVisitor
{
    void SwitchOn(iPhone device);
    void SwitchOn(WindowsMobile device);
    void SwitchOn(Android device);
}


class IntelBlueToothVisitor : IBlueToothVisitor
{
    IBlueToothRadio intelRadio = new IntelBlueToothRadio();

    public void SwitchOn(iPhone device)
    {
        Console.WriteLine("Swithing On intel radio on iPhone");
    }

    public void SwitchOn(WindowsMobile device)
    {
        Console.WriteLine("Swithing On intel radio on Windows Mobile");
    }

    public void SwitchOn(Android device)
    {
        Console.WriteLine("Swithing On intel radio on Android");
    }
}

class BroadComBlueToothVisitor : IBlueToothVisitor
{
    IBlueToothRadio broadCom = new BroadComBlueToothRadio();

    public void SwitchOn(iPhone device)
    {
        Console.WriteLine("Swithing On BroadCom radio on iPhone");
    }

    public void SwitchOn(WindowsMobile device)
    {
        Console.WriteLine("Swithing On BroadCom radio on Windows Mobile");
    }

    public void SwitchOn(Android device)
    {
        Console.WriteLine("Swithing On BroadCom radio on Android");
    }
}

Позвольте мне пройти через некоторые точки этой структуры -

  • У меня есть 2 посетителя Bluetooth, которые содержат алгоритм для включения Bluetooth на каждом типе мобильного устройства.
  • Я сохранил BluetoothVistor и BluetoothRadio отдельно, чтобы придерживаться философии посетителя - "Добавить операции без изменения самих классов". Возможно, другие захотят объединить его в сам класс BluetoothRadio.
  • Каждый посетитель имеет 3 функции, определенные - по одному для каждого мобильного устройства типа.
  • Также здесь, поскольку существует 6 вариантов алгоритма (в зависимости от типа объекта) требуется двойная отправка.
  • Как я писал выше, мое первоначальное требование состояло в том, чтобы иметь такую ​​функцию - void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio), теперь для двойной отправки на работу я изменил подпись - вместо IBlueToothRadio использую IBlueToothVisitor

Пожалуйста, дайте мне знать, если какой-либо бит неясен, мы можем обсудить его дальше.

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


Изменить 1

В соответствии с комментариями я удаляю оболочку IBlueToothVisitor

Вот как будет выглядеть шаблон посетителя без этой обертки - введите описание изображения здесь

Однако он все еще является шаблоном посетителя

  • IMobileDevice - это интерфейс класса Visitable.

  • IMobileDevice.TurnOn - это метод "Accept" для посещаемого класса. Но теперь он принимает IBlueToothRadio в качестве посетителя вместо IBlueToothVisitor

  • IBlueToothRadio становится новым интерфейсом класса посетителей.

Таким образом, подпись функции в клиенте теперь изменена для использования   IBlueToothRadio

  public void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
  public void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

Ответ 2

Я думаю, что важно не то, насколько сложна ваша иерархия, насколько фиксирована иерархия.

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

Это все компромисс, поэтому трудно нарисовать "линию". Вам просто нужно решить, какой дизайн будет более управляемым в долгосрочной перспективе, учитывая ваши требования.

Например, вам может не потребоваться, чтобы ваш класс Animal имел множество функций-членов, таких как printToPDF(), getMigrationReport5() и т.д., и шаблон посетителя был бы лучше.

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

Существует третий вариант, который должен использовать какое-то соответствие шаблонов. Но в настоящее время это сложно сделать элегантно на С++ без какой-либо внешней библиотеки.

Ответ 3

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

По существу шаблон посетителя является костылем для языков, у которых отсутствует "истинный" полиморфизм [1] и функции более высокого порядка. В противном случае вы просто определяете состав операций как полиморфную функцию более высокого порядка foo(), которая просто принимает вспомогательные функции в качестве параметров для решения специфики.

Так что это ограничение языка больше, чем сила дизайна. Если вы много чего используете, чтобы сократить время компиляции, вы должны подумать о том, пытаетесь ли вы работать с инструментом или с помощью инструмента, т.е. Выполняете ли вы эквивалент "реального программиста, который может программировать Fortran в любой язык". (И возможно ли это время, чтобы забрать/изучить другой инструмент, помимо молота.)

  • Под этим я подразумеваю способность функций работать над значениями "произвольных типов", не прибивая тип к чему-то конкретному (A или B), что означает точный тип/подпись изменения функции/вызова в зависимости от что вы проходите.

Ответ 4

Когда я видел шаблон посетителя, рекомендуемый для SO, он почти всегда должен избегать проверки типов. Поэтому шаблон посетителя применяется к иерархии небольших классов, особенно когда каждый подкласс может быть указан в коде.

Вопросы, которые приводят к этому шаблону, часто начинаются с: "У меня есть длинная цепочка операторов if-else, где каждое условие проверяет другой класс из иерархии. Как я могу избежать так много операторов if-else?"

Например, скажем, у меня есть приложение для планирования с классом под названием Day, который имеет семь подклассов (по одному для каждого дня недели). Наивный подход заключается в том, чтобы реализовать этот планировщик, используя if-else или switch-case.

// pseudocode
if (day == Monday)
    // Monday logic
else if (day == Tuesday)
    // Tuesday logic
// etc.

Шаблон посетителя избегает этой цепочки условной логики, используя двойную отправку. Семь дней - это семь методов, причем соответствующий метод выбран во время выполнения на основе типа аргумента Day, который передается.

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