2010-07-01 10 views
3

Dans mon application, j'ai beaucoup de classes. La plupart de ces classes stockent certaines données, et il est important que les autres modules de mon application soient également 'mis à jour' si le contenu de l'une des classes de données change.Différentes façons d'observer les changements de données

La façon typique de le faire est comme ceci:

void MyDataClass::setMember(double d) 
{ 
m_member = d; 
notifyAllObservers(); 
} 

Cette méthode est assez bon si le membre est pas souvent changé et les besoins des «classes d'observation de d'être mise à jour aussi vite possible.

Une autre façon d'observer les changements est la suivante:

void MyDataClass::setMember(double d) 
{ 
setDirty(); 
m_member = d; 
} 

Ceci est une bonne méthode si le membre est changé plusieurs fois, et l'aspect « cours d'observation » à intervalles réguliers à tous les cas « sales ».

Malheureusement, j'ai un mélange des deux types de membres de données dans mes classes. Certains sont changés pas si souvent (et je peux vivre avec des observateurs normaux), d'autres sont changés plusieurs fois (c'est dans des algorithmes mathématiques complexes) et appellent les observateurs chaque fois que les changements de valeur vont tuer les performances de mon application.

Existe-t-il d'autres astuces pour observer les modifications de données ou des modèles dans lesquels vous pouvez facilement combiner plusieurs méthodes différentes d'observation des modifications de données?

Bien que ce soit une question plutôt indépendante de la langue (et je peux essayer de comprendre des exemples dans d'autres langages), la solution finale devrait fonctionner en C++.

Répondre

4

Les deux méthodes que vous avez décrites couvrent (conceptuellement) les deux aspects, mais je pense que vous n'avez pas suffisamment expliqué leurs avantages et leurs inconvénients.

Il y a un élément dont vous devez être conscient, c'est le facteur population.

  • méthode Push est grand quand il y a beaucoup de notifiants et quelques observateurs
  • méthode de traction est grande quand il y a peu notifiants et de nombreux observateurs

Si vous avez beaucoup notifiants et votre observateur est censé parcourir sur chacun d'eux pour découvrir les 2 ou 3 qui sont dirty ... ça ne marchera pas. D'un autre côté, si vous avez beaucoup d'observateurs et que vous avez besoin de les notifier à chaque mise à jour, vous êtes probablement condamné parce que le simple fait d'itérer sur tous ces éléments va tuer votre performance.

Il y a cependant une possibilité dont vous n'avez pas parlé: combiner les deux approches, avec un autre niveau d'indirection.

  • Poussez tout changement à un GlobalObserver
  • Demandez à chaque contrôle d'observateur pour la GlobalObserver si nécessaire

Ce n'est pas facile cependant, parce que chaque observateur a besoin de se rappeler à quand remonte la dernière fois qu'il a vérifié, être notifié seulement sur les changements qu'il n'a pas encore observés. L'astuce habituelle consiste à utiliser des époques.

Epoch 0  Epoch 1  Epoch 2 
event1  event2  ... 
...   ... 

Chaque observateur se souvient l'époque suivante, il a besoin de lire (quand un observateur souscrit il est donné l'époque actuelle en retour), et lit de cette époque jusqu'à l'actuel pour connaître de tous les événements. Généralement l'époque actuelle n'est pas accessible par un notificateur, vous pouvez par exemple décider de changer d'époque à chaque fois qu'une requête de lecture arrive (si l'époque actuelle n'est pas vide).

La difficulté ici est de savoir quand se débarrasser des époques (quand elles ne sont plus nécessaires). Cela nécessite un certain nombre de références. Rappelez-vous que le GlobalObserver est celui qui renvoie les époques actuelles aux objets. Nous introduisons donc un compteur pour chaque époque, qui compte simplement combien d'observateurs n'ont pas encore observé cette époque (et les suivantes).

  • En vous inscrivant, nous revenons le numéro d'époque et incrémenter le compteur de cette époque
  • sur les sondages, nous décrémenter le compteur de l'époque sondé et retourner le numéro d'époque actuelle et incrémente son compteur
  • Sur désabonnement , on décrémente le compteur de l'époque -> assurez-vous que le destructeur se désinscrit!

Il est également possible de combiner cela avec un délai d'attente, l'enregistrement de la dernière fois que nous avons modifié l'époque (par exemple la création de la prochaine) et de décider que, après un certain laps de temps, nous pouvons jeter (dans ce cas, nous réclamons le compteur et l'ajouter à l'époque suivante).

Notez que le schéma est multithread, car une époque est accessible pour l'écriture (opération push sur une pile) et les autres sont en lecture seule (sauf pour un compteur atomique). Il est possible d'utiliser des opérations sans verrou pour pousser une pile à la condition qu'aucune mémoire ne soit allouée. Il est parfaitement sain de décider de changer d'époque lorsque la pile est complète.

+0

Impressionnant. +1 – Patrick

+0

Nouvelle question sur le même thème (par Patrick): http://stackoverflow.com/questions/3667317/best-way-to-keep-the-user-interface-up-to-date où il remarque que générer simplement un beaucoup d'événements peuvent tuer des spectacles. –

0

Vous avez décrit les deux options de haut niveau disponibles (push vs pull/polling). Il n'y a pas d'autres options que je connais.

4

autres astuces d'observation des données change

Pas vraiment. Vous avez "pousser" et "tirer" des modèles de conception. Il n'y a pas d'autres choix.

A notifyAllObservers est une poussée, l'accès d'attribut ordinaire est une traction.

Je recommande la cohérence. De toute évidence, vous avez une situation où un objet a beaucoup de changements, mais tous les changements ne percolent pas à travers d'autres objets.

Ne soyez pas dérouté par ceci.

Un observateur n'a pas besoin de faire un calcul coûteux simplement parce qu'il a été informé d'un changement.

Je pense que vous devriez avoir des classes comme celle-ci pour gérer les classes "changements fréquents mais demandes lentes".

class PeriodicObserver { 
    bool dirty; 
    public void notification(...) { 
     // save the changed value; do nothing more. Speed matters. 
     this.dirty= True; 
    } 
    public result getMyValue() { 
     if(this.dirty) { 
      // recompute now 
     } 
     return the value 
} 
2

Vous avez une notification push and push.Je pense essayer de cacher les détails dans la mesure du possible, donc au moins le déclarant n'a pas besoin de se soucier de la différence:

class notifier { 
public: 
    virtual void operator()() = 0; 
}; 

class pull_notifier : public notifier { 
    bool dirty; 
public: 
    lazy_notifier() : dirty(false) {} 
    void operator()() { dirty = true; } 
    operator bool() { return dirty; } 
}; 

class push_notifier : public notifier { 
    void (*callback)(); 
public: 
    push_notifier(void (*c)()) : callback(c) {} 
    void operator()() { callback(); } 
}; 

Ensuite, l'observateur peut passer soit un push_notifier ou un pull_notifier comme il voit automatiquement et le mutator n'a pas besoin de se soucier de la différence:

class MyDataClass { 
    notifier &notify; 
    double m_member; 
public: 
    MyDataClass(notifier &n) : n_(n) {} 
    void SetMember(double d) { 
     m_member = d; 
     notify(); 
    } 
}; 

pour le moment je l'ai écrit avec un seul observateur par mutator, mais il est assez simple de changer cela à un vecteur de pointeurs aux objets observateurs pour un mutateur donné si vous en avez besoin de plus. Avec cela, un mutateur donné supportera une combinaison arbitraire de notificateurs push_ et pull_. Si vous êtes sûr qu'un mutateur donné utilisera uniquement pull_notifier s ou push_notifiers, vous pouvez envisager d'utiliser un modèle avec le notificateur en tant que paramètre de modèle (une stratégie) pour éviter la surcharge d'un appel de fonction virtuelle (probablement négligeable pour un push_notifier, mais moins pour un pull_notifier).

Questions connexes