Problèmes avec le modèle Singleton

J’ai lu des articles sur Singleton depuis quelques jours. La perception générale est que les scénarios où il est requirejs sont très rares (voire rares), probablement parce qu’ils ont leurs propres problèmes, tels que:

  • Dans un environnement de récupération de place, il peut s’agir d’un problème lié à la gestion de la mémoire.
  • Dans un environnement multithread, il peut provoquer des goulots d’étranglement et introduire des problèmes de synchronisation.
  • Maux de tête de tester la préséance.

Je commence à avoir les idées derrière ces problèmes, mais je ne suis pas tout à fait sûr de ces préoccupations. Comme dans le cas d’un problème de récupération de place, l’utilisation de static dans l’implémentation singleton (qui est inhérente au pattern), est-ce le problème? Puisque cela signifierait que l’instance statique durera jusqu’à l’application. Est-ce quelque chose qui dégrade la gestion de la mémoire (cela signifie simplement que la mémoire allouée au motif singleton ne sera pas libérée)?

Bien sûr, dans une configuration multithread, avoir tous les threads en conflit pour l’instance singleton serait un goulot d’étranglement. Mais comment l’utilisation de ce modèle pose-t-elle des problèmes de synchronisation (nous pouvons certainement utiliser un mutex ou quelque chose du genre pour synchroniser l’access).

Du sharepoint vue des tests (unit?), Les singletons utilisant des méthodes statiques (difficiles à simuler ou à écraser), ils peuvent poser problème. Pas sûr de ça. Quelqu’un peut-il s’il vous plaît élaborer sur cette préoccupation de test?

Merci.

Dans un environnement de récupération de mémoire, il peut s’agir d’un problème lié à la gestion de la mémoire.

Dans les implémentations de singleton typiques, une fois que vous avez créé le singleton, vous ne pouvez jamais le détruire. Cette nature non destructive est parfois acceptable lorsque le singleton est petit. Cependant, si le singleton est massif, vous utilisez inutilement plus de mémoire que vous n’auriez dû.

C’est un problème plus important dans les langues où vous avez un ramasse-miettes (comme Java, Python, etc.) car le ramasse-miettes pense toujours que le singleton est nécessaire. En C ++, vous pouvez sortingcher en delete le pointeur. Cependant, cela ouvre son propre bidon de vers car il est supposé être un singleton, mais en le supprimant, vous en créez un second.

Dans la plupart des cas, cette utilisation excessive de la mémoire ne dégrade pas les performances de la mémoire, mais elle peut être considérée comme une fuite de mémoire. Avec un grand singleton, vous perdez de la mémoire sur l’ordinateur ou le périphérique de votre utilisateur. (Vous pouvez vous heurter à une fragmentation de la mémoire si vous allouez un singleton énorme, mais cela ne pose généralement aucun problème).

Dans un environnement multithread, il peut provoquer des goulots d’étranglement et introduire des problèmes de synchronisation.

Si chaque thread accède au même object et que vous utilisez un mutex, chaque thread doit attendre qu’un autre ait déverrouillé le singleton. Et si les threads dépendent beaucoup du singleton, vous allez dégrader les performances à un environnement à un seul thread car un thread passe la majeure partie de sa vie à attendre.

Cependant, si votre domaine d’application le permet, vous pouvez créer un object pour chaque thread – de cette manière, le thread n’attend pas le temps et le fait.

Maux de tête de tester la préséance.

Notamment, le constructeur d’un singleton ne peut être testé qu’une seule fois. Vous devez créer une suite de tests entièrement nouvelle pour tester à nouveau le constructeur. Cela ne pose pas de problème si votre constructeur ne prend aucun paramètre, mais une fois que vous avez accepté un paramètre, vous ne pouvez plus appliquer le test d’unité.

De plus, vous ne pouvez pas exclure le singleton de la manière la plus efficace et votre utilisation d’objects fictifs devient difficile à utiliser (il y a des moyens de contourner cela, mais c’est plus difficile que cela en vaut la peine). Continuez à lire pour plus d’informations à ce sujet …

(Et cela conduit à une mauvaise conception aussi!)

Les singletons sont aussi un signe de mauvaise conception. Certains programmeurs veulent faire de leur classe de firebase database un singleton. “Notre application n’utilisera jamais deux bases de données”, pensent-ils généralement. Mais, à un moment donné, il peut être judicieux d’utiliser deux bases de données ou les tests unitaires que vous souhaitez utiliser avec deux bases de données SQLite différentes. Si vous avez utilisé un singleton, vous devrez apporter de sérieux changements à votre application. Mais si vous avez utilisé des objects ordinaires dès le début, vous pouvez tirer parti de la POO pour que votre tâche soit effectuée efficacement et à temps.

La plupart des cas de singleton sont le résultat du paresseux du programmeur. Ils ne souhaitent pas faire passer un object (par exemple, un object de firebase database) à un ensemble de méthodes, ils créent donc un singleton que chaque méthode utilise comme paramètre implicite. Mais, cette approche mord pour les raisons ci-dessus.

Essayez de ne jamais utiliser un singleton, si vous le pouvez. Bien qu’elles puissent sembler être une bonne approche dès le départ, elles conduisent généralement toujours à une conception médiocre et à une maintenance difficile du code.

Si vous n’avez pas vu l’article Singletons sont des menteurs pathologiques , vous devriez lire cela aussi. Il explique comment les interconnexions entre singletons sont cachées de l’interface, de sorte que la manière dont vous devez construire un logiciel est également masquée.

Il existe des liens vers quelques autres articles sur les singletons du même auteur.

Lorsque vous évaluez le motif Singleton, vous devez vous demander “Quelle est l’alternative? Les mêmes problèmes se produiraient-ils si je n’utilisais pas le motif Singleton?”

La plupart des systèmes ont besoin de Big Global Objects . Ce sont des éléments volumineux et coûteux (par exemple, les gestionnaires de connexion à la firebase database) ou qui contiennent des informations d’état omniprésentes (par exemple, des informations de locking).

L’alternative à un singleton consiste à créer ce grand object global au démarrage et à le transmettre en tant que paramètre à toutes les classes ou méthodes nécessitant un access à cet object.

Les mêmes problèmes se produiraient-ils dans le cas non-singleton? Examinons-les un par un:

  • Gestion de la mémoire : le Big Global Object existera au démarrage de l’application et l’object existera jusqu’à son arrêt. Comme il n’y a qu’un seul object, il prendra exactement la même quantité de mémoire que le cas singleton. L’utilisation de la mémoire n’est pas un problème. (@MadKeithV: L’ordre de destruction à l’arrêt est un problème différent).

  • Multithreading et goulots d’étranglement : tous les threads ont besoin d’accéder au même object, qu’ils aient reçu cet object en paramètre ou qu’ils aient appelé MyBigGlobalObject.GetInstance () . Donc, Singleton ou non, vous auriez toujours les mêmes problèmes de synchronisation (qui, heureusement, ont des solutions standard). Ce n’est pas un problème non plus.

  • Test unitaire : Si vous n’utilisez pas le modèle Singleton, vous pouvez créer le Big Global Object au début de chaque test et le ramasse-miettes le supprimera à la fin du test. Chaque test commencera avec un nouvel environnement propre qui ne sera pas affecté par le test précédent. Alternativement, dans le cas Singleton, l’object un vit tous les tests et peut facilement devenir “contaminé”. Donc oui, le pattern Singleton mord vraiment quand il s’agit de tests unitaires.

Ma préférence: à cause du problème des tests unitaires, j’ai tendance à éviter le modèle Singleton. Si c’est l’un des rares environnements où je n’ai pas de test unitaire (par exemple, la couche d’interface utilisateur), je pourrais utiliser Singletons, sinon je les évite.

Mon argument principal contre les singletons est fondamentalement qu’ils combinent deux mauvaises propriétés.

Les choses que vous mentionnez peuvent être un problème, bien sûr, mais elles ne doivent pas l’être. La synchronisation peut être corrigée, elle ne devient un goulot d’étranglement que si de nombreux threads accèdent fréquemment au singleton, etc. Ces problèmes sont agaçants, mais pas déséquilibrés.

Le problème beaucoup plus fondamental avec les célibataires est que ce qu’ils essaient de faire est fondamentalement mauvais.

Un singleton, tel que défini par le GoF, a deux propriétés:

  • Il est accessible partout dans le monde et
  • Cela empêche la classe d’être instanciée plus d’une fois.

Le premier devrait être simple. Les globaux sont généralement mauvais. Si vous ne voulez pas d’un global, alors vous ne voulez pas non plus un singleton.

Le deuxième problème est moins évident, mais fondamentalement, il tente de résoudre un problème inexistant.

À quand remonte la dernière fois que vous avez instancié une classe par accident , à la place vous avez voulu réutiliser une instance existante?

Quand est-ce la dernière fois que vous avez tapé accidentellement ” std::ostream() << "hello world << std::endl ", quand vous vouliez dire " std::cout << "hello world << std::endl "?

Ça n'arrive pas. Nous n'avons donc pas besoin de prévenir cela en premier lieu.

Mais plus important encore, le sentiment que "seul un exemple doit exister" est presque toujours faux. Ce que nous voulons généralement dire, c’est «Je ne peux actuellement voir qu’une utilisation pour une instance».

mais "je ne peux voir qu'une utilisation pour une instance" n'est pas la même chose que "l'application tombera en panne si quelqu'un ose créer deux instances".

Dans ce dernier cas, un singleton pourrait être justifié. mais dans le premier cas, c'est vraiment un choix de conception prématuré.

En général, nous finissons par vouloir plus d'une instance.

Vous finissez souvent par avoir besoin de plus d'un enregistreur. Il y a le journal dans lequel vous écrivez des messages propres et structurés, pour que le client puisse les surveiller, et il y a celui vers lequel vous voulez vider les données de débogage pour votre propre usage.

Il est également facile d'imaginer que vous pourriez finir par utiliser plusieurs bases de données.

Ou parameters du programme. Bien sûr, un seul ensemble de parameters peut être actif à la fois. Mais pendant qu'ils sont actifs, l'utilisateur peut entrer dans la boîte de dialog "options" et configurer un deuxième ensemble de parameters. Il ne les a pas encore appliqués, mais une fois qu'il a atteint "ok", ils doivent être échangés et remplacer le groupe actuellement actif. Et cela signifie que jusqu’à ce qu’il touche 'ok', deux jeux d’options existent.

Et plus généralement, les tests unitaires:

L'une des règles fondamentales des tests unitaires est qu'ils doivent être exécutés isolément. Chaque test doit définir l'environnement à partir de zéro, exécuter le test et tout détruire. Ce qui signifie que chaque test voudra créer un nouvel object singleton, exécuter le test avec lui et le fermer.

Ce qui n'est évidemment pas possible, car un singleton est créé une fois, et une seule fois. Il ne peut pas être supprimé. De nouvelles instances ne peuvent pas être créées.

En fin de compte, le problème avec les singletons n’est pas une technicité comme "il est difficile d’avoir une bonne sécurité des threads", mais beaucoup plus fondamental "ils n’apportent rien de positif à votre code. Ils ajoutent deux traits, chacun négatif , à votre code de base. Qui voudrait jamais cela? "

À propos de ce problème de test d’unité. Les principaux problèmes semblent être non pas en testant les singletons eux-mêmes, mais en testant les objects qui les utilisent .

De tels objects ne peuvent pas être isolés pour les tests, car ils ont des dépendances sur les singletons qui sont à la fois cachés et difficiles à supprimer. Cela devient encore pire si le singleton représente une interface avec un système externe (connexion à la firebase database, processeur de paiement, unité de déclenchement ICBM). Tester un tel object pourrait, de manière inattendue, écrire dans la firebase database, envoyer de l’argent qui sait où ou même envoyer des missiles intercontinentaux.

Je suis d’accord avec le sentiment précédent selon lequel ils sont fréquemment utilisés afin de ne pas avoir à se disputer partout. Je le fais. L’exemple type est votre object de journalisation du système. Je le ferais généralement comme un singleton, donc je n’ai pas à le transmettre partout dans le système.

Enquête – Dans l’exemple de l’object de journalisation, combien d’entre vous (point à main levée) appendaient un argument supplémentaire à une routine qui pourrait nécessiter la connexion de quelque chose? -Vs utilise un singleton?

Je ne voudrais pas nécessairement assimiler Singletons avec Globals. Rien ne doit empêcher un développeur de transmettre une instance de l’object, singleton ou autre, en tant que paramètre, plutôt que de le faire sortir du ciel. L’intention de cacher son accessibilité globale pourrait même se faire en cachant sa fonction getInstance à quelques amis choisis.

En ce qui concerne la faille des tests unitaires, Unit signifie petit, donc, invoquer à nouveau l’application pour tester le singleton de manière différente semble raisonnable, sauf si quelque chose me manque.