Alors les singletons sont mauvais, alors quoi?

554

Il y a eu beaucoup de discussions ces derniers temps sur les problèmes liés à l'utilisation (et à la surutilisation) de singletons. J'ai aussi été l'une de ces personnes plus tôt dans ma carrière. Je peux voir quel est le problème maintenant, et pourtant, il existe encore de nombreux cas où je ne vois pas de solution de rechange intéressante - et peu de discussions anti-singleton en fournissent réellement une.

Voici un exemple réel d'un grand projet récent auquel j'ai participé:

L'application était un client lourd avec de nombreux écrans et composants distincts, qui utilisait d'énormes quantités de données provenant d'un état de serveur qui n'était pas mis à jour trop souvent. Ces données ont été essentiellement mises en cache dans un objet "manager" de Singleton - "l'état global" redouté. L’idée était d’avoir cet endroit unique dans l’application qui conserve les données stockées et synchronisées. Ainsi, tous les nouveaux écrans ouverts peuvent simplement interroger la plupart de leurs besoins à partir de là, sans faire de requêtes répétitives pour diverses données de support provenant du serveur. Demander constamment au serveur prendrait trop de bande passante - et je parle de milliers de dollars de factures Internet supplémentaires par semaine, donc c'était inacceptable.

Y a-t-il une autre approche qui pourrait être appropriée ici que d'avoir fondamentalement ce type d'objet de cache de gestionnaire de données global? Cet objet ne doit pas obligatoirement être un "Singleton", bien sûr, mais il est conceptuellement logique de l'être. Quelle est une belle alternative propre ici?

Tables Bobby
la source
10
Quel problème l'utilisation d'un Singleton est-elle censée résoudre? Comment est-il meilleur pour résoudre ce problème que les alternatives (comme une classe statique)?
Anon.
14
@Anon: Comment l'utilisation d'une classe statique améliore-t-elle la situation? Il y a encore un couplage serré?
Martin York
5
@Martin: Je ne dis pas que ça le rend "meilleur". Je suggère que dans la plupart des cas, un singleton est une solution à la recherche d'un problème.
Anon.
9
@Anon: Ce n'est pas vrai. Les classes statiques ne vous permettent (presque) pas de contrôler l'instanciation et rendent le multi-threading encore plus difficile que les singletons (vous devez sérialiser l'accès à chaque méthode individuelle plutôt qu'à l'instance). Les singletons peuvent également au moins implémenter une interface, ce que les classes statiques ne peuvent pas. Les classes statiques ont certes leurs avantages, mais dans ce cas, le Singleton est certainement le moindre de deux maux considérables. Une classe statique qui implémente n’importe quel état mutable est comme un grand néon clignotant "AVERTISSEMENT: MAUVAISE CONCEPTION!" signe.
Aaronaught
7
@Aaronaught: Si vous synchronisez simplement l'accès au singleton, votre accès simultané est interrompu . Votre thread peut être interrompu juste après l'extraction de l'objet singleton, un autre thread s'active et blam, une situation de concurrence critique. Utiliser un Singleton au lieu d'une classe statique, dans la plupart des cas, consiste simplement à supprimer les signes avant-coureurs et à penser que le problème est résolu .
Anon.

Réponses:

809

Il est important de faire la distinction entre les instances uniques et le modèle de conception Singleton .

Les instances uniques sont simplement une réalité. La plupart des applications ne sont conçues que pour fonctionner avec une configuration à la fois, une interface utilisateur à la fois, un système de fichiers à la fois, etc. S'il y a beaucoup d'état ou de données à gérer, alors vous voudrez certainement avoir une seule instance et la garder en vie le plus longtemps possible.

Le modèle de conception Singleton est un type très spécifique d'instance unique, à savoir:

  • Accessible via un champ d'instance global et statique;
  • Créé lors de l'initialisation du programme ou lors du premier accès;
  • Aucun constructeur public (ne peut pas instancier directement);
  • Jamais explicitement libéré (implicitement libéré à la fin du programme).

C'est à cause de ce choix de conception spécifique que le motif introduit plusieurs problèmes potentiels à long terme:

  • Incapacité d'utiliser des classes abstraites ou d'interface;
  • Incapacité à sous-classer;
  • Haut couplage dans l'application (difficile à modifier);
  • Difficile à tester (impossible de simuler / simuler des tests unitaires);
  • Difficulté à paralléliser dans le cas d'un état mutable (nécessite un verrouillage important);
  • etc.

Aucun de ces symptômes n’est endémique à des cas uniques, il n’ya que le modèle Singleton.

Que pouvez-vous faire à la place? Il suffit simplement de ne pas utiliser le motif Singleton.

Citant de la question:

L’idée était d’avoir cet endroit unique dans l’application qui conserve les données stockées et synchronisées. Ainsi, tous les nouveaux écrans ouverts peuvent simplement interroger la plupart de leurs besoins à partir de là, sans faire de requêtes répétitives pour diverses données de support provenant du serveur. Demander constamment au serveur prendrait trop de bande passante - et je parle de milliers de dollars de factures Internet supplémentaires par semaine, donc c'était inacceptable.

Ce concept a un nom, comme vous le suggérez, mais son son est incertain. Cela s'appelle une cache . Si vous voulez avoir envie, vous pouvez appeler cela un "cache hors ligne" ou simplement une copie hors ligne de données distantes.

Une cache n'a pas besoin d'être un singleton. Il se peut que vous deviez avoir une seule instance si vous souhaitez éviter d'extraire les mêmes données pour plusieurs instances de cache. mais cela ne signifie pas que vous devez réellement tout exposer à tout le monde .

La première chose que je ferais est de séparer les différentes zones fonctionnelles du cache en interfaces distinctes. Par exemple, supposons que vous fabriquiez le pire clone YouTube au monde basé sur Microsoft Access:

                          MSAccessCache
                                ▲
                                |
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage MostPopularPage

Vous avez ici plusieurs interfaces décrivant les types de données spécifiques auxquels une classe particulière peut avoir besoin d'accéder - médias, profils utilisateur et pages statiques (comme la page d'accueil). Tout cela est mis en œuvre par un méga-cache, mais vous concevez vos classes individuelles pour qu'elles acceptent les interfaces, de sorte qu'elles ne s'intéressent pas à leur type d'instance. Vous initialisez une fois l'instance physique au démarrage de votre programme, puis vous ne faites que commencer à transmettre les instances (transtypées vers un type d'interface particulier) via des constructeurs et des propriétés publiques.

C'est ce qu'on appelle l' injection de dépendance , au fait; vous n'avez pas besoin d'utiliser Spring ni aucun conteneur IoC spécial, uniquement si votre conception de classe générale accepte ses dépendances de l'appelant au lieu de les instancier de son propre chef ou de faire référence à un état global .

Pourquoi devriez-vous utiliser la conception basée sur une interface? Trois raisons:

  1. Cela rend le code plus facile à lire; vous pouvez clairement comprendre depuis les interfaces les données sur lesquelles dépendent les classes dépendantes.

  2. Si et quand vous réalisez que Microsoft Access n'est pas le meilleur choix pour un back-end de données, vous pouvez le remplacer par quelque chose de mieux - disons SQL Server.

  3. Si et quand vous réalisez que SQL Server n'est pas le meilleur choix pour un média en particulier , vous pouvez diviser votre implémentation sans affecter aucune autre partie du système . C’est là que le vrai pouvoir de l’abstraction entre en jeu.

Si vous voulez aller plus loin, vous pouvez utiliser un conteneur IoC (infrastructure DI) tel que Spring (Java) ou Unity (.NET). Presque chaque infrastructure d’ID réalisera sa propre gestion de la durée de vie et vous permettra notamment de définir un service particulier comme une instance unique (l’appelant souvent "singleton", mais ce n’est que par familiarité). Fondamentalement, ces frameworks vous épargnent la plupart des tâches fastidieuses consistant à passer manuellement des instances, mais ils ne sont pas strictement nécessaires. Vous n'avez pas besoin d'outils spéciaux pour mettre en œuvre cette conception.

Par souci d’exhaustivité, je dois souligner que la conception ci-dessus n’est vraiment pas idéale non plus. Lorsque vous traitez avec un cache (comme vous êtes), vous devriez en fait avoir un calque entièrement séparé . En d'autres termes, un design comme celui-ci:

                                                        + - IMediaRepository
                                                        |
                          Cache (générique) --------------- + - IProfileRepository
                                ▲ |
                                | + - IPageRepository
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage MostPopularPage

L'avantage de ceci est que vous n'avez même jamais besoin de casser votre Cacheinstance si vous décidez de refactoriser; vous pouvez changer la façon dont le média est stocké en lui fournissant simplement une autre implémentation de IMediaRepository. Si vous réfléchissez à la manière dont cela s’assemble, vous constaterez qu’il ne crée toujours qu’une seule instance physique d’un cache. Vous n’avez donc jamais besoin d’extraire deux fois les mêmes données.

Rien de tout cela ne signifie que chaque logiciel dans le monde doit être conçu selon ces normes rigoureuses de cohésion élevée et de couplage lâche; cela dépend de la taille et de la portée du projet, de votre équipe, de votre budget, des délais, etc. Mais si vous demandez quel est le meilleur design (à utiliser à la place d'un singleton), alors c'est là.

PS Comme d'autres l'ont dit, ce n'est probablement pas la meilleure idée pour les classes dépendantes de savoir qu'elles utilisent un cache - c'est un détail d'implémentation dont elles ne devraient tout simplement pas se soucier. Cela étant dit, l'architecture globale ressemblerait toujours beaucoup à ce qui est décrit ci-dessus, vous ne voudriez tout simplement pas que les interfaces individuelles soient appelées Caches . Au lieu de cela, vous les nommeriez Services ou quelque chose de similaire.

Aaronaught
la source
131
Le premier message que j'ai jamais lu et qui explique en réalité le DI comme une alternative à l'état global. Merci pour le temps et les efforts consacrés à cela. Nous sommes tous mieux lotis à la suite de cet article.
MrLane
4
Pourquoi la cache ne peut-elle pas être un singleton? N'est-ce pas un singleton si vous le faites circuler et utilisez l'injection de dépendance? Singleton consiste simplement à se limiter à un cas, pas à la manière dont on y accède, non? Voir mon opinion
Erik Engheim le
29
@AdamSmith: vous avez bien lu en fait une de cette réponse? Votre question reçoit une réponse dans les deux premiers paragraphes. Singleton Pattern! == Instance unique.
Aaronaught
5
@Cawas et Adam Smith - En lisant vos liens, j'ai l'impression que vous n'avez pas vraiment lu cette réponse - tout y est déjà.
Wilbert
19
@ Cawas, j'estime que le fond de cette réponse est la distinction entre un seul exemple et un seul. Singleton est mauvais, l'instance simple ne l'est pas. L'injection de dépendance est un moyen génial et génial d'utiliser des instances uniques sans avoir à utiliser des singletons.
Wilbert
48

Dans le cas que vous donnez, il semble que l'utilisation d'un Singleton ne soit pas le problème, mais le symptôme d'un problème - un problème architectural plus vaste.

Pourquoi les écrans interrogent-ils l'objet cache pour obtenir des données? La mise en cache doit être transparente pour le client. Il doit exister une abstraction appropriée pour la fourniture des données, et la mise en œuvre de cette abstraction peut utiliser la mise en cache.

Le problème est probablement que les dépendances entre les parties du système ne sont pas configurées correctement, ce qui est probablement systémique.

Pourquoi les écrans doivent-ils savoir où ils obtiennent leurs données? Pourquoi les écrans ne sont-ils pas fournis avec un objet capable de répondre à leurs demandes de données (derrière lequel un cache est caché)? Souvent, la responsabilité de la création d’écrans n’est pas centralisée et il n’ya donc aucun intérêt à injecter des dépendances.

Encore une fois, nous examinons des problèmes d’architecture et de conception à grande échelle.

En outre, il est très important de comprendre que la durée de vie d'un objet peut être complètement dissociée de la manière dont il est trouvé pour utilisation.

Un cache devra rester en vie tout au long de la vie de l'application (pour être utile), donc la durée de vie de cet objet est celle d'un Singleton.

Mais le problème avec Singleton (du moins l’implémentation commune de Singleton en tant que classe / propriété statique), est de savoir comment les autres classes qui l’utilisent s’y prennent pour le trouver.

Avec une implémentation Singleton statique, la convention consiste simplement à l’utiliser chaque fois que nécessaire. Mais cela cache complètement la dépendance et couple étroitement les deux classes.

Si nous fournissons la dépendance à la classe, cette dépendance est explicite et tout ce que la classe consommatrice doit connaître est le contrat disponible pour pouvoir être utilisé.

Quentin Starin
la source
2
Certains écrans peuvent nécessiter une quantité de données gigantesque, mais pas nécessairement. Et vous ne savez pas avant que les actions de l'utilisateur définissent ceci - et il y a beaucoup, beaucoup de combinaisons. Ainsi, les données globales communes conservées en mémoire cache et synchronisées dans le client (la plupart du temps lors de la connexion) ont été conservées, puis les requêtes ultérieures ont renforcé le cache, car les données explicitement demandées ont tendance à être réutilisées à nouveau. la même session. L'accent est mis sur la réduction des demandes au serveur, d'où la nécessité d'un cache côté client. <cont>
Bobby Tables
1
<cont> C’est essentiellement transparent. En ce sens qu'il existe un rappel du serveur si certaines données requises ne sont pas encore mises en cache. Mais l'implémentation (logique et physique) de ce gestionnaire de cache est un Singleton.
Bobby Tables
6
Je suis avec qstarin ici: les objets accédant aux données ne doivent pas savoir (ou doivent savoir) que les données sont mises en cache (il s'agit d'un détail d'implémentation). Les utilisateurs des données demandent simplement les données (ou demandent une interface pour récupérer les données).
Martin York
1
La mise en cache est essentiellement un détail d'implémentation. Il existe une interface par laquelle les données sont interrogées et les objets qui l'obtiennent ne savent pas si elles proviennent du cache ou non. Mais sous ce gestionnaire de cache se trouve un Singleton.
Bobby Tables
2
@Bobby Tables: votre situation n'est donc pas aussi grave qu'il y paraissait. Ce singleton (en supposant que vous parliez d'une classe statique, et pas simplement d'un objet avec une instance qui vit aussi longtemps que l'application) est toujours problématique. Cela cache le fait que votre objet fournisseur de données dépend d'un fournisseur de cache. C'est mieux si c'est explicite et externalisé. Découple-les. Il est essentiel pour la testabilité que vous puissiez facilement remplacer des composants, et un fournisseur de cache est un excellent exemple d'un tel composant (à quelle fréquence un fournisseur de cache est-il sauvegardé par ASP.Net).
Quentin-Starin
45

J'ai écrit un chapitre entier sur cette question. Principalement dans le contexte des jeux, mais la plupart d’entre eux devraient s’appliquer en dehors des jeux.

tl; dr:

Le modèle Gang of Four Singleton a deux objectifs: vous donner un accès pratique à un objet de n'importe où et vous assurer qu'une seule instance peut être créée. 99% du temps, tout ce qui vous intéresse est la première moitié de cela, et le fait de rouler la seconde moitié pour l'obtenir ajoute une limitation inutile.

De plus, il existe de meilleures solutions pour un accès pratique. Rendre un objet global est l’option nucléaire pour résoudre ce problème et facilite la destruction de votre encapsulation. Tout ce qui ne va pas avec les globals s'applique complètement aux singletons.

Si vous l'utilisez simplement parce que vous avez beaucoup d'endroits dans le code qui doivent toucher le même objet, essayez de trouver un meilleur moyen de le donner uniquement à ces objets sans l'exposer à la totalité de la base de code. Autres solutions:

  • Fossé entièrement. J'ai vu beaucoup de classes de singleton qui n'ont pas d'état et sont juste des sacs de fonctions d'assistance. Ceux-ci n'ont pas besoin d'une instance du tout. Il suffit de les transformer en fonctions statiques ou de les déplacer dans l’une des classes que la fonction prend en argument. Vous n'auriez pas besoin d'une Mathclasse spéciale si vous pouviez simplement faire 123.Abs().

  • Passez-le. La solution simple si une méthode a besoin d'un autre objet est simplement de la transmettre. Il n'y a rien de mal à transmettre certains objets.

  • Mettez-le dans la classe de base. Si vous avez beaucoup de classes qui ont toutes besoin d'accéder à un objet spécial et qui partagent une classe de base, vous pouvez faire de cet objet un membre de la base. Lorsque vous le construisez, passez l'objet. Maintenant, les objets dérivés peuvent tous l'obtenir quand ils en ont besoin. Si vous le protégez, vous vous assurez que l'objet reste toujours encapsulé.

munificent
la source
1
Pouvez-vous résumer ici?
Nicole
1
C'est fait, mais je vous encourage quand même à lire tout le chapitre.
magnifique
4
une autre alternative: injection de dépendance!
Brad Cupit
1
@ BradCupit, il en parle aussi sur le lien ... Je dois dire que j'essaie encore de tout digérer. Mais ce fut la lecture la plus élucidante sur des singletons que j'ai jamais lue. Jusque-là, j’étais persuadé que des singletons étaient nécessaires, tout comme les vars mondiaux, et je faisais la promotion de la Boîte à outils . Maintenant, je ne sais plus rien. M. munificient pouvez-vous me dire si le localisateur de service est juste une boîte à outils statique ? Ne vaudrait-il pas mieux en faire un singleton (donc une boîte à outils)?
cregox
1
"Boîte à outils" me ressemble beaucoup. Que vous utilisiez un statique ou que vous en fassiez un singleton n’est pas si important pour la plupart des programmes. J'ai tendance à pencher vers la statique, car pourquoi traiter l'initialisation paresseuse et l'allocation de tas si cela n'est pas nécessaire?
munificence
21

Ce n'est pas l'état global en soi, c'est le problème.

Vraiment vous devez seulement vous inquiéter global mutable state. L'état constant n'est pas affecté par les effets secondaires et pose donc moins de problèmes.

Le problème majeur avec singleton est qu’il ajoute un couplage et rend donc les choses plus difficiles, comme les tests. Vous pouvez réduire le couplage en récupérant le singleton d'une autre source (par exemple une usine). Cela vous permettra de découpler le code d'une instance particulière (bien que vous deveniez plus couplé à l'usine (mais au moins l'usine peut avoir d'autres implémentations pour différentes phases)).

Dans votre situation, je pense que vous pouvez vous en tirer à condition que votre singleton implémente réellement une interface (de sorte qu'une alternative puisse être utilisée dans d'autres situations).

Mais un autre inconvénient majeur des singletons est qu’une fois qu’ils sont en place, les supprimer du code et les remplacer par quelque chose d’autre devient une tâche vraiment ardue (il ya à nouveau ce couplage).

// Example from 5 minutes (con't be too critical)
class ServerFactory
{
    public:
        // By default return a RealServer
        ServerInterface& getServer();

        // Set a non default server:
        void setServer(ServerInterface& server);
};

class ServerInterface { /* define Interface */ };

class RealServer: public ServerInterface {}; // This is a singleton (potentially)

class TestServer: public ServerInterface {}; // This need not be.
Martin York
la source
Ça a du sens. Cela me fait aussi penser que je n'ai jamais vraiment abusé de Singletons, mais que je commence à douter de TOUTE utilisation. Mais je ne peux penser à aucun abus pur et simple que j'ai commis, selon ces points. :)
Bobby Tables
2
(vous voulez probablement dire en soi pas "à dire")
nohat le
4
@nohat: Je suis le locuteur natif de "Queens English" et rejette donc tout ce qui ressemble au français à moins que nous ne le rendions meilleur (comme le weekendoops, c'est l'un des nôtres). Merci :-)
Martin York le
21
en soi, c'est latin.
Anon.
2
@Anon: OK. Ce n'est pas si mal alors ;-)
Martin York
19

Alors quoi? Puisque personne ne l'a dit: Boîte à outils . C'est si vous voulez des variables globales .

L’ abus de Singleton peut être évité en examinant le problème sous un angle différent. Supposons qu'une application n'ait besoin que d'une seule instance d'une classe et qu'elle configure cette classe au démarrage: pourquoi la classe elle-même devrait-elle être tenue pour responsable d'être un singleton? Il semble assez logique que l’application assume cette responsabilité, car elle requiert ce type de comportement. L'application, et non le composant, devrait être le singleton. L'application rend ensuite une instance du composant disponible pour tout code spécifique à l'application à utiliser. Lorsqu'une application utilise plusieurs composants de ce type, elle peut les regrouper dans ce que nous avons appelé une boîte à outils.

En termes simples, la boîte à outils de l'application est un singleton qui est chargé de se configurer ou de permettre au mécanisme de démarrage de l'application de le configurer ...

public class Toolbox {
     private static Toolbox _instance; 

     public static Toolbox Instance {
         get {
             if (_instance == null) {
                 _instance = new Toolbox(); 
             }
             return _instance; 
         }
     }

     protected Toolbox() {
         Initialize(); 
     }

     protected void Initialize() {
         // Your code here
     }

     private MyComponent _myComponent; 

     public MyComponent MyComponent() {
         get {
             return _myComponent(); 
         }
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object GetComponent (String componentName) {
         return components.Get(componentName); 
     }

     public void RegisterComponent(String componentName, Object component) 
     {
         components.Put(componentName, component); 
     }

     public void DeregisterComponent(String componentName) {
         components.Remove(componentName); 
     }

}

Mais devinez quoi? C'est un singleton!

Et qu'est-ce qu'un singleton?

Peut-être que c'est là que la confusion commence.

Pour moi, le singleton est un objet contraint de n'avoir qu'une seule instance et toujours. Vous pouvez y accéder n'importe où, à tout moment, sans avoir à l'instancier. C'est pourquoi il est si étroitement lié à static. À titre de comparaison, staticest fondamentalement la même chose, sauf que ce n'est pas une instance. Nous n'avons pas besoin de l'instancier, nous ne pouvons même pas, car c'est alloué automatiquement. Et cela peut et apporte des problèmes.

D'après mon expérience, le simple remplacement staticde Singleton a résolu de nombreux problèmes dans le cadre d'un projet de sac patchwork de taille moyenne. Cela signifie seulement qu'il a une certaine utilité pour les projets mal conçus. Je pense qu'il y a trop de discussion si le motif singleton est utile ou non et je ne peux pas vraiment discuter si c'est vraiment mauvais . Mais il existe toujours de bons arguments en faveur du singleton par rapport aux méthodes statiques, en général .

La seule chose dont je suis sûr qu’il est mauvais en ce qui concerne les singletons, c’est de les utiliser en ignorant les bonnes pratiques. C'est en effet quelque chose de pas facile à gérer. Mais les mauvaises pratiques peuvent être appliquées à n'importe quel modèle. Et, je sais, c'est trop générique pour dire que ... je veux dire, c'est trop.

Ne vous méprenez pas!

En termes simples, tout comme les vars globaux , les singletons doivent toujours être évités . Spécialement parce qu'ils sont trop maltraités. Mais les serveurs mondiaux ne peuvent pas toujours être évités et nous devrions les utiliser dans ce dernier cas.

Quoi qu'il en soit, il existe de nombreuses suggestions autres que la Boîte à outils et, tout comme la boîte à outils, chacune a son application ...

Autres alternatives

  • Le meilleur article que je viens de lire sur les singletons suggère le localisateur de services comme alternative. Pour moi, il s’agit d’une " boîte à outils statique ", si vous voulez. En d'autres termes, faites du service de localisation un singleton et vous aurez une boîte à outils. Cela va à l’encontre de sa suggestion initiale d’éviter le singleton, bien sûr, mais ce n’est que pour faire respecter le problème du singleton, c’est de savoir comment il est utilisé, pas le motif en soi.

  • D'autres suggèrent le modèle d'usine comme alternative. C'était la première alternative d'un collègue et nous l'avons rapidement éliminée pour notre utilisation en tant que var global . Il a son usage, mais les singletons aussi.

Les deux alternatives ci-dessus sont de bonnes alternatives. Mais tout dépend de votre utilisation.

Maintenant, impliquer les singletons doit être évité à tout prix, c'est faux

  • La réponse d' Aaronaught suggère de ne jamais utiliser de singletons , pour une série de raisons. Mais ce sont toutes des raisons contre la façon dont il est mal utilisé et abusé, et non directement contre le schéma en soi. Je suis d'accord avec tous les soucis concernant ces points, comment puis-je? Je pense juste que c'est trompeur.

Les inaptitudes (à l’abstrait ou à la sous-classe) sont bien là, mais alors quoi? Ce n'est pas fait pour ça. Il n'y a pas d' incapacité d'interface , pour autant que je sache . Un couplage élevé peut également être là, mais c'est simplement parce qu'il est couramment utilisé. Ce n'est pas obligé . En fait, le couplage en soi n'a rien à voir avec le motif singleton. Ceci étant clarifié, cela élimine également déjà la difficulté à tester. Quant à la difficulté de paralléliser, cela dépend de la langue et de la plate-forme, donc encore une fois, ce n’est pas un problème pour le modèle.

Exemples pratiques

Je vois souvent que 2 sont utilisés, à la fois en faveur et contre des singletons. Cache Web (mon cas) et service de journalisation .

L'exploitation forestière, diront certains , est un exemple singleton parfait, car, et je cite:

  • Les demandeurs ont besoin d'un objet connu auquel envoyer des demandes pour se connecter. Cela signifie un point d'accès global.
  • Étant donné que le service de journalisation est une source d'événement unique pouvant être enregistrée par plusieurs écouteurs, il ne doit y avoir qu'une seule instance.
  • Bien que différentes applications puissent se connecter à différents périphériques de sortie, la façon dont elles enregistrent leurs écouteurs est toujours la même. Toute la personnalisation est faite par les auditeurs. Les clients peuvent demander une journalisation sans savoir comment ni où le texte sera consigné. Chaque application utiliserait donc le service de journalisation exactement de la même manière.
  • Toute application doit pouvoir s’en tirer avec une seule instance du service de journalisation.
  • Tout objet peut être un demandeur de journalisation, y compris des composants réutilisables, de sorte qu'il ne soit associé à aucune application particulière.

Tandis que d’autres diront qu’il est difficile d’élargir le service de journalisation une fois que vous réalisez qu’il ne devrait en réalité pas y avoir une seule instance.

Eh bien, je dis les deux arguments sont valables. Ici encore, le problème ne concerne pas le motif singleton. C'est sur les décisions architecturales et la pondération si la refactorisation est un risque viable. C'est un problème supplémentaire lorsque, habituellement, la refactorisation est la dernière mesure corrective nécessaire.

cregox
la source
@gnat Merci! Je pensais justement, en modifiant la réponse, à mettre en garde sur l’utilisation de Singletons ... Votre citation s’intègre parfaitement!
cregox
2
content que tu aies aimé. Je ne sais pas si cela contribuera à éviter les votes négatifs - les lecteurs ont probablement du mal à faire le lien entre ce message et le problème concret exposé dans la question, en particulier à la lumière de la formidable analyse fournie dans la réponse précédente
Gnat
@gn oui, je savais que c'était une longue bataille. J'espère que le temps le dira. ;-)
cregox
1
Je suis d'accord avec votre orientation générale ici, même si vous êtes peut-être un peu trop enthousiaste face à une bibliothèque qui semble ne pas être beaucoup plus qu'un conteneur IoC extrêmement dépouillé (encore plus fondamental que Funq , par exemple ). Service Locator est en réalité un anti-modèle; c'est un outil utile principalement dans les projets patrimoniaux / liés aux terrains désaffectés, ainsi que dans celui du "pauvre homme", où il serait trop coûteux de tout refactoriser pour utiliser correctement un conteneur IoC.
Aaronaught
1
@Cawas: Ah, désolé - j'ai été confondu avec java.awt.Toolkit. Mon point est cependant le même: cela Toolboxressemble à une corbeille de morceaux sans rapport, plutôt qu’à une classe cohérente ayant un seul but. Cela ne me semble pas être un bon design. (Notez que l'article auquel vous faites référence est de 2001, avant que l'injection de dépendance et les conteneurs DI ne soient devenus monnaie courante.)
Jon Skeet
5

Mon principal problème avec le modèle de conception singleton est qu’il est très difficile d’écrire de bons tests unitaires pour votre application.

Chaque composant dépendant de ce "manager" le fait en interrogeant son instance singleton. Et si vous voulez écrire un test unitaire pour un tel composant, vous devez injecter des données dans cette instance de singleton, ce qui risque de ne pas être simple.

Si par contre votre "manager" est injecté dans les composants dépendants via un paramètre constructeur et que le composant ne connaisse pas le type concret du manager, seulement une interface ou une classe de base abstraite implémentée par le manager, alors une unité test pourrait fournir d'autres implémentations du gestionnaire lors du test des dépendances.

Si vous utilisez des conteneurs IOC pour configurer et instancier les composants de votre application, vous pouvez facilement configurer votre conteneur IOC de manière à ne créer qu'une seule instance du "gestionnaire", ce qui vous permet de réaliser la même chose, une seule instance contrôlant le cache d'applications global. .

Mais si vous ne vous souciez pas des tests unitaires, un modèle de conception singleton peut parfaitement convenir. (mais je ne le ferais pas quand même)

Pete
la source
Bien expliqué, c'est la réponse qui explique le mieux la question des tests de singletons
José Tomás Tocino
4

Un singleton n’est pas fondamentalement mauvais en ce sens que tout ce que l’informatique de conception peut être bon ou mauvais. Cela ne peut être que correct (donne les résultats escomptés) ou non. Cela peut également être utile ou non, si cela rend le code plus clair ou plus efficace.

Un cas dans lequel les singletons sont utiles est quand ils représentent une entité qui est vraiment unique. Dans la plupart des environnements, les bases de données sont uniques, il n'y en a qu'une. La connexion à cette base de données peut s'avérer compliquée car elle nécessite des autorisations spéciales ou une traversée de plusieurs types de connexion. Organiser cette connexion en un singleton a probablement beaucoup de sens pour cette seule raison.

Mais vous devez également vous assurer que le singleton est vraiment un singleton, et non une variable globale. Ceci est important lorsque la base de données unique est constituée de 4 bases de données, une pour la production, la mise en place, le développement et les montages de test. Un Singleton de base de données déterminera à qui il doit se connecter, saisira l'instance unique de cette base de données, la connectera si nécessaire et le renverra à l'appelant.

Lorsqu'un singleton n'est pas vraiment un singleton (c'est alors que la plupart des programmeurs s'énervent), c'est un global instancié paresseusement, il n'y a aucune possibilité d'injecter une instance correcte.

Une autre caractéristique utile d'un motif singleton bien conçu est qu'il n'est souvent pas observable. L'appelant demande une connexion. Le service qui le fournit peut renvoyer un objet en pool ou, s'il effectue un test, il peut en créer un pour chaque appelant ou fournir un objet fictif à la place.

SingleNegationElimination
la source
3

L'utilisation du motif singleton qui représente des objets réels est parfaitement acceptable. J'écris pour l'iPhone, et il y a beaucoup de singletons dans le framework Cocoa Touch. L'application elle-même est représentée par un singleton de la classe UIApplication. Vous êtes une seule application, il est donc approprié de la représenter avec un singleton.

L'utilisation d'un singleton en tant que classe de gestionnaire de données est acceptable tant qu'elle est conçue correctement. S'il s'agit d'un ensemble de propriétés de données, ce n'est pas mieux que la portée globale. S'il s'agit d'un ensemble de getters et de setters, c'est mieux, mais ce n'est toujours pas génial. S'il s'agit d'une classe qui gère réellement toutes les interfaces de données, y compris peut-être l'extraction de données distantes, la mise en cache, la configuration et le démontage ... Cela pourrait être très utile.

Dan Ray
la source
2

Les singletons ne sont que la projection d'une architecture orientée service dans un programme.

Une API est un exemple de singleton à un niveau de protocole. Vous accédez à Twitter, Google, etc. via essentiellement des singletons. Alors, pourquoi les singletons deviennent-ils mauvais dans un programme?

Cela dépend de la façon dont vous pensez d'un programme. Si vous considérez un programme comme une société de services plutôt que comme des instances en cache liées au hasard, les singletons sont parfaitement logiques.

Les singletons sont un point d'accès au service. L'interface publique vers une bibliothèque de fonctionnalités étroitement liée qui cache peut-être une architecture interne très sophistiquée.

Donc, je ne vois pas un singleton différent de celui d’une usine. Le singleton peut avoir des paramètres de constructeur passés. Il peut être créé par un contexte qui sait comment résoudre l'imprimante par défaut par rapport à tous les mécanismes de sélection possibles, par exemple. Pour tester, vous pouvez insérer votre propre maquette. Donc, cela peut être assez flexible.

La clé se trouve en interne dans un programme lorsque j'exécute et nécessite un peu de fonctionnalité. Je peux accéder au singleton en toute confiance sur le fait que le service est opérationnel et prêt à être utilisé. Cela est essentiel lorsque différents threads démarrant dans un processus doivent passer par une machine à états pour être considérés comme prêts.

En règle générale, je voudrais envelopper une XxxServiceclasse qui enveloppe un singleton autour de la classe Xxx. Le singleton est pas en classe Xxxdu tout, il est séparé dans une autre classe, XxxService. En effet, Xxxplusieurs instances peuvent être utilisées, bien que cela ne soit pas probable, mais nous souhaitons toujours avoir une Xxxinstance accessible de manière globale sur chaque système. XxxServicefournit une belle séparation des préoccupations. XxxIl n'est pas nécessaire d'appliquer une stratégie de singleton, mais nous pouvons l'utiliser Xxxcomme un singleton lorsque nous en avons besoin.

Quelque chose comme:

//XxxService.h:
/**
 * Provide singleton wrapper for Xxx object. This wrapper
 * can be autogenerated so is not made part of the object.
 */

#include "Xxx/Xxx.h"


class XxxService
{
    public:
    /**
     * Return a Xxx object as a singleton. The double check
     * singleton algorithm is used. A 0 return means there was
     * an error. Developers should use this as the access point to
     * get the Xxx object.
     *
     * <PRE>
     * @@ #include "Xxx/XxxService.h"
     * @@ Xxx* xxx= XxxService::Singleton();
     * <PRE>
     */

     static Xxx*     Singleton();

     private:
         static Mutex  mProtection;
};


//XxxService.cpp:

#include "Xxx/XxxService.h"                   // class implemented
#include "LockGuard.h"     

// CLASS SCOPE
//
Mutex XxxService::mProtection;

Xxx* XxxService::Singleton()
{
    static Xxx* singleton;  // the variable holding the singleton

    // First check to see if the singleton has been created.
    //
    if (singleton == 0)
    {
        // Block all but the first creator.
        //
        LockGuard lock(mProtection);

        // Check again just in case someone had created it
        // while we were blocked.
        //
        if (singleton == 0)
        {
            // Create the singleton Xxx object. It's assigned
            // to a temporary so other accessors don't see
            // the singleton as created before it really is.
            //
            Xxx* inprocess_singleton= new Xxx;

            // Move the singleton to state online so we know that is has
            // been created and it ready for use.
            //
            if (inprocess_singleton->MoveOnline())
            {
                LOG(0, "XxxService:Service: FAIL MoveOnline");
                return 0;
            }

            // Wait until the module says it's in online state.
            //
            if (inprocess_singleton->WaitTil(Module::MODULE_STATE_ONLINE))
            {
                LOG(0, "XxxService:Service: FAIL move to online");
                return 0;
            }

            // The singleton is created successfully so assign it.
            //
            singleton= inprocess_singleton;


        }// still not created
    }// not created

    // Return the created singleton.
    //
    return singleton;

}// Singleton  
Todd Hoff
la source
1

Première question, trouvez-vous beaucoup de bugs dans l'application? peut-être oublier de mettre à jour le cache, ou un mauvais cache ou avoir du mal à changer? (Je me souviens d'une application ne changerait pas de taille sauf si vous changiez aussi de couleur ... vous pouvez toutefois changer la couleur et conserver la taille).

Ce que vous feriez, c'est d'avoir cette classe mais SUPPRIMER TOUS LES MEMBRES STATIQUES. Ok ce n'est pas nessacary mais je le recommande. Vraiment, vous venez d’initialiser la classe comme une classe normale et de PASSER le pointeur. Ne parlez pas de frigen, dites ClassIWant.APtr (). LetMeChange.ANYTHINGATALL (). Andhave_no_structure ()

C'est plus de travail mais vraiment, c'est moins déroutant. Certains endroits où vous ne devriez pas changer des choses que vous ne pouvez pas maintenant car ce n’est plus global. Toutes mes classes de manager sont des classes régulières, traitez-le simplement comme ça.


la source
1

OMI, votre exemple semble bien. Je suggérerais de factoriser comme suit: objet de cache pour chaque objet de données (et derrière chaque); Les objets en cache et les objets d'accès à la base de données ont la même interface. Cela donne la possibilité d'échanger des caches dans et hors du code; De plus, cela donne une voie d'expansion facile.

Graphique:

DB
|
DB Accessor for OBJ A
| 
Cache for OBJ A
|
OBJ A Client requesting

L'accesseur de base de données et le cache peuvent hériter du même objet ou du même type de canard pour ressembler au même objet, peu importe. Tant que vous pouvez brancher / compiler / tester et que cela fonctionne toujours.

Cela permet de découpler les éléments afin que vous puissiez ajouter de nouveaux caches sans avoir à entrer et modifier un objet Uber-Cache. YMMV. IANAL. ETC.

Paul Nathan
la source
1

Un peu tard pour la fête, mais quand même.

Singleton est un outil dans une boîte à outils, comme n'importe quoi d'autre. J'espère que vous avez plus dans votre boîte à outils qu'un simple marteau.

Considère ceci:

public void DoSomething()
{
    MySingleton.Instance.Work();
}

contre

public void DoSomething(MySingleton singleton)
{
    singleton.Work();
}
DoSomething(MySingleton.instance);

1er cas conduit à un couplage élevé, etc. La 2ème manière n'a pas de problèmes @Aaronaught décrit, pour autant que je sache. Son tout sur la façon dont vous l'utilisez.

Evgeni
la source
Être en désaccord. Bien que la deuxième voie soit "meilleure" (couplage diminué), il reste des problèmes résolus par DI. Vous ne devez pas compter sur le consommateur de votre classe pour assurer la mise en œuvre de vos services - cela est mieux effectué dans le constructeur lorsque la classe est créée. Votre interface ne devrait nécessiter qu'un minimum d'arguments. Il existe également une chance décente que votre classe ne nécessite le fonctionnement d’une seule instance. Là encore, compter sur le consommateur pour appliquer cette règle est risqué et inutile.
AlexFoxGill
Parfois, Di est excessif pour une tâche donnée. La méthode dans un exemple de code peut être un constructeur, mais pas nécessairement - sans examiner un exemple concret, c'est un argument théorique. En outre, DoSomething pourrait prendre ISomething et MySingleton pourrait implémenter cette interface - ok, ce n’est pas dans un exemple .. mais c’est juste un exemple.
Evgeni
1

Demandez à chaque écran de prendre le gestionnaire dans leur constructeur.

Lorsque vous démarrez votre application, vous créez une instance du gestionnaire et vous la transmettez.

Cette opération s'appelle Inversion of Control et vous permet d'échanger le contrôleur lorsque la configuration change et lors des tests. De plus, vous pouvez exécuter plusieurs instances de votre application ou des parties de votre application en parallèle (utile pour les tests!). Enfin, votre responsable mourra avec son objet propriétaire (la classe de démarrage).

Alors, structurez votre application comme un arbre, où tout ce qui se trouve au-dessus d’eux est utilisé en dessous d’eux. N'implémentez pas une application comme un maillage, où tout le monde se connaît et se trouve grâce à des méthodes globales.

Alexander Torstling
la source