c++ - pattern - Problemas al implementar el patrón "Observer"
strategy pattern (14)
He encontrado un problema interesante al implementar el patrón Observer con C ++ y STL. Considera este ejemplo clásico:
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 cada libro 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 decisiones:
[1] "No lo hagas" Lo siento, 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 solo 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 desasignación de observadores" Ponga en cola a los observadores para eliminarlos mientras lo 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.
¿Está bien que edite mi pregunta para resumir toda la publicación? Soy novato en StackOverflow ...
¿Qué le parece usar una lista enlazada en su bucle for
?
¿Qué tal tener un iterador miembro llamado current
(inicializado para ser el iterador end
). 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();
}
}
Acabo de escribir una clase completa de observadores. Lo incluiré después de que haya sido probado.
Pero mi respuesta a tu pregunta es: ¡maneja el caso!
Mi versión permite que se activen bucles de notificación dentro de bucles de notificación (se ejecutan inmediatamente, piense en esto como la primera recursión en profundidad), pero hay un contador para que la clase Observable sepa que se está ejecutando una notificación y la profundidad.
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 el observador está, entonces ese observable se borra de un std :: list <pair <Observer *, int >> para ese evento si está en un bucle, entonces 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.
Entonces, básicamente, si no puede eliminar de manera 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 de no-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 libere el bloqueo.
Aquí hay una variación de la idea que TED ya presentó.
Siempre que remObserver pueda anular una entrada en lugar de eliminarla inmediatamente, entonces podría implementar 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.
Defina y use un iterador de trabajo pesado sobre el contenedor de notificadores que sea resistente a la eliminación (por ejemplo, anulación, como se mencionó anteriormente) y pueda manejar la adición (por ejemplo, agregar)
Por otro lado, si desea exigir el mantenimiento del contenedor const durante la notificación, declare notifyAll y el contenedor iterado como const.
El problema es el de la propiedad. Podría usar punteros inteligentes, por ejemplo las clases boost::shared_ptr
y boost::weak_ptr
, para extender la vida útil de sus observadores más allá del punto de " boost::weak_ptr
".
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 una que no depende de impulso, punteros inteligentes, etc.
En resumen, aquí está el boceto de la solución:
- The Observer es un singleton con claves para que los Sujetos registren interés. Debido a que es un singleton, siempre existe.
- 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.
- Dentro del propio Observer, si se llama Detach (...) mientras está en curso un Notify (...), los sujetos separados terminan en una lista.
- 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.
- 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 de 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.
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;
}
}
Hay varias soluciones para este problema:
- Use
boost::signal
que permite la eliminación automática de la conexión cuando el objeto se destruye. Pero debes tener mucho cuidado con la seguridad de los hilos - Utilice
boost::weak_ptr
otr1::weak_ptr
para la gestión de los observadores, yboost::shared_ptr
otr1::shared_ptr
para los observadores. El recuento de referencia automática le ayudaría a invalidar los objetos, weak_ptr le avisará si existe un objeto. Si está ejecutando un ciclo de eventos, 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. Solo posponga el trabajo, es decir
SomeObserver::notify() { main_loop.post(boost::bind(&SomeObserver::someMember,this)); }
Nunca se puede evitar que los observadores sean eliminados mientras se itera.
El observador incluso puede eliminarse WHILE
intenta llamar a su función notify()
.
Por lo tanto, supongo que necesitas un mecanismo de prueba / captura .
El bloqueo es para asegurar que el conjunto de observadores no se cambie mientras se copia 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
}
}
Personalmente, uso boost::signals para implementar mis observadores; Tendré que verificarlo, pero creo que maneja los escenarios anteriores ( editado : lo encontré, consulte "¿Cuándo pueden ocurrir las desconexiones?" ). Simplifica su implementación y no se basa en crear 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.
}
Si su programa es de subprocesos múltiples, es posible que necesite usar algún bloqueo aquí.
De todos modos, a partir 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();
Tema muy interesante.
Prueba esto:
- Cambie remObserver para anular la entrada, en lugar de simplemente eliminarla (e invalidar los iteradores de la lista).
Cambie su notifyAll loop para que sea:
para (todos los observadores registrados) {si (observador) observador-> notificar (); }
Agregue otro bucle al final de notifyAll para eliminar todas las entradas nulas de su lista de observadores
Un hombre acude al médico y le dice: "¡Doc, cuando levanto el brazo de esta manera me duele mucho!" 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.