Как передать данные «универсальному» наблюдателю? В качестве аргументов или в виде единой структуры?

Я занят добавлением универсального механизма наблюдателя в устаревшее приложение C ++ (использующее Visual Studio 2010, но не использующее .Net, поэтому о делегатах .Net не может быть и речи).

В дизайне я хочу как можно больше отделить специфичную для приложения часть от общего механизма наблюдателя.

Наиболее логичный способ реализации наблюдателей выглядит так:

class IDoThisObserver
   {
   public:
      void handlDoThis(int arg1, int arg2) = 0;
   };

Для каждого типа наблюдателя (IDoThisObserver, IDoThatObserver, ...) аргументы методов (handleDoThis, handleDoThat) разные.

Что остается в общем способе хранения наблюдателей, например:

template<typename T>
class ObserverContainer
   {
   public:
      void addObserver (T &t) {m_observers.push_back(&t);}
   private:
      std::list<T*> m_observers;
   };

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

Альтернативный способ - «упаковать» все аргументы в один аргумент, например:

struct DoThisInfo
   {
   DoThisInfo (int arg1, int arg2) : m_arg1(arg1), m_arg2(arg2) {}
   int m_arg1;
   int m_arg2;
   };

А затем определите более общего наблюдателя, например:

template<typename T>
class IObserver
   {
   public:
      void notify(const T &t) = 0;
   };

И тогда набор этих наблюдателей стал бы таким:

template<typename T>
class ObserverContainer
   {
   public:
      void addObserver (IObserver<T> &obs) {m_observers.push_back(&obs);}
   private:
      std::list<IObserver<T>*> m_observers;
   };

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

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

class MyObserver : public IObserver<NotifyThis>, public IObserver<NotifyThat>
   {
   ...
   };

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

РЕДАКТИРОВАТЬ: я немного изучил альтернативные подходы, и подход слот / сигнал кажется еще одним хорошим кандидатом. Есть ли какие-либо важные недостатки в Slot / Signal, о которых мне следует знать?


person Patrick    schedule 30.08.2010    source источник


Ответы (5)


Дизайн с аргументом struct определенно лучше, поскольку он позволяет писать общий код в ObserverContainer. Обычно рекомендуется заменять длинные списки аргументов объектами, которые инкапсулируют аргументы, и это хороший пример выгоды. Создавая более общую абстракцию для вашего notify метода (с помощью структуры, которую вы определяете notify как метод, который принимает фрагмент «данных», тогда как с помощью списка аргументов вы определяете метод, который принимает два числа), вы позволяете себе написать общий код, который использует метод и не должен заботиться о точном составе переданного блока данных.

person bshields    schedule 30.08.2010

Почему бы просто не сделать:

class IObserver {
    // whatever is in common
};

class IDoThisObserver : public IObserver
{
   public:
      void handlDoThis(int arg1, int arg2) = 0;
};

class IDoThatObserver : public IObserver
{
   public:
      void handlDoThat(double arg1) = 0;
};

?

Тогда у вас есть:

class ObserverContainer
{
   public:
      void addObserver (IObserver* t) {m_observers.push_back(t);}
   private:
      std::list<IObserver*> m_observers;
};
person Kornel Kisielewicz    schedule 30.08.2010
comment
Это просто помещает общий код в IObserver. Я искал способ поместить общий код в ObserverContainer. - person Patrick; 30.08.2010
comment
Этот подход хорош до тех пор, пока нет необходимости перебирать наблюдателей и вызывать их методы обработки каким-либо обычным способом. Это был бы кошмар. - person manneorama; 30.08.2010
comment
Проблема в том, что ObserverContainer не может вызывать handle методы для наблюдателей без большого количества приведений. - person bshields; 30.08.2010
comment
@bshields, как он может кастовать handle, если не знает, какие параметры они принимают? - person Kornel Kisielewicz; 30.08.2010
comment
@Kornel Основная функция ObserverContainer, по-видимому, состоит в том, чтобы перебирать список наблюдателей и вызывать метод notify / handle для каждого из них. Версия шаблона IObserver из исходного вопроса прекрасно это поддерживает. У вас нет версии, ObserverContainer пришлось бы преобразовывать каждый IObserver* в IDoThisObserver или IDoThatObserver, чтобы иметь возможность вызвать соответствующий метод handle. Очевидно, это неправильный подход. - person bshields; 30.08.2010
comment
@bshields, как ObserverContainer узнает, что нужно преобразовать? Если у вас нет отражения, нет способа получить правильный метод и параметры, которые он должен получить в любом случае. Если только ObserverContainer не имеет жестко запрограммированных случаев для каждого типа Observer, но это все равно сводится к некоторой форме RTTI. - person Kornel Kisielewicz; 30.08.2010
comment
@Kornel Да, я хочу сказать, что для каждого типа Observer + dynamic_cast потребуются жестко запрограммированные кейсы. Я не защищаю это, просто указываю, что это то, что нужно, чтобы дать ObserverContainer предполагаемую функциональность в написанном вами коде. - person bshields; 30.08.2010

Вы изучали Boost.Signals? Лучше, чем переделывать колесо.

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

Два недостатка Observer-Pattern / SignalSlots, которые я видел:
1) Ход программы трудно или даже невозможно понять, глядя только на источник.
2) Сильно динамические программы с большим количеством Observer / SignalSlots могут столкнуться с а "удалить это"

Помимо всего прочего, мне нравятся Observers / SignalSlots больше, чем подклассы и, следовательно, высокая степень связи.

person Markus Kull    schedule 30.08.2010

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

 enum Type {
    NOTIFY_THIS,
    NOTIFY_THAT
 };

 struct Data {
 virtual Type getType() = 0;
 };

 struct NotifyThisData: public Data {
    NotifyThisData(int _a, int _b):a(_a), b(_b) { }
    int a,b;
    Type getType() { return NOTIFY_THIS; }
 };

 struct NotifyThatData: public Data {
    NotifyThatData(std::string _str):str(_str) { }
    std::string str;
    Type getType() { return NOTIFY_THAT; }
 };

 struct DataCarrier {
    std::vector<Data*> m_TypeData;  
 };

 class IObserver {
 public:
     virtual void handle(DataCarrier& data) = 0;
 };

 class NotifyThis: public virtual IObserver {
 public:
         virtual void handle(DataCarrier& data) {
                 vector<Data*>::iterator iter = find_if(data.m_TypeData.begin(), data.m_TypeData.end(), bind2nd(functor(), NOTIFY_THIS);
                 if (iter == data.m_TypeData.end())
                         return;
                 NotifyThisData* d = dynamic_cast<NotifyThisData*>(*iter);
                 std::cout << "NotifyThis a: " << d->a << " b: " << d->b << "\n";
         }
 };

 class NotifyThat: public virtual IObserver {
 public:
         virtual void handle(DataCarrier& data) {
                 vector<Data*>::iterator iter = find_if(data.m_TypeData.begin(), data.m_TypeData.end(), bind2nd(functor(),NOTIFY_THAT);
                 if (iter == data.m_TypeData.end())
                         return;            
                 NotifyThatData* d = dynamic_cast<NotifyThatData*>(*iter);
                 std::cout << "NotifyThat str: " << d->str << "\n";
         }
 };

 class ObserverContainer
    {
    public:
       void addObserver (IObserver* obs) {m_observers.push_back(obs);}
       void notify(DataCarrier& d) {
                 for (unsigned i=0; i < m_observers.size(); ++i) {
                         m_observers[i]->handle(d);
                 }
         }
    private:
       std::vector<IObserver*> m_observers;
    };

 class MyObserver: public NotifyThis, public NotifyThat {
 public:
         virtual void handle(DataCarrier& data) { std::cout << "In MyObserver Handle data\n"; }
 };

 int main() {
         ObserverContainer container;
         container.addObserver(new NotifyThis());
         container.addObserver(new NotifyThat());
         container.addObserver(new MyObserver());

         DataCarrier d;
         d.m_TypeData.push_back(new NotifyThisData(10, 20));
         d.m_TypeData.push_back(new NotifyThatData("test"));

    container.notify(d);
    return 0;
 }

Таким образом, вам нужно изменить только перечисление, если вы добавляете новую структуру. Также вы можете использовать boost :: shared_ptr для обработки беспорядка указателей.

person aeh    schedule 30.08.2010

Я бы не понял синтаксис правильно, поэтому просто перечислю объявления, чтобы проиллюстрировать структуры. Общий Observer может ожидать параметр, который либо подклассифицируется для определенных форм ваших требуемых параметров, либо является структурой, включающей горизонтальное сопоставление всех примитивных параметров, которые потребуются вашим Observers. Тогда ObserverContainer может функционировать как AbstractFactory, а каждый подкласс ObserverContainer может быть DoThatObserverFactory и DoThisObserverFactory. Фабрика создаст наблюдателя и назначит ему конфигурацию, чтобы сообщить ему, какой параметр ожидать.

class AbstractObserverFactory {...};
class DoThatObserverFactory : AbstractObserverFactory {...};
class DoThisObserverFactory : AbstractObserverFactory {...};
class ObserverParam {...};
class DoThatObserverParam : ObserverParam {...};
class DoThisObserverParam : ObserverParam {...};
class Observer;
class DoThisObserver : public Observer
{
   public:
      void handlDoThis(DoThisObserverParam);
};
person Aaron    schedule 30.08.2010