Почему нам нужно принять () в шаблоне Visitor и почему мы не можем вызвать visitor.visit () напрямую? - PullRequest
0 голосов
/ 17 мая 2018

Я пересматриваю шаблон посетителя, который использовал некоторое время назад.У нас есть базовый класс Element, у которого есть виртуальный метод accept (Visitor), и этот метод переопределяется во всех классах, наследуемых от Element.И все, что accept () делает в любом производном классе, это вызывает visitor-> visit (* this).Теперь, когда клиент запускает код, он / она делает, например:

Visitor& theVisitor = *new ConcreteVisitor();    
for_each(elements.begin(), elements.end(), [](Element& e) { e.accept(theVisitor));})

Почему клиент не может просто вызвать visitor-> visit (element) следующим образом:

Visitor& theVisitor = *new ConcreteVisitor();    
for_each(elements.begin(), elements.end(), [&theVisitor](Element& e) { theVisitor.visit(e); });

Чтополезная информация в вызове element.accept (посетитель), который в свою очередь вызывает visitor.visit (элемент)?Это делает использование шаблона Visitor громоздким и требует дополнительного кода во всей иерархии классов Element.

Итак, кто-то может объяснить преимущества accept () здесь?

1 Ответ

0 голосов
/ 24 мая 2018

Я долго путался с шаблоном Visitor, и я пытался найти объяснения по всему Интернету, и эти объяснения смутили меня еще больше.Теперь я понял причины, по которым нужен шаблон посетителя, и способ его реализации, поэтому вот он:

Шаблон посетителя, необходимый для решения проблемы двойной отправки.

Одна отправка - когдау вас есть одна иерархия классов, и у вас есть экземпляр конкретного класса в этой иерархии, и вы хотите вызвать соответствующий метод для этого экземпляра.Это решается переопределением функций (с использованием таблицы виртуальных функций в C ++).

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

Давайте рассмотрим пример.

Первоклассная иерархия: животные.База: Animal, производная: Fish, Mammal, Bird.Иерархия второго класса: призыватели.База: Invoker, производная: MovementInvoker (двигать животное), VoiceInvoker (заставляет животное звучать), FeedingInvoker (кормит животное).

Теперь для каждого конкретного животного и каждогоДля конкретного вызывающего мы хотим, чтобы была вызвана только одна конкретная функция, которая будет выполнять определенную работу (например, Feed the Bird или Sound the Fish).Таким образом, в целом у нас есть 3x3 = 9 функций для выполнения работы.

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

Итак, клиент хочет сделать что-то вроде:

void act(Animal& animal, Invoker& invoker)
{
  // Do the job for this specific animal using this specific invoker
}

Или:

void act(vector<shared_ptr<Animal>>& animals, vector<shared_ptr<Invoker>>& invokers)
{
    for(auto& animal : animals)
    {
        for(auto& invoker : invokers)
        {
            // Do the job for this specific animal and invoker.
        }
    }
}

Теперь: как это возможно в RUN-ВРЕМЯ для вызова одного из 9 (или любого другого) конкретного метода, который имеет дело с этим конкретным Animal и с этим конкретным Invoker?

Здесь идет двойная отправка.Вам абсолютно необходимо вызвать одну виртуальную функцию из иерархии первого класса и одну виртуальную функцию из второй.

Поэтому вам нужно вызвать виртуальный метод Animal (используя таблицу виртуальных функций, вы найдете конкретную функциюконкретный экземпляр в иерархии классов Animal, и вам также необходимо вызвать виртуальный метод Invoker (который найдет конкретный инициатор).

ВЫ ДОЛЖНЫ ВЫЗВАТЬ ДВА ВИРТУАЛЬНЫХ МЕТОДА.

Итак, вот реализация (вы можете скопировать и запустить, я тестировал ее с помощью компилятора g ++):

visitor.h:

#ifndef __VISITOR__
#define __VISITOR__

struct Invoker; // forward declaration;

// -----------------------------------------//

struct Animal
{
    // The name of the function can be anything of course.
    virtual void accept(Invoker& invoker) = 0;
};

struct Fish : public Animal
{
    void accept(Invoker& invoker) override;
};

struct Mammal : public Animal
{
    void accept(Invoker& invoker) override;
};

struct Bird : public Animal
{
    void accept(Invoker& invoker) override;
};

// -----------------------------------------//

struct Invoker
{
  virtual void doTheJob(Fish&   fish)   = 0;
  virtual void doTheJob(Mammal& Mammal) = 0;
  virtual void doTheJob(Bird&   Bird)   = 0;
};

struct MovementInvoker : public Invoker
{
  void doTheJob(Fish&   fish)   override;
  void doTheJob(Mammal& Mammal) override;
  void doTheJob(Bird&   Bird)   override;
};

struct VoiceInvoker : public Invoker
{
  void doTheJob(Fish&   fish)   override;
  void doTheJob(Mammal& Mammal) override;
  void doTheJob(Bird&   Bird)   override;
};

struct FeedingInvoker : public Invoker
{
  void doTheJob(Fish&   fish)   override;
  void doTheJob(Mammal& Mammal) override;
  void doTheJob(Bird&   Bird)   override;
};

#endif

visitor.cpp:

#include <iostream>
#include <memory>
#include <vector>
#include "visitor.h"
using namespace std;

// -----------------------------------------//

void Fish::accept(Invoker& invoker)
{
    invoker.doTheJob(*this);
}

void Mammal::accept(Invoker& invoker)
{
    invoker.doTheJob(*this);
}

void Bird::accept(Invoker& invoker)
{
    invoker.doTheJob(*this);
}

// -----------------------------------------//

void MovementInvoker::doTheJob(Fish& fish)
{
    cout << "Make the fish swim" << endl;
}

void MovementInvoker::doTheJob(Mammal& Mammal)
{
    cout << "Make the mammal run" << endl;
}

void MovementInvoker::doTheJob(Bird& Bird)
{
    cout << "Make the bird fly" << endl;
}

// -----------------------------------------//

void VoiceInvoker::doTheJob(Fish& fish)
{
    cout << "Make the fish keep silence" << endl;
}

void VoiceInvoker::doTheJob(Mammal& Mammal)
{
    cout << "Make the mammal howl" << endl;
}

void VoiceInvoker::doTheJob(Bird& Bird)
{
    cout << "Make the bird chirp" << endl;
}

// -----------------------------------------//

void FeedingInvoker::doTheJob(Fish& fish)
{
    cout << "Give the fish some worms" << endl;
}

void FeedingInvoker::doTheJob(Mammal& Mammal)
{
    cout << "Give the mammal some milk" << endl;
}

void FeedingInvoker::doTheJob(Bird& Bird)
{
    cout << "Give the bird some seed" << endl;
}

int main()
{
    vector<shared_ptr<Animal>> animals = { make_shared<Fish>   (),
                                           make_shared<Mammal> (),
                                           make_shared<Bird>   () };

    vector<shared_ptr<Invoker>> invokers = { make_shared<MovementInvoker> (),
                                             make_shared<VoiceInvoker>    (),
                                             make_shared<FeedingInvoker>  () };

    for(auto& animal : animals)
    {
        for(auto& invoker : invokers)
        {
            animal->accept(*invoker);
        }
    }
}

Вывод приведенного выше кода:

Make the fish swim
Make the fish keep silence
Give the fish some worms
Make the mammal run
Make the mammal howl
Give the mammal some milk
Make the bird fly
Make the bird chirp
Give the bird some seed

Так что же происходит, когда клиент получает экземпляр Animal и экземпляр Invoker и вызывает animal.accept(invoker)?

Предположим, что экземпляр Animal равен Bird, а экземпляр Invoker равен FeedingInvoker.

Тогда благодаря виртуальной таблице функций будет вызвана таблица Bird::accept(Invoker&), которая в свою очередь будет выполняться invoker.doTheJob(Bird&).Поскольку Invoker экземпляр равен FeedingInvoker, таблица виртуальных функций будет использовать FeedingInvoker::accept(Bird&) для этого вызова.

Итак, мы сделали двойную диспетчеризацию и вызвали правильный метод (один из 9 возможных методов) для Birdи FeedingInvoker.

Почему шаблон Visitor хорош?

  1. Клиенту не нужно зависеть как от иерархий сложных классов Animals, так и Invokers.

  2. Если необходимо добавить нового конкретного животного (скажем, Insect), не нужно изменять существующую иерархию Animal.Нам нужно только добавить: doTheJob(Insect& insect) к Invoker и всем производным инициаторам.

Шаблон посетителя элегантно реализует принцип открытого / закрытого объектно-ориентированного проектирования: система должна быть открытойк расширениям и закрыты для изменений.

(В классическом шаблоне Visitor Invoker будет заменен на Visitor и doTheJob() на visit(), но для меня эти имена на самом деле не отражают того факта, чтонекоторая работа сделана над элементами).

...