Проблемы с реализацией паттерна "Наблюдатель" - PullRequest
32 голосов
/ 19 июня 2009

Я столкнулся с интересной проблемой при реализации шаблона Observer с C ++ и STL. Рассмотрим этот классический пример:

class Observer {
public:
   virtual void notify() = 0;
};

class Subject {
public:
   void addObserver( Observer* );
   void remObserver( Observer* );
private:
   void notifyAll();
};

void Subject::notifyAll() {
   for (all registered observers) { observer->notify(); }
}

Этот пример можно найти в каждой книге по шаблонам проектирования. К сожалению, реальные системы более сложны, поэтому возникает первая проблема: некоторые наблюдатели принимают решение добавить других наблюдателей в Субъект после получения уведомления. Это делает недействительным цикл for и все итераторы, которые я использую. Решение довольно простое - я делаю снимок списка зарегистрированных наблюдателей и перебираю снимок. Добавление новых наблюдателей не делает снимок недействительным, поэтому все выглядит хорошо. Но тут возникает другая проблема: наблюдатели решают уничтожить себя, получив уведомление. Хуже того, один наблюдатель может решить уничтожить всех остальных наблюдателей (они управляются из сценариев), и это делает недействительной очередь и снимок. Я ловлю себя на переборе нераспределенных указателей.

Мой вопрос: как мне справляться с ситуациями, когда наблюдатели убивают друг друга? Есть ли готовые шаблоны? Я всегда думал, что «Обозреватель» - это самый простой шаблон проектирования в мире, но сейчас кажется, что его не так просто реализовать правильно ...

Спасибо всем за проявленный интерес. Позвольте нам иметь резюме решений:

[1] «Не делай этого» * ​​1013 * Извините, но это необходимо. Наблюдатели управляются из сценариев и собирают мусор. Я не могу контролировать сборку мусора, чтобы предотвратить их удаление;

[2] «Использовать надстройку :: сигнал» Самое многообещающее решение, но я не могу дать надежду проекту, такие решения должны приниматься только руководителем проекта (мы пишем в Playstation) ;

[3] «Использовать shared__ptr» Это предотвратит перераспределение наблюдателей. Некоторые подсистемы могут полагаться на очистку пула памяти, поэтому я не думаю, что могу использовать shared_ptr.

[4] «Отложить освобождение наблюдателя» Поставить в очередь наблюдателей для удаления во время уведомления, а затем использовать второй цикл для их удаления. К сожалению, я не могу предотвратить освобождение, поэтому я использую хитрость обертывания наблюдателя с каким-то «адаптером», сохраняя фактически список «адаптеров». На деструкторе наблюдатели отменяют назначение со своих адаптеров, затем я беру второй цикл, чтобы уничтожить пустые адаптеры.

p.s. это нормально, что я редактирую свой вопрос, чтобы подвести итог всего поста? Я нуб на StackOverflow ...

Ответы [ 14 ]

14 голосов
/ 19 июня 2009

Очень интересная тема.

Попробуйте это:

  1. Измените remObserver, чтобы исключить запись, а не просто удалить ее (и лишить законной силы итераторы списка).
  2. Измените цикл notifyAll на:

    для (всех зарегистрированных наблюдателей) {if (наблюдатель) наблюдатель-> notify (); }

  3. Добавить еще один цикл в конце notifyAll, чтобы удалить все нулевые записи из списка наблюдателей

7 голосов
/ 19 июня 2009

Лично я использую boost :: сигналы для реализации своих наблюдателей; Мне придется проверить, но я считаю, что он обрабатывает вышеописанные сценарии ( отредактировано : найдено, см. «Когда могут произойти отключения» ). Это упрощает вашу реализацию и не требует создания собственного класса:

class Subject {
public:
   boost::signals::connection addObserver( const boost::function<void ()>& func )
   { return sig.connect(func); }

private:
   boost::signal<void ()> sig;

   void notifyAll() { sig(); }
};

void some_func() { /* impl */ }

int main() {
   Subject foo;
   boost::signals::connection c = foo.addObserver(boost::bind(&some_func));

   c.disconnect(); // remove yourself.
}
6 голосов
/ 19 июня 2009

Мужчина идет к врачу и говорит: «Док, когда я так поднимаю руку, мне очень больно!» Доктор говорит: «Не делай этого».

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

5 голосов
/ 20 июня 2009

Вот вариант идеи, уже представленной TED.

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

void Subject::notifyAll()
{
    list<Observer*>::iterator i = m_Observers.begin();
    while(i != m_Observers.end())
    {
        Observer* observer = *i;
        if(observer)
        {
            observer->notify();
            ++i;
        }
        else
        {
            i = m_Observers.erase(i);
        }
    }
}

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

4 голосов
/ 19 июня 2009

Проблема заключается в собственности. Вы могли бы использовать умные указатели, например, классы boost::shared_ptr и boost::weak_ptr, чтобы продлить время жизни ваших наблюдателей до точки "удаления".

3 голосов
/ 19 июня 2009

Существует несколько решений этой проблемы:

  1. Использование boost::signal позволяет автоматически удалять соединения при разрушении объекта. Но вы должны быть очень осторожными с безопасностью потоков
  2. Используйте boost::weak_ptr или tr1::weak_ptr для управления наблюдателями и boost::shared_ptr или tr1::shared_ptr для наблюдателей. Слабый_птр поможет вам узнать, существует ли объект.
  3. Если вы работаете в каком-то цикле событий, убедитесь, что каждый наблюдатель не уничтожить себя, добавить себя или любого другого в том же вызове. Просто отложите работу, что означает

    SomeObserver::notify()
    {
       main_loop.post(boost::bind(&SomeObserver::someMember,this));
    }
    
0 голосов
/ 18 августа 2014

Я только что написал полный класс наблюдателей. Я включу его после того, как оно будет проверено.

Но мой ответ на ваш вопрос: справиться с делом!

Моя версия позволяет запускать циклы уведомлений внутри циклов уведомлений (они запускаются сразу, воспринимайте это как глубину первой рекурсии), но есть счетчик, так что класс Observable знает, что работает уведомление, и сколько .

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

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

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

0 голосов
/ 06 октября 2013

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

Короче, вот эскиз решения:

  1. Наблюдатель - это одноэлементный файл с ключами для субъектов для регистрации интереса. Поскольку это одноэлементный файл, он всегда существует.
  2. Каждый предмет является производным от общего базового класса. Базовый класс имеет абстрактную виртуальную функцию Notify (...), которая должна быть реализована в производных классах, и деструктор, который удаляет его из Observer (которого он всегда может достичь) при удалении.
  3. Внутри самого Обозревателя, если Detach (...) вызывается во время выполнения Уведомления (...), все отсоединенные субъекты оказываются в списке.
  4. Когда в Observer вызывается Notify (...), он создает временную копию списка тем. Поскольку это повторяется по этому, это сравнивает это с недавно отделенным. Если цель не указана, для нее вызывается уведомление (...). В противном случае оно пропускается.
  5. Notify (...) в Observer также отслеживает глубину обработки каскадных вызовов (A уведомляет B, C, D, а D.Notify (...) инициирует вызов Notify (...) к E и т. д.)

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

0 голосов
/ 20 января 2011

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

Наблюдатель может быть даже удален WHILE Вы пытаетесь вызвать его notify() функцию.

Поэтому я полагаю, вам нужен механизм try / catch .

Блокировка для гарантии того, что набор наблюдателей не изменяется при копировании набора наблюдателей

  lock(observers)
  set<Observer> os = observers.copy();
  unlock(observers)
  for (Observer o: os) {
    try { o.notify() }
    catch (Exception e) {
      print "notification of "+o+"failed:"+e
    }
  }
0 голосов
/ 13 сентября 2010

Это немного медленнее, так как вы копируете коллекцию, но я думаю, что это тоже проще.

class Subject {
public:
   void addObserver(Observer*);
   void remObserver(Observer*);
private:
   void notifyAll();
   std::set<Observer*> observers;
};

void Subject::addObserver(Observer* o) {
  observers.insert(o);
}

void Subject::remObserver(Observer* o) {
  observers.erase(o);
}

void Subject::notifyAll() {
  std::set<Observer*> copy(observers);
  std::set<Observer*>::iterator it = copy.begin();
  while (it != copy.end()) {
    if (observers.find(*it) != observers.end())
      (*it)->notify();
    ++it;
  }
}
...