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:
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.
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.
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 Cache
instance 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.