2009-06-19 10 views
31

He encontrado un problema interesante al implementar el patrón Observer con C++ y STL. Considere este ejemplo clásico:Problemas al implementar el patrón "Observer"

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

Este ejemplo se puede encontrar en todos los libros sobre patrones de diseño. Desafortunadamente, los sistemas de la vida real son más complejos, así que este es el primer problema: algunos observadores deciden agregar otros observadores al Sujeto al ser notificados. Esto invalida el ciclo "for" y todos los iteradores que uso. La solución es bastante fácil: hago una instantánea de la lista de observadores registrados e itero sobre la instantánea. Agregar nuevos observadores no invalida la instantánea, por lo que todo parece estar bien. Pero aquí viene otro problema: los observadores deciden destruirse a sí mismos al ser notificados. Peor aún, un solo observador puede decidir destruir a todos los demás observadores (están controlados a partir de los scripts) y eso invalida tanto la cola como una instantánea. Me encuentro iterando sobre punteros desasignados.

Mi pregunta es ¿cómo debo manejar las situaciones, cuando los observadores se matan entre sí? ¿Hay algún patrón listo para usar? Siempre pensé que "Observer" es el patrón de diseño más fácil del mundo, pero ahora parece que no es tan fácil implementarlo correctamente ...

Gracias a todos por su interés. Tengamos un resumen de las decisiones:

[1] "No lo hagas" Lo sentimos, pero es obligatorio. Los observadores se controlan desde los scripts y son recolectados. No puedo controlar la recolección de basura para evitar su desasignación;

[2] "Use boost :: signal" La decisión más prometedora, pero no puedo introducir impulso en el proyecto, tales decisiones deben tomarlas únicamente el líder del proyecto (estamos escribiendo bajo Playstation);

[3] "Usar shared__ptr" Eso evitará que los observadores se desasignen. Algunos subsistemas pueden confiar en la limpieza del grupo de memoria, por lo que no creo que pueda usar shared_ptr.

[4] "Posponer la desasignación de observador" Poner en cola los observadores para su eliminación mientras notifica, luego use el segundo ciclo para eliminarlos. Desafortunadamente, no puedo evitar la desasignación, así que utilizo un truco para envolver al observador con algún tipo de "adaptador", manteniendo en realidad la lista de "adaptadores". En destructor, los observadores eliminan la asignación de sus adaptadores, luego tomo mi segundo ciclo para destruir los adaptadores vacíos.

p.s. ¿Está bien, que edite mi pregunta para resumir toda la publicación? Soy novato en StackOverflow ...

+1

¡Buena pregunta! No había considerado usar el patrón de observadores donde los observadores pueden crear y destruir otros observadores del sujeto. –

+0

Me gusta resumir las respuestas de la pregunta en la pregunta, simplemente no altere la pregunta original con eliminaciones o lectores posteriores pueden perderse los matices de la pregunta original (no es que lo haya hecho, creo que su resumen y señalar que su resumen es excelente) – Jay

+0

¿Alguna vez ha probado alguno de estos para ver cuál le gustó más o si se sintió mejor? – prolink007

Respuesta

14

tema muy interesante.

Prueba esto:

  1. Cambio remObserver para anular la entrada, en lugar de quitarla (e invalidando los iteradores lista).
  2. Cambiar el bucle notifyAll ser:

    para (todos los observadores registrados) {if (observador) del observador> notificar(); }

  3. Agregar otro bucle al final de notifyAll para eliminar todas las entradas nulas de su lista de observación

+0

Ese sonido es el más adecuado para mí ya que no puedo usar señales o shared_ptr. Aunque dos bucles en lugar de uno pueden llevar a una penalización de rendimiento, creo que es la forma más fácil. ¡Gracias! – SadSido

+1

Supongo que si estás * realmente * preocupado por el rendimiento podrías agregar una bandera "sucia" de algún tipo para que el último ciclo no tenga que activarse a menos que haya algo que eliminar. Sin embargo, no me molestaría a menos que haya un problema de rendimiento medido y verificado con este ciclo. Optimización prematura y todo eso. –

0

¿Qué le parece usar una lista vinculada en su bucle for?

+0

Estoy usando std :: list ahora mismo. Pero eso no soluciona el problema: las operaciones de eliminación aún invalidan la iteración sobre la lista. ¿Debo escribir algo más complejo dentro de mi ciclo "para"? – SadSido

+0

O eso (verifique el puntero "siguiente" contra NULL y en su método de eliminación, asegúrese de que sea NULL) o utilice boost :: signal como lo sugiere Todd Gardner. –

0

Si su programa es de subprocesos múltiples, es posible que necesite usar algún bloqueo aquí.

De todos modos, de su descripción parece que el problema no es la concurrencia (multi-thrading), sino las mutaciones inducidas por la llamada Observer :: notify(). Si este es el caso, entonces puede resolver el problema utilizando un vector y atravesándolo a través de un índice en lugar de un iterador.

for(int i = 0; i < observers.size(); ++i) 
    observers[i]->notify(); 
+0

El uso del vector no es una solución, ya que deja a algunos de los observadores sin procesar ... Tienes i = 3, el observador # 3 se mata, el vector se desplaza, aumentas i y ... uno de los observadores queda sin notificar :) – SadSido

+0

Si se hubiera quedado con la advertencia de subprocesos múltiples, habría votado esto. Soy muy sospechoso por la descripción de que hay algunos hilos en proceso, lo que significa que es probable que existan condiciones de carrera. –

7

Personalmente, yo uso boost::signals para poner en práctica mis observadores; Tendré que verificarlo, pero creo que maneja los escenarios anteriores (editado: lo encontré, vea "When can disconnections occur"). Se simplifica su aplicación, y que no se basa en la creación de clases personalizadas:

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. 
} 
+0

+1 para boost :: signal Es la misma forma en que implementé observadores y hace la vida mucho más simple –

6

Un hombre va al médico y le dice: "Doc cuando levanto mi brazo como esto le duele muy mal!" El médico dice: "No hagas eso".

La solución más simple es trabajar con su equipo y decirles que no lo hagan. Si los observadores "realmente necesitan" suicidarse, o todos los observadores, programe la acción para cuando finalice la notificación. O, mejor aún, cambie la función remObserver para saber si hay un proceso de notificación y simplemente ponga en cola las eliminaciones para cuando todo haya terminado.

+0

Este es el método que he usado con un patrón de observador modificado. Si la eliminación tiene que realizarse de inmediato, está bloqueado, lo que complica su manejo lógico de los casos de esquina. –

+1

Es cierto que ese fue mi primer impulso. Sin embargo, creo que esta actitud muestra precisamente lo que está mal con los "patrones". El desarrollo de software no se trata de unir ladrillos perfectos. Se trata de resolver problemas. –

+2

@ T.E.D. No podría estar más de acuerdo en que el patrón no es una Sagrada Escritura que nunca debería mancharse con modificaciones. Pero a menudo me he dado cuenta de que los desarrolladores se inclinan un poco demasiado en la dirección de complicar las cosas con soluciones rápidas y, a menudo, no dan un paso atrás y preguntan cuál es el verdadero problema. Podría ser que estén usando el patrón Observer en una situación para la que no fue diseñado. – Lee

4

El problema es el de la propiedad. Puede utilizar punteros inteligentes, por ejemplo, las clases boost::shared_ptr y boost::weak_ptr, para extender la vida útil de sus observadores después del punto de "desasignación".

3

Hay varias soluciones para este problema:

  1. Use boost::signal que permite la extracción de conexión automática cuando el objeto destruido. Pero debe ser muy cuidado con la seguridad hilo
  2. Uso boost::weak_ptr o tr1::weak_ptr de mando de los observadores, y boost::shared_ptr o tr1::shared_ptr de observadores los auto - Referencia conteo se que ayuda para invalidar objetos, weak_ptr le permiten saber si el objeto existe
  3. Si está ejecutando algún bucle de evento, asegúrese de que cada observador no se destruya a sí mismo, ni se agregue a sí mismo ni a ningún otro en la misma llamada. Simplemente posponer el trabajo, lo que significa

    SomeObserver::notify() 
    { 
        main_loop.post(boost::bind(&SomeObserver::someMember,this)); 
    } 
    
0

¿Qué hay de tener un miembro de iterador llamada current (inicializado a ser el end iterador).Entonces

void remObserver(Observer* obs) 
{ 
    list<Observer*>::iterator i = observers.find(obs); 
    if (i == current) { ++current; } 
    observers.erase(i); 
} 

void notifyAll() 
{ 
    current = observers.begin(); 
    while (current != observers.end()) 
    { 
     // it's important that current is incremented before notify is called 
     Observer* obs = *current++; 
     obs->notify(); 
    } 
} 
+0

Esta estrategia puede ser fácilmente derrotada, cuando el observador decide reactivar notifyAll on Subject. ¿Cuántos miembros "actuales" deberían tener sujeto entonces? Bueno, supongo que es un problema teórico: realmente deberíamos restringir a nuestros observadores. ¡Gracias por la respuesta! – SadSido

0

definir y utilizar un iterador resistente sobre el recipiente de notificadores que es resistente a la eliminación (por ejemplo, anulando a cabo, como se ha mencionado anteriormente) y puede manejar adición (por ejemplo, añadiendo)

Por otro De la mano, si desea hacer cumplir la conservación del contenedor const durante la notificación, declare notifyAll y el contenedor que se itera como const.

+0

El iterador personalizado, que no se puede invalidar, también es una buena decisión. Llevará algún tiempo implementarlo, sin embargo, será muy útil. ¡Gracias! – SadSido

5

Aquí hay una variación de la idea T.E.D. ya presentado.

Mientras remObserver puede anular una entrada en lugar de eliminar de inmediato, entonces se podría aplicar notifyAll como:

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

Esto evita la necesidad de un segundo bucle de limpieza. Sin embargo, esto significa que si alguna llamada particular de notify() desencadena la eliminación de sí mismo o de un observador ubicado anteriormente en la lista, entonces la eliminación real del elemento de lista se aplazará hasta el próximo notifyAll(). Pero siempre que las funciones que operan en la lista tengan cuidado de verificar si hay entradas nulas cuando corresponda, entonces esto no debería ser un problema.

+0

Sí, eso debería hacerlo. –

0

Esto es un poco más lento ya que está copiando la colección, pero creo que es más simple también.

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; 
    } 
}
+0

El problema que veo es verificar "find (* it)" para cada elemento, en lugar de copiar una colección. Aumenta la complejidad y puede ser doloroso cuando tienes muchos observadores. De todos modos, la idea es genial, ¡gracias! – SadSido

0

Nunca se puede evitar que se eliminen los observadores al iterar.

El observador incluso se puede eliminar WHILE que está tratando de llamar a su función notify().

Por lo tanto, supongo que necesita un try/catch mecanismo.

El bloqueo es asegurar observerset no se changedd al copiar el conjunto de observadores

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

que estaba buscando una solución a este problema cuando me encontré con este artículo hace unos meses. Me hizo pensar en la solución y creo que tengo uno que no depende de impulso, punteros inteligentes, etc.

En resumen, aquí está el boceto de la solución:

  1. El Observador es un singleton con claves para que los Sujetos registren interés. Debido a que es un singleton, siempre existe.
  2. Cada sujeto se deriva de una clase base común. La clase base tiene una función virtual abstracta Notify (...) que debe implementarse en clases derivadas, y un destructor que la elimina del Observer (al que siempre puede acceder) cuando se elimina.
  3. Dentro del Observer mismo, si se llama Detach (...) mientras está en curso un Notify (...), cualquier Subject separado terminará en una lista.
  4. Cuando se llama a Notify (...) en el Observer, crea una copia temporal de la lista de temas. Mientras lo itera, lo compara con el recientemente desapegado. Si el objetivo no está en él, se llama a Notify (...) en el objetivo. De lo contrario, se salta.
  5. Notify (...) en el Observer también realiza un seguimiento de la profundidad para manejar llamadas en cascada (A notifica B, C, D y D.Notify (...) activa una llamada Notify (...) a E, etc.)

Esto parece funcionar bien. La solución se publica en la web here junto con el código fuente. Este es un diseño relativamente nuevo, por lo que cualquier comentario es muy apreciado.

0

Acabo de escribir una clase de observador completa. Lo incluiré después de que haya sido probado.

Pero mi respuesta a su pregunta es: ¡maneje el caso!

Mi versión permite que se activen bucles de notificación dentro de notificar bucles (se ejecutan inmediatamente, piense en esto como primera repetición de profundidad), pero hay un contador para que la clase Observable sepa que se está ejecutando una notificación y cuántos profundo.

Si un observador se elimina, su destructor le dice a todos los observables que está suscrito acerca de la destrucción. Si no están en un bucle de notificación en el que se encuentra el observador, ese observable se elimina de un par < std :: list < Observer *, int > > para ese evento si está en un bucle, luego su entrada en el la lista se invalida y un comando se inserta en una cola que se ejecutará cuando el contador de notificaciones se reduzca a cero. Ese comando eliminará la entrada invalidada.

Básicamente, si no puede eliminar de forma segura (porque puede haber un iterador que contenga la entrada que le notificará), entonces invalidará la entrada en lugar de eliminarla.

Así que, como todos los sistemas concurrentes sin espera, la regla es: maneje el caso si no está bloqueado, pero si lo hace, ponga en cola el trabajo y quien tenga el bloqueo hará el trabajo cuando suelte el candado .

Cuestiones relacionadas