Si les singletons sont mauvais, pourquoi un conteneur de service est-il bon?

91

Nous savons tous à quel point les singletons sont mauvais parce qu'ils cachent des dépendances et pour d' autres raisons .

Mais dans un framework, il peut y avoir de nombreux objets qui ne doivent être instanciés qu'une seule fois et appelés de partout (logger, db, etc.).

Pour résoudre ce problème, on m'a dit d'utiliser un soi-disant "Objects Manager" (ou Service Container comme symfony) qui stocke en interne toutes les références aux Services (enregistreur, etc.).

Mais pourquoi un fournisseur de services n'est-il pas aussi mauvais qu'un pur Singleton?

Le fournisseur de services masque également les dépendances et termine simplement la création de la première istance. J'ai donc vraiment du mal à comprendre pourquoi nous devrions utiliser un fournisseur de services au lieu de singletons.

PS. Je sais que pour ne pas cacher les dépendances, je devrais utiliser DI (comme indiqué par Misko)

Ajouter

J'ajouterais: ces jours-ci, les singletons ne sont pas si maléfiques, le créateur de PHPUnit l'a expliqué ici:

DI + Singleton résout le problème:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

c'est assez intelligent même si cela ne résout pas du tout tous les problèmes.

À part DI et Service Container, existe-t-il une bonne solution acceptable pour accéder à ces objets d'aide?

dynamique
la source
2
@oui Votre modification fait de fausses hypothèses. Sebastian ne suggère en aucun cas que l'extrait de code rend l'utilisation de Singleons moins problématique. C'est juste une façon de rendre le code qu'il serait autrement impossible de tester plus testable. Mais c'est toujours du code problématique. En fait, il note explicitement: "Juste parce que vous pouvez, ne signifie pas que vous devriez". La bonne solution serait toujours de ne pas utiliser du tout les Singletons.
Gordon
3
@oui suit le principe SOLID.
Gordon
19
Je conteste l'affirmation selon laquelle les singletons sont mauvais. Ils peuvent être mal utilisés, oui, tout comme n'importe quel outil. Un scalpel peut être utilisé pour sauver une vie ou y mettre fin. Une tronçonneuse peut dégager les forêts pour éviter les feux de brousse ou elle peut couper une partie importante de votre bras si vous ne savez pas ce que vous faites. Apprenez à utiliser vos outils à bon escient et ne considérez pas les conseils comme de l'Évangile - c'est ainsi que réside l'esprit irréfléchi.
paxdiablo
4
@paxdiablo mais ils sont mauvais. Les singletons enfreignent SRP, OCP et DIP. Ils introduisent un état global et un couplage étroit dans votre application et feront mentir votre API sur ses dépendances. Tout cela affectera négativement la maintenabilité, la lisibilité et la testabilité de votre code. Il peut y avoir de rares cas où ces inconvénients l'emportent sur les petits avantages, mais je dirais que dans 99%, vous n'avez pas besoin d'un Singleton. Surtout en PHP où les singletons ne sont de toute façon uniques que pour la requête et il est très simple d'assembler des graphiques de collaborateur à partir d'un constructeur.
Gordon
5
Non, je ne le crois pas. Un outil est un moyen d'exécuter une fonction, généralement en la facilitant d'une manière ou d'une autre, bien que certains (emacs?) Aient la rare distinction de la rendre plus difficile :-) En cela, un singleton n'est pas différent d'un arbre équilibré ou d'un compilateur . Si vous ne devez garantir qu'une seule copie d'un objet, un singleton le fait. On peut débattre de la question de savoir si cela fonctionne bien, mais je ne pense pas que vous puissiez soutenir que cela ne le fait pas du tout. Et il peut y avoir de meilleurs moyens, comme une scie à chaîne plus rapide qu'une scie à main, ou un pistolet à clous par rapport à un marteau. Cela ne rend pas la scie à main / marteau moins un outil.
paxdiablo

Réponses:

76

Service Locator est juste le moindre de deux maux pour ainsi dire. Le «moindre» se résumant à ces quatre différences ( du moins, je ne peux penser à aucune autre pour le moment ):

Principe de responsabilité unique

Le conteneur de services ne viole pas le principe de responsabilité unique comme le fait Singleton. Les singletons mélangent la création d'objets et la logique métier, tandis que le conteneur de services est strictement responsable de la gestion des cycles de vie des objets de votre application. À cet égard, Service Container est meilleur.

Couplage

Les singletons sont généralement codés en dur dans votre application en raison des appels de méthode statiques, ce qui conduit à des dépendances étroitement couplées et difficiles à simuler dans votre code. Le SL en revanche n'est qu'une classe et il peut être injecté. Ainsi, bien que tous vos classifiés en dépendent, au moins c'est une dépendance faiblement couplée. Donc, à moins que vous n'implémentiez le ServiceLocator en tant que Singleton lui-même, c'est un peu mieux et aussi plus facile à tester.

Cependant, toutes les classes utilisant le ServiceLocator dépendront désormais du ServiceLocator, qui est également une forme de couplage. Cela peut être atténué en utilisant une interface pour ServiceLocator afin que vous ne soyez pas lié à une implémentation concrète de ServiceLocator, mais vos classes dépendront de l'existence d'une sorte de localisateur alors que le fait de ne pas utiliser de ServiceLocator augmente considérablement la réutilisation.

Dépendances cachées

Le problème du masquage des dépendances existe cependant. Lorsque vous injectez simplement le localisateur dans vos classes consommatrices, vous ne connaîtrez aucune dépendance. Mais contrairement au Singleton, le SL instanciera généralement toutes les dépendances nécessaires dans les coulisses. Ainsi, lorsque vous récupérez un Service, vous ne vous retrouvez pas comme Misko Hevery dans l'exemple de CreditCard , par exemple , vous n'avez pas à instancier toutes les dépendances des dépendances à la main.

Récupérer les dépendances à l'intérieur de l'instance enfreint également la loi de Demeter , qui stipule que vous ne devez pas fouiller dans les collaborateurs. Une instance ne doit parler qu'à ses collaborateurs immédiats. Il s'agit d'un problème avec Singleton et ServiceLocator.

État mondial

Le problème de l'état global est également quelque peu atténué car lorsque vous instanciez un nouveau localisateur de service entre les tests, toutes les instances créées précédemment sont également supprimées (sauf si vous avez fait l'erreur et les avez enregistrées dans des attributs statiques dans le SL). Cela n'est vrai pour aucun état global dans les classes gérées par le SL, bien sûr.

Voir également Fowler sur Service Locator vs Dependency Injection pour une discussion beaucoup plus approfondie.


Une note sur votre mise à jour et l'article lié de Sebastian Bergmann sur le test de code qui utilise des Singletons : Sebastian ne suggère en aucun cas que la solution de contournement proposée rend l'utilisation de Singleons moins problématique. C'est juste une façon de rendre le code qu'il serait autrement impossible de tester plus testable. Mais c'est toujours du code problématique. En fait, il note explicitement: "Juste parce que vous pouvez, ne signifie pas que vous devriez".

Gordon
la source
1
En particulier, la testabilité doit être appliquée ici. Vous ne pouvez pas vous moquer des appels de méthode statiques. Vous pouvez cependant simuler des services qui ont été injectés via un constructeur ou un setter.
David
44

Le modèle de localisation de service est un anti-modèle. Cela ne résout pas le problème de l'exposition des dépendances (vous ne pouvez pas dire en regardant la définition d'une classe quelles sont ses dépendances car elles ne sont pas injectées, au lieu de cela, elles sont retirées du localisateur de services).

Votre question est donc la suivante: pourquoi les localisateurs de services sont-ils bons? Ma réponse est: ils ne le sont pas.

Évitez, évitez, évitez.

Jason
la source
6
On dirait que vous ne savez rien sur les interfaces. La classe décrit simplement l'interface nécessaire dans la signature du constructeur - et c'est tout ce qu'il a besoin de savoir. Passed Service Locator devrait implémenter l'interface, c'est tout. Et si l'EDI vérifie l'implémentation de l'interface, il sera assez facile de contrôler les changements.
OZ_
4
@ yes123: Les gens qui disent que c'est faux, et ils se trompent parce que SL est un anti-modèle. Votre question est "pourquoi SL est-il bon?" Ma réponse est: ils ne le sont pas.
jason
5
Je ne discuterai pas si SL est un anit-pattern ou non, mais ce que je dirai, c'est que c'est beaucoup moins de maux par rapport au singleton et aux globals. Vous ne pouvez pas tester une classe qui dépend d'un singleton, mais vous pouvez certainement tester une classe qui dépend d'un SL (albiet vous pouvez bousiller la conception SL au point où cela ne fonctionne pas) ... Alors ça vaut le coup notant ...
ircmaxell
3
@Jason vous devez passer un objet qui implémente Interface - et ce n'est que ce que vous devez savoir. Vous vous limitez uniquement à la définition du constructeur de classe et souhaitez écrire dans le constructeur toutes les classes (pas les interfaces) - c'est une idée stupide. Tout ce dont vous avez besoin est une interface. Vous pouvez tester avec succès cette classe avec des simulacres, vous pouvez facilement changer de comportement sans changer de code, il n'y a pas de dépendances et de couplage supplémentaires - c'est tout (en général) ce que nous voulons avoir dans Dependency Injection.
OZ_
2
Bien sûr, je vais simplement jeter ensemble Database, Logger, Disk, Template, Cache et User dans un seul objet "Input", il sera sûrement plus facile de dire sur quelles dépendances mon objet s'appuie que si j'avais utilisé un conteneur.
Mahn
4

Le conteneur de services masque les dépendances comme le fait le modèle Singleton. Vous voudrez peut-être suggérer d'utiliser des conteneurs d'injection de dépendances à la place, car il présente tous les avantages du conteneur de services mais aucun (à ma connaissance) des inconvénients du conteneur de services.

Pour autant que je sache, la seule différence entre les deux est que dans le conteneur de services, le conteneur de services est l'objet injecté (masquant ainsi les dépendances), lorsque vous utilisez DIC, le DIC injecte les dépendances appropriées pour vous. La classe gérée par le DIC est complètement inconsciente du fait qu'elle est gérée par un DIC, vous avez donc moins de couplage, des dépendances claires et des tests unitaires heureux.

C'est une bonne question à SO expliquant la différence des deux: Quelle est la différence entre les modèles d'injection de dépendances et de localisateur de service?

rickchristie
la source
"le DIC injecte les dépendances appropriées pour vous" Cela ne se produit-il pas aussi avec Singleton?
dynamique
5
@ yes123 - Si vous utilisez un Singleton, vous ne l'injecteriez pas, la plupart du temps vous y accéderiez globalement (c'est le but de Singleton). Je suppose que si vous dites que si vous injectez le Singleton, cela ne cachera pas les dépendances, mais cela va à l'encontre de l'objectif initial du modèle Singleton - vous vous demanderiez, si je n'ai pas besoin d'accéder à cette classe globalement, pourquoi dois-je faire Singleton?
rickchristie
2

Parce que vous pouvez facilement remplacer des objets dans Service Container par
1) héritage (la classe Object Manager peut être héritée et les méthodes peuvent être remplacées)
2) modification de la configuration (dans le cas de Symfony)

Et les singletons sont mauvais non seulement à cause d'un couplage élevé, mais parce qu'ils sont des _tons simples . C'est une mauvaise architecture pour presque toutes sortes d'objets.

Avec DI 'pure' (dans les constructeurs), vous paierez un très gros prix - tous les objets doivent être créés avant d'être passés dans le constructeur. Cela signifiera plus de mémoire utilisée et moins de performances. De plus, l'objet ne peut pas toujours être simplement créé et passé dans le constructeur - une chaîne de dépendances peut être créée ... Mon anglais n'est pas assez bon pour en discuter complètement, lisez-le dans la documentation Symfony.

OZ_
la source
0

Pour moi, j'essaie d'éviter les constantes globales, les singletons pour une raison simple, il y a des cas où je pourrais avoir besoin de faire fonctionner des API.

Par exemple, j'ai front-end et admin. À l'intérieur de l'administrateur, je veux qu'ils puissent se connecter en tant qu'utilisateur. Considérez le code dans admin.

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

Cela peut établir une nouvelle connexion à la base de données, un nouvel enregistreur, etc. pour l'initialisation du frontend et vérifier si l'utilisateur existe réellement, valide, etc. Il utiliserait également des services de cookies et de localisation séparés appropriés.

Mon idée de singleton est - Vous ne pouvez pas ajouter deux fois le même objet à l'intérieur du parent. Par exemple

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

vous laisserait une seule instance et les deux variables pointant vers elle.

Enfin, si vous souhaitez utiliser le développement orienté objet, travaillez avec des objets, pas avec des classes.

romaninsh
la source
1
donc votre méthode est de passer la $api var autour de votre framework? Je n'ai pas vraiment compris ce que tu veux dire. De plus, si l'appel add('Logger')renvoie la même instance, vous avez essentiellement un cotainer de service
dynamique
Oui c'est vrai. Je les appelle "Contrôleur système" et ils sont destinés à améliorer les fonctionnalités de l'API. De la même manière, ajouter deux fois un contrôleur "Auditable" à un modèle fonctionnerait exactement de la même manière - ne créer qu'une seule instance et un seul ensemble de champs d'audit.
romaninsh