Singletons: un peu, beaucoup, à la folie, pas du tout
Joanna Carter, tu as trouvé LA solution !
En effet, si la carte est directement affichée avec un tab bar controller, elle n'est chargée qu'une seule fois, et donc, il n'y a pas de souci de mémoire.
Or, j'aimerai tout de même faire quelque chose qui se rapproche de ce que j'avais fait au départ.
J'ai pensé à une solution : au lieu de supprimer les UIViewController du Navigation Controller, je pourrai tout simplement simuler un click sur l'item de la carte ou sur un autre item pour revenir. Le problème est que je ne parviens pas, avec le code, à récupérer la tab bar que j'ai créé dans mon storyboard. Comment faire ?
C'est bien compliqué ton système. Tu pourrais aussi charger la carte en mémoire au lancement de l'application, et la stocker dans un singleton, ou une variable globale (je sais, les variables globales c'est le mal).
Réponses
Je ne comprends pas cet crainte des singletons.
Si on est disposé d'utiliser :
... qui est, effectivement, un singleton, pourquoi pas créer vos propres singletons ?
Et pourquoi polluer le délégué de l'application avec les entités comme les données ?
Or, les base de données sont souvent un "lieu de stockage" unique ; du coup, c'est logique de créer un point d'accès unique avec un singleton
J'ai arrêté d'utiliser les singletons le jour où je me suis mis à écrire des tests unitaires.
Donc je présume que tu n'utilises jamais l'application delegate, qui est associé avec UIApplication.shared, qui est un singleton
Pour ce qui est de l'AppDelegate, c'est évidemment mon point d'entrée dans le programme, et souvent c'est bien là que je vais instancier le Modèle, puisque l'AppDelegate est le contrôleur racine de l'application. Ainsi, il pourra passer le Modèle aux ViewControllers.
Et je dis bien "passer". Dans mon modèle "tell, don't ask", aucun objet n'a de raison de demander quoi que ce soit à l'AppDelegate. Les flux de données vont forcément vers l'avant.
À noter que l'AppDelegate est difficilement testable, parce qu'on ne peut pas lui passer un objet Application, du moins en Swift. En ObjC, on peut utiliser OCMock pour mocker UIApplication.
Je vais peut être dire une grosse connerie mais pas grave : Coredata fonctionne pas avec des singletons ?
Perso, j'utilise toujours CoreData + MagicalRecord (oui, je sais, ça fait un peu vieux mais ça fonctionne bien, même en swift !) et quand je veux accèder à ma base, je fais un simple :
ou en swift :
C'est pas du bon gros Singleton ça ?
ça ressemble plus à des propriétés et meÌthodes de classe non ?
Je sais pas trop parce que par exemple, si je fetch une fois ma base, pour les fetch suivants, je sais qu'il va réutiliser les précédents.
Alors est ce que c'est du singleton, est ce que c'est du "lazy load"... Je sais pas trop. J'ai jamais trop creusé...
En tous cas, c'est pratique.
SInon, perso, j'utilise un singleton pour les achats inApp dans mon app.
Pas exactement. Là , tu utilises les méthodes "statiques" ou "classes".
Moi, je n'aime pas cette démarche parce qu'elle signifie que la classe sache comment trouver et trier tous les instances du type ; ce que l'on n'attend pas. Ce n'est pas le type que s'occupe de ça, c'est le mechanism de stockage et c'est lui qu'il faut demander.
Pour faciliter les tests unitaires avec un Singleton, on peut toujours fournir une méthode internal que l'on puisse accéder dans les test en utilisant @testable import
Dans cet exemple, j'ai créé un deuxième init paramétré pour que je puisse fournir un état connu :
MagicalRecords essaie de tout faciliter au maximum, d'où la proposition de passer par un singleton pour accéder au MOC.
Autant pour l'utilisation des singletons je suis entièrement d'accord avec toi. En revanche je ne partage pas ton point de vue sur tes derniers propos. Le but de la fonction static est d'offrir une action extrinsèque à aux instances de sa classe. Dans le cas présenté la méthode findAllSortedBy retourne une liste qui n'est en aucun cas lié à l''état d'une instance de la classe Site.
Dans ton exemple, l'objet de test est forcement un DataProvider. ça pose quand même problème, parce qu'on ne peut pas le remplacer par ce qu'on veut. En test unitaires, on cherche à placer l'objet qu'on teste dans l'environnement le plus cloisonné possible. S'il faut instancier des objets complexes à créer:
1) les tests deviennent difficiles à écrire
2) les tests sont à la merci du fonctionnement interne de ces objets, et ne sont donc pas fiables.
Le gros problème du singleton réside essentiellement dans la sa gestion face à un contexte "multithreadé".
Dans une application je ne trouve pas déconnant que les instances des classes de ta couche service et persistance soient uniques pour les raisons évoquées par Joanna. Je ne vois vraiment pas la plus value de créer un objet dans chaque controllers pour accéder au datas (par exemple) dans la mesure où l'utilisateur est seul sur son device.
Avec Singleton:
Avec Injection de dépendance:
Peut-être que la version avec le singleton paraà®t plus simple, mais en pratique, elle a une dépendance cachée sur Database, et on ne peut pas remplacer facilement la Database dans les tests.
C'était un exemple.
Je ne vois pas en quoi ton second exemple met en cause l'emploi des singletons.
Tu peux très bien faire :
Mais le gros avantage de ta solution réside dans la souplesse de ton service qui sera facilement mockable dans les TU. Mais encore une fois, je ne vois pas en quoi l'emploi du singleton doit être prohibé.
Cette design pattern impose des contraintes inutiles pour les tests, et elle encourage de mauvaises pratiques, telles que masquer les dépendances et coupler fortement les classes.
Il y a 20 ans, je codais en utilisant les variables globales et si quelqu'un était venu me dire que j'avais tort de faire ainsi, je lui aurais répondu: " En quoi les globales doivent être prohibées? ça marche, et c'est facile! ". Cependant, le fruit de l'expérience est venu me démontrer qu'en grossissant, le programme devenait ingérable: ça plantait tout le temps, je passais des heures sur le débogueur, et plus rien n'avançait. (à l'époque, un plantage d'un programme obligeait souvent à redémarrer le Mac).
ça m'a donné une bonne leçon, mais chacun doit faire son propre chemin pour comprendre ce genre de choses. À la suite de cette expérience, je fus capable d'énoncer clairement quels étaient les inconvénients des variables globales, leur avantages et les solutions alternatives.
Pour les singletons, vous connaissez les avantages, je vous expose les inconvénients et les solutions alternatives. Comme pour les globales, il faut déjà avoir un programme assez gros pour comprendre les problèmes qu'ils suscitent.
??? ??? ???
C'est sérieux comme phrase ? Ou un excès de contrariété qui n'a pas vraiment lieu d'être ?
Je ne dis pas le contraire. Restons sur le cas des singletons.
Ce que tu dis est loin d'être faux, seulement tu démontes toi même tes arguments en donnant l'exemple ci-dessous :
- Facilement testable grâce à l'injection de dépendance.
- Couplage beaucoup moins fort (si Database était un protocol ce serait encore mieux).
- La dépendance n'est pas masquée.
J'ai vraiment du mal à te suivre dans ton argumentation.
Mais rien ne t'empêche d'utiliser un singleton avec l'injection de dépendance.
Je me répète mais tu peux très bien faire ça :
La façon dont tu nous le montres me donne l'impressions que ta proposition d'injection de dépendance est une alternative à l'utilisation des singletons... C'est juste complémentaire, tu peux très bien injecter un singleton avec ta solution.
Et je suis 100% d'accord avec toi que ça résout le problème du test.
Oui j'affirme que c'est bien une alternative, parce qu'il n'y a pas d'intérêt à injecter une dépendance si on peut accéder à cette dépendance par son singleton. As-tu déjà vu beaucoup de code comme ci-dessus ?
(Moi, j'en ai déjà vu, mais écrit par quelqu'un qui justement était ennuyé par UserDefaults dans ses tests unitaires).
Peut être à rendre le couplage entre les classes moins fort ?
Au delà de ça, quand tu as des services (ou je ne sais trop quoi d'autre), tu vas créer une instance par controller ?
Un exemple où j'utiliserais un Singleton: le logging. Logguer n'est pas sensé modifier le fonctionnement de la classe et filer un objet Logging à tout le monde devient carrément lourd; il y en a qui vont jusque là , voire même qui testent que ça loggue bien, mais je doute du retour sur investissement. En pratique NSLog() me suffit souvent.
Ah okay d'accord !
Juste pour bien comprendre ta façon de procéder. Si tu as une couche de service et couche de persistance (le service "travaille" les datas avant de les persister). Tu dois injecter la classe de persistance dans ta classe de service. Où fais tu ça ? Dans ton controller ? Ou tu passes pas une classe tiers qui fait les injections et qui te retourne ton service correctement construit ?
Bah non ! Le Data Provider singleton n'est qu'un emballage autour un mécanisme de stockage qui pourrait être remplacé.
Du coup, on peut tester les mécanisme à part du singleton et, après, tester la fonctionnalité du singleton.
Je attendrais qqch. comme :
Puis, au commencement de l'Appli :
et, après :
Je ne vais pas te donner une réponse précise, parce que... ça dépend. La persistance des données est un point sur lequel pêche la POO. Je ne connais pas de solution parfaite.
Le cas simple, c'est le JSON/plist/XML: chaque classe se décrit elle-même puis ses enfants en renvoyant un dictionnaire. Et peut aussi s'initialiser avec. C'est un contrôleur qui va demander l'instanciation à partir de la NSData, mais pas forcément un View Controller.
Pour une base de données, on peut imaginer avoir une sorte de Pool qui se débrouille pour instancier ou sauvegarder des objets Modèle, mais cette solution n'a pas ma faveur, parce que l'objet Pool devient vite chargé et peu réutilisable.
Si possible, je préfère que chaque objet soit capable de s'enregistrer lui-même dans la BdD, et lui dire objet.update(in: db). L'avantage est que c'est plus léger, et bien localisé. L'inconvénient est qu'il n'y a pas une séparation claire entre la couche Métier et la couche Persistance.
Je ne suis pas sûr d'avoir répondu à ta question. Mais disons que mes contrôleurs sont peu concernés par la persistance qui est du ressort de la couche Modèle. Ce sont tout de fois bien les instigateurs des opérations de lecture et enregistrement.
Le problème n'est pas de tester le singleton, mais de tester les objets qui utilisent le singleton.
Un exemple simpliste:
Le test passe parfaitement chez moi, mais pas chez mon collègue Allemand, puisque sa machine a une Locale différente.
On peut ici résoudre le problème en passant une Locale explicitement, comme proposé par Jérémy:
Un exemple qui pose vraiment problème dans les tests, c'est quand on veut s'assurer qu'une méthode a bien été appelée sur le singleton. Je vous montrerai un autre jour.
Ah ? Quand ?
Dans un même ordre d'idée j'aurais aimé avoir votre avis dans le but de me proposer une meilleur solution. Je fabrique une partie de mes interfaces à la mano (entendre par là sans interface builder). Pour définir les marges entre les composants et dans un but d'avoir une interface cohérente et propre j'ai créé une classe qui s'apparente au code ci-dessous :
Pour l'utiliser j'ai juste à écrire le code suivant :
Mais nous sommes bien d'accord que, derrière un objet employant le design pattern singleton, nous sommes face à une variable globale ce qui n'est pas forcément top... D'ailleurs la frontière entre singleton et variable globale est souvent très mince.
L'idée est d'avoir à toucher le paramètre gap pour modifier l'apparence générale de l'application sans modifier une à une les différentes contraintes graphiques.
Auriez vous quelque chose de mieux à me proposer ?
L'inconvénient évident de ta solution est qu'on ne peut pas changer de UIProperty. Par exemple, si tu veux que le gap soit différent juste pour un composant.
L'alternative serait alors de passer l'UIProperty au composant graphique, par exemple à son init. ça supprime le singleton, mais évidemment, ça demande de passer l'UIProperty de proche en proche. Je ne dirais pas que c'est une meilleure solution.
Personnellement, je ne ferais ici de l'injection de dépendance que si je voulais écrire des tests unitaires. Mais dans ce cas précis, un tests UI serait peut-être plus indiqué.
Oui je vois le truc. A la limite je pourrais faire quelque chose comme ça :
Après j'avais également pensé à mettre une propriété calculé dans une extension de la classe UIView que j'aurais pu surcharger dans le cas d'une vue qui aurait un gap spécifique.