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

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

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

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

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

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

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

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

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

4 ответа

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

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

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

Пример:

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

На всех этих трех устройствах установлено радио Bluetooth.

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

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

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

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

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

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

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

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

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

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

Двойная диспетчеризация здесь необходима благодаря матрице 3х2

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

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

Вот установка

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

 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");
    }
}

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

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

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

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


Редактировать 1

Согласно комментариям, я удаляю обертку IBlueToothVisitor

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

Однако его по-прежнему является моделью посетителя

  1. IMobileDevice интерфейс интерфейса Visitable

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

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

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

  public void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

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

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

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

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

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

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

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

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

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

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

Вы можете использовать шаблон посетителя для разработки хранителя БД, как показано на этой диаграмме здесь:диаграмма классов для хранителя объектов в разных БД

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

    Visitor mongosaver = new MongodbSaver();
    Visitable user = new User();
    //save the user object using mongodb saver
    user.accept(mongosaver);
    Visitable post = new BlogPost();
    Visitor mysqlsaver = new MysqlSaver();
    //save the BlogPost using Mysql saver
    post.accept(mysqlsaver);

вы также можете сослаться на это:https://www.oodesign.com/visitor-pattern.html

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

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

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

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

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

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

Другие вопросы по тегам