2009-06-19 15 views
31

Ho riscontrato un problema interessante durante l'implementazione del pattern Observer con C++ e STL. Considera questo classico esempio:Problemi nell'implementazione del pattern "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(); } 
} 

Questo esempio può essere trovato in ogni libro su modelli di progettazione. Sfortunatamente, i sistemi di vita reale sono più complessi, quindi ecco il primo problema: alcuni osservatori decidono di aggiungere altri osservatori all'oggetto per essere notificati. Ciò invalida il ciclo "for" e tutti gli iteratori che uso. La soluzione è piuttosto semplice: creo un'istantanea dell'elenco degli osservatori registrati e iteriamo sull'istantanea. L'aggiunta di nuovi osservatori non invalida l'istantanea, quindi tutto sembra ok. Ma ecco che arriva un altro problema: gli osservatori decidono di distruggersi per essere notificati. Ancora peggio, un singolo osservatore può decidere di distruggere tutti gli altri osservatori (sono controllati dagli script) e ciò invalida sia la coda che un'istantanea. Mi trovo a ripetere su puntatori desolocati.

La mia domanda è: come devo gestire le situazioni, quando gli osservatori si uccidono a vicenda? Ci sono dei pattern pronti all'uso? Ho sempre pensato che "Observer" fosse il modello di design più semplice al mondo, ma ora sembra che non sia così facile implementarlo correttamente ...

Grazie a tutti per il vostro interesse. Prendiamo un riassunto delle decisioni:

[1] "Non farlo" Siamo spiacenti, ma è un must. Gli osservatori sono controllati dagli script e sono raccolti dalla spazzatura. Non riesco a controllare la garbage collection per impedire la loro de-assegnazione;

[2] "Usa boost: segnale" La decisione più promettente, ma non posso introdurre una spinta sul progetto, tali decisioni devono essere prese solo dal leader del progetto (stiamo scrivendo sotto Playstation);

[3] "Usa shared__ptr" Ciò impedirà agli osservatori di de-allocare. Alcuni sottosistemi possono fare affidamento sulla pulizia del pool di memoria, quindi non credo di poter utilizzare shared_ptr.

[4] "Postpone dealer dealer" Gli osservatori di coda per la rimozione durante la notifica, quindi utilizzare il secondo ciclo per eliminarli. Sfortunatamente, non posso impedire la deallocazione, quindi utilizzo una sorta di "adattatore" con l'involucro di observer, mantenendo in realtà la lista degli "adattatori". Sul distruttore, gli osservatori non assegnano i loro adattatori, quindi prendo il mio secondo ciclo per distruggere gli adattatori vuoti.

p.s. va bene, che modifico la mia domanda per riassumere tutto il post? Sono in Noob su StackOverflow ...

+1

Buona domanda! Non avevo pensato di utilizzare il modello di osservatore in cui gli osservatori sono autorizzati a creare e distruggere altri osservatori del soggetto. –

+0

Mi piace riassumere le risposte della domanda nella domanda, semplicemente non modificare la domanda originale con delezioni o lettori successivi potrebbero perdere sfumature della domanda originale (non che l'hai fatto, penso che la tua sintesi e la nota che è sintetizzata è superba). – Jay

+0

Hai mai provato nessuno di questi per vedere quale ti è piaciuto di più o hai sentito il migliore? – prolink007

risposta

14

questione molto interessante.

Prova questo:

  1. Change remObserver a null fuori la voce, piuttosto che rimuoverlo (e invalidare le iteratori della lista).
  2. Cambia il loop notifyAll essere:

    per (tutti gli osservatori registrati) {if (osservatore) observer-> notify(); }

  3. Aggiungi un altro ciclo alla fine del notifyAll per rimuovere tutte le voci nulli dall'elenco osservatore

+0

Questo mi sembra il più adatto perché non posso usare segnali o shared_ptr. Anche se due anelli invece di uno possono portare a penalità per le prestazioni, penso che sia il modo più semplice. Grazie! – SadSido

+1

Suppongo che se sei * veramente * preoccupato per performace potresti aggiungere un flag "sporco" di qualche tipo in modo che l'ultimo ciclo non debba essere attivato a meno che non ci sia qualcosa da rimuovere. Tuttavia, non mi preoccuperei se non c'è un problema di prestazioni verificato e misurato con questo ciclo. Ottimizzazione prematura e tutto il resto. –

0

Come utilizzare un elenco collegato nel ciclo for?

+0

Sto usando std :: list adesso. Ma ciò non risolve il problema: le operazioni di rimozione continuano a invalidare l'iterazione sull'elenco. Dovrei scrivere qualcosa di più complesso nel mio ciclo "for"? – SadSido

+0

O (controlla il puntatore "successivo" su NULL e nel tuo metodo di rimozione, assicurati che sia NULL) o usa boost :: signal come suggerito da Todd Gardner. –

0

Se il programma è multithread, potrebbe essere necessario utilizzare un blocco qui.

In ogni caso, dalla tua descrizione sembra che il problema non sia la concorrenza (multi-thrading) ma piuttosto le mutazioni indotte dalla chiamata Observer :: notify(). Se questo è il caso, puoi risolvere il problema utilizzando un vettore e attraversandolo tramite un indice piuttosto che un iteratore.

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

L'uso del vettore non è una soluzione, in quanto lascia alcuni degli osservatori non processati ... Hai i = 3, l'osservatore # 3 si uccide, il vettore è spostato, tu incrementa i e ... uno degli osservatori è lasciato non notificato :) – SadSido

+0

Se ti fossi attenuto al caveat multi-thread, avrei votato. Sono molto sospettoso dalla descrizione che ci sia qualche threading in corso, il che significa che ci sono passibili di condizioni di gara. –

7

Personalmente, io uso boost::signals per implementare i miei osservatori; Dovrò controllare, ma credo che gestisca gli scenari di cui sopra (modificato: trovato, vedi "When can disconnections occur"). Semplifica l'implementazione, e non si basa sulla creazione di classe personalizzata:

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 per boost :: signal È lo stesso metodo con cui ho implementato gli osservatori e semplifica la vita –

6

Un uomo va dal dottore e dice: "Doc quando alzo il braccio come questo fa male veramente male!" Il dottore dice "Non farlo".

La soluzione più semplice è quella di lavorare con il vostro team e dire loro di non farlo. Se gli osservatori "hanno davvero bisogno" di uccidere se stessi, o tutti gli osservatori, quindi pianificare l'azione per quando la notifica è finita. O, meglio ancora, cambia la funzione di remObserver per sapere se c'è un processo di notifica che accade e basta accodare le rimozioni per quando tutto è fatto.

+0

Questo è il metodo che ho usato con un pattern di osservatore modificato. Se la rimozione deve essere eseguita immediatamente anche se sei bloccato complicando la tua logica di gestione dei casi d'angolo. –

+1

Devo ammettere che quello fu il mio primo impulso. Tuttavia, penso che questo atteggiamento mostri esattamente cosa c'è di sbagliato nei "modelli". Lo sviluppo del software non consiste nello spezzare insieme i mattoni perfetti. Si tratta di risolvere i problemi. –

+2

@ T.E.D. Non potrei essere più d'accordo sul fatto che il modello non sia una Sacra Scrittura che non dovrebbe mai, mai essere macchiata dalle modifiche. Ma ho spesso riscontrato che gli sviluppatori si sporgono un po 'troppo nella direzione di complicare le cose con soluzioni rapide e spesso non fanno un passo indietro e chiedono quale sia il vero problema. Potrebbe essere che stanno usando il pattern Observer in una situazione per la quale non è stato progettato. – Lee

4

Il problema è quello della proprietà. Potresti usare puntatori intelligenti, ad esempio le classi boost::shared_ptr e boost::weak_ptr, per estendere la durata dei tuoi osservatori oltre il punto di "de-assegnazione".

3

Ci sono diverse soluzioni per questo problema:

  1. Usa boost::signal permette rimozione connessione automatica quando l'oggetto distrutto. Ma si dovrebbe essere molto attenti con sicurezza filo
  2. Usa boost::weak_ptr o tr1::weak_ptr per managment degli osservatori, e boost::shared_ptr o tr1::shared_ptr per gli osservatori loro auto - riferimento conteggio sarebbero aiuto per invalidare gli oggetti, weak_ptr lasceremmo conoscere se oggetto esiste.
  3. Se si sta eseguendo un ciclo di eventi, assicurarsi che ciascun osservatore non si blocchi , aggiunga se stesso o qualsiasi altro nella stessa chiamata. Basta rinviare il lavoro, il che significa

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

ne dite di avere un iteratore membro chiamato current (inizializzato per essere il end iteratore).Poi

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

Questa strategia può essere facilmente sconfitta, quando l'osservatore decide di riattivare notifyAll sul Subject. Quanti membri "attuali" dovrebbero avere allora? Bene, immagino sia un problema teorico - dovremmo davvero limitare i nostri osservatori. Grazie per la risposta! – SadSido

0

Definire e utilizzare un duty iteratore pesante sul contenitore di notificanti che è resistente alla cancellazione (per esempio, annullamento dei fuori, come detto in precedenza) e in grado di gestire aggiunta (ad esempio aggiungendo)

altra mano se vuoi imporre mantenere il contenitore const durante la notifica, dichiarare notifyAll e il contenitore che viene iterato come const.

+0

L'iteratore personalizzato, che non può essere invalidato, è anche una buona decisione. Ci vorrà del tempo per implementarlo, ma sarà molto utile. Grazie! – SadSido

5

Ecco una variazione sull'idea T.E.D. già presentato.

Finché remObserver può su null una voce invece di rimuovere immediatamente, allora si potrebbe implementare notifyAll come:

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

Questo evita la necessità di un secondo ciclo di pulizia. Tuttavia, ciò significa che se qualche particolare chiamata a notify() attiva la rimozione di se stesso o un osservatore situato in precedenza nell'elenco, la rimozione effettiva dell'elemento di lista verrà posticipata alla successiva notificaAll(). Ma finché tutte le funzioni che operano sull'elenco si prendono cura di controllare le voci nulle quando appropriato, allora questo non dovrebbe essere un problema.

+0

Sì, dovrebbe farlo. –

0

Questo è un po 'più lento dal momento che si sta copiando la raccolta, ma penso che sia anche più semplice.

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

Il problema che vedo è controllare "trova (* it)" per ogni elemento, piuttosto che copiare una raccolta. Aumenta la complessità e può essere doloroso quando hai molti osservatori. Comunque, l'idea è bella, grazie! – SadSido

0

Non si può mai evitare di rimuovere gli osservatori durante l'iterazione.

L'osservatore può anche essere rimosso WHILE si sta tentando di chiamare la sua funzione notify().

Pertanto, suppongo che sia necessario un meccanismo try/catch.

Il blocco è quello di garantire observerset non è changedd durante la copia del set di osservatori

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

ero alla ricerca di una soluzione a questo problema quando mi sono imbattuto in questo articolo qualche mese fa. E mi ha fatto pensare alla soluzione e penso di avere uno che non si basa su spinta, puntatori intelligenti, ecc

In breve, qui è il disegno della soluzione:

  1. The Observer è un singleton con le chiavi per i soggetti per registrare interesse. Poiché è un singleton, esiste sempre.
  2. Ogni soggetto deriva da una classe di base comune. La classe base ha una funzione virtuale astratta Notifica (...) che deve essere implementata nelle classi derivate e un distruttore che la rimuove dall'Observer (che può sempre raggiungere) quando viene cancellata.
  3. All'interno dello stesso Observer, se Detach (...) viene chiamato mentre è in corso una notifica (...), qualsiasi oggetto staccato finisce in una lista.
  4. Quando Notify (...) viene chiamato su Observer, crea una copia temporanea dell'elenco Subject. Mentre scorre su di esso, lo confronta con il distaccato di recente. Se il bersaglio non è su di esso, Notifica (...) viene richiamato sul bersaglio. Altrimenti, viene saltato.
  5. Notifica (...) nell'Observer tiene traccia della profondità per gestire chiamate in cascata (A notifica B, C, D e D.Notify (...) attiva una chiamata di notifica (...) a E, ecc.)

Questo sembra funzionare bene. La soluzione è pubblicata sul web here insieme al codice sorgente. Questo è un design relativamente nuovo, quindi qualsiasi feedback è molto apprezzato.

0

Ho appena scritto una classe di osservatori completa. Lo includerò dopo che è stato testato.

Ma la mia risposta alla tua domanda è: gestisci il caso!

La mia versione consente l'attivazione dei loop di notifica all'interno dei loop di notifica (vengono eseguiti immediatamente, pensate a questo come prima ricorsione di profondità), ma c'è un contatore in modo che la classe Observable sappia che è in esecuzione una notifica e quanti in profondità.

Se un osservatore viene cancellato, il suo distruttore indica a tutti gli osservabili che è stato abbonato alla distruzione. Se non sono in un ciclo di notifica in cui è presente l'osservatore, tale osservabile viene eliminato da una coppia < std :: < Observer *, int > > per quell'evento se è in un ciclo, quindi la sua voce nel l'elenco viene invalidato e un comando viene inserito in una coda che verrà eseguita quando il contatore delle notifiche è a zero. Quel comando cancellerà la voce invalidata.

Quindi, in pratica, se non è possibile cancellare in modo sicuro (perché potrebbe esserci un iteratore che contiene la voce che ti avviserà), allora si annulla la voce anziché eliminarla.

Così come tutti i sistemi contemporanei di non attesa, la regola è: gestire il caso se non si è bloccati, ma se si è in coda il lavoro e chi tiene il lucchetto eseguirà il lavoro quando rilascerà il blocco .