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.
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. –