3

Je suis en train de refactoriser une application dans laquelle les classes peuvent appeler des observateurs si leur état change. Cela signifie que les observateurs sont appelés à chaque fois:Comment protéger la cohérence de la structure de données lors de l'appel de la logique externe via des observateurs?

  • les données dans une instance de la classe change
  • nouvelles instances de la classe sont créés
  • instances de la classe sont supprimés

C'est cette dernier cas qui me fait m'inquiéter.

Supposons que ma classe est Livre. Les observateurs sont stockés dans une classe appelée BookManager (le BookManager conserve également une liste de tous les livres). Cela signifie que nous avons ceci:

class Book 
    { 
    ... 
    }; 

class BookManager 
    { 
    private: 
     std::list<Book *> m_books; 
     std::list<IObserver *> m_observers; 
    }; 

Si un livre est supprimé (retiré de la liste et supprimé de la mémoire), les observateurs sont appelés:

void BookManager::removeBook (Book *book) 
    { 
    m_books.remove(book); 
    for (auto it=m_observers.cbegin();it!=m_observers.cend();++it) (*it)->onRemove(book *); 
    delete book; 
    } 

Le problème est que je ne contrôle pas la logique dans les observateurs. Les observateurs peuvent être livrés par un plugin, par un code écrit par les développeurs chez le client.

Ainsi, bien que je peux écrire du code comme ceci (et je vous assurer d'obtenir le suivant dans la liste au cas où l'instance est supprimée):

auto itNext; 
for (auto it=m_books.begin();it!=m_books.end();it=itNext) 
    { 
    itNext = it: 
    ++itNext; 
    Book *book = *it; 
    if (book->getAuthor()==string("Tolkien")) 
    { 
    removeBook(book); 
    } 
    } 

Il y a toujours la possibilité d'un observateur supprimant d'autres livres de la liste ainsi:

void MyObserver::onRemove (Book *book) 
    { 
    if (book->getAuthor()==string("Tolkien")) 
     { 
     removeAllBooksFromAuthor("Carl Sagan"); 
     } 
    } 

Dans ce cas, si la liste contient un livre de Tolkien, suivi d'un livre de Carl Sagan, la boucle qui supprime tous les livres de Tolkien vont probablement tomber en panne depuis le prochain iterator (itNext) est devenu invalide.

Le problème illustré peut également apparaître dans d'autres situations, mais le problème de suppression est le plus grave, car il peut facilement planter l'application.

Je pourrais résoudre le problème en m'assurant que dans l'application j'obtiens d'abord toutes les instances que je veux supprimer, placez-les dans un second conteneur, puis bouclez le second conteneur et supprimez les instances, mais comme il y a toujours le risque qu'un observateur supprime explicitement d'autres instances qui figuraient déjà sur ma liste à effacer, je dois mettre des observateurs explicites pour garder cette deuxième copie à jour également.

Exiger également que tous les codes d'application fassent des copies de listes chaque fois qu'ils veulent parcourir un conteneur tout en appelant des observateurs (directement ou indirectement) rend l'écriture de code d'application beaucoup plus difficile. Y at-il des modèles [de conception] qui peuvent être utilisés pour résoudre ce genre de problème? De préférence, pas d'approche de pointeur partagé puisque je ne peux pas garantir que toute l'application utilise des pointeurs partagés pour accéder aux instances.

+3

Votre problème semble être plus profond: vos clients ont un accès direct aux itérateurs de la collection et, en même temps, vous voulez que la collection ait un comportement complexe qui ne préserve pas les itérateurs. Je voudrais isoler les API pour les opérations de mutateur, en indiquant explicitement que tous les itérateurs détenus par les clients deviennent invalides. –

Répondre

1

Le problème de base ici est que la collection de livres est modifiée (sans connaissance par l'application) pendant que l'application itère sur cette même collection.

Deux façons de régler ce sont:

  1. mettent en place un mécanisme de verrouillage sur la collecte. L'application prend un verrou sur la collection et tant que ce verrou existe, aucune action modifiant la collection (ajouter/supprimer des livres) n'est autorisée. Si un observateur doit effectuer une modification pendant cette période, il devra s'en souvenir et effectuer la modification lorsqu'il est averti de la libération du verrou.
  2. Utilisez votre propre classe d'itérateur, qui peut gérer le changement de collection en dessous. Par exemple, en utilisant le modèle Observer pour informer tous les itérateurs qu'un élément est sur le point d'être supprimé. Si cela invalide l'itérateur, il peut avancer lui-même en interne pour rester valide.

Si la boucle de suppression fait partie de BookManager, alors il pourrait être restructurés comme ceci:

  • Boucle sur la collecte et déplacer tous les éléments qui doivent être supprimés à une procédure distincte, locale , "supprimé" collection. A ce stade, informez uniquement les itérateurs (si vous avez implémenté l'option 2 ci-dessus) des modifications apportées à la collection.
  • Parcourez la collection 'deleted' et informez les observateurs de chaque suppression. Si un observateur essaie de supprimer d'autres éléments, ce n'est pas un problème tant que la suppression d'éléments inexistants n'est pas une erreur fatale.
  • Effectuez un nettoyage de la mémoire sur les éléments de la collection 'deleted'.

Quelque chose de similaire pourrait être fait pour d'autres modifications multi-éléments dans BookManager.

Questions connexes