Modèle de conception d'injection de dépendance et de singleton

89

Comment identifier quand utiliser l'injection de dépendances ou le modèle de singleton. J'ai lu dans beaucoup de sites Web où ils disent "Utiliser l'injection de dépendance sur le modèle singleton". Mais je ne sais pas si je suis totalement d'accord avec eux. Pour mes projets à petite ou moyenne échelle, je vois vraiment l'utilisation du modèle singleton simple.

Par exemple Logger. Je pourrais utiliser Logger.GetInstance().Log(...) Mais, au lieu de cela, pourquoi ai-je besoin d'injecter chaque classe que je crée, avec l'instance de l'enregistreur ?.

SysAdmin
la source

Réponses:

66

Si vous souhaitez vérifier ce qui est consigné dans un test, vous avez besoin d'une injection de dépendances. De plus, un enregistreur est rarement un singleton - en général, vous avez un enregistreur pour chacune de vos classes.

Regardez cette présentation sur la conception orientée objet pour la testabilité et vous verrez pourquoi les singletons sont mauvais.

Le problème avec les singletons est qu'ils représentent un état global difficile à prévoir, en particulier dans les tests.

Gardez à l'esprit qu'un objet peut être de facto singleton mais toujours être obtenu via l'injection de dépendances, plutôt que via Singleton.getInstance().

Je ne fais qu'énumérer quelques points importants soulevés par Misko Hevery dans sa présentation. Après l' avoir vu , vous gagnerez plein perspective pourquoi il est préférable d'avoir un objet de définir ce que ses dépendances sont, mais pas définir d' une manière à créer eux.

Bozho
la source
89

Les singletons sont comme le communisme: ils sonnent tous les deux très bien sur le papier, mais explosent de problèmes dans la pratique.

Le modèle singleton met un accent disproportionné sur la facilité d'accès aux objets. Il évite complètement le contexte en exigeant que chaque consommateur utilise un objet de portée AppDomain, ne laissant aucune option pour différentes implémentations. Il intègre les connaissances de l'infrastructure dans vos classes (l'appel à GetInstance()) tout en ajoutant une puissance d'expression exactement nulle . Cela réduit en fait votre pouvoir expressif, car vous ne pouvez pas changer l'implémentation utilisée par une classe sans la changer pour toutes . Vous ne pouvez tout simplement pas ajouter des fonctionnalités uniques.

De plus, lorsque la classe Foodépend de Logger.GetInstance(), Foocache effectivement ses dépendances aux consommateurs. Cela signifie que vous ne pouvez pas le comprendre pleinement Fooou l'utiliser avec confiance à moins de lire sa source et de découvrir le fait dont il dépend Logger. Si vous n'avez pas la source, cela limite votre capacité à comprendre et à utiliser efficacement le code dont vous dépendez.

Le modèle singleton, tel qu'implémenté avec des propriétés / méthodes statiques, n'est guère plus qu'un hack autour de l'implémentation d'une infrastructure. Il vous limite de multiples façons tout en n'offrant aucun avantage perceptible par rapport aux alternatives. Vous pouvez l'utiliser comme vous le souhaitez, mais comme il existe des alternatives viables qui favorisent une meilleure conception, cela ne devrait jamais être une pratique recommandée.

Bryan Watts
la source
9
@BryanWatts tout cela dit, les singletons sont toujours beaucoup plus rapides et moins sujets aux erreurs dans une application normale à moyenne échelle. Habituellement, je n'ai pas plus d'une implémentation possible pour un enregistreur (personnalisé), alors pourquoi le ** aurais-je besoin de: 1. Créer une interface pour cela et la modifier à chaque fois que j'ai besoin d'ajouter / modifier des membres publics 2. Maintenir une configuration DI 3. Masquer le fait qu'il existe un seul objet de ce type dans tout le système 4. Me lier à une fonctionnalité stricte provoquée par une séparation prématurée des préoccupations.
Uri Abramson
@UriAbramson: Il semble que vous ayez déjà pris la décision concernant les compromis que vous préférez, donc je n'essaierai pas de vous convaincre.
Bryan Watts
@BryanWatts Ce n'est pas le cas, peut-être que mon commentaire était un peu trop dur, mais cela m'intéresse beaucoup d'entendre ce que vous avez à dire sur le point que j'ai soulevé ...
Uri Abramson
2
@UriAbramson: Très bien. Seriez-vous au moins d'accord pour dire que l'échange des implémentations pour l'isolement pendant les tests est important?
Bryan Watts
6
L'utilisation de singletons et l'injection de dépendances ne s'excluent pas mutuellement. Un singleton peut implémenter une interface, donc peut être utilisé pour satisfaire une dépendance sur une autre classe. Le fait qu'il s'agisse d'un singleton ne force pas chaque consommateur à obtenir une référence via sa méthode / propriété "GetInstance".
Oliver
18

D'autres ont très bien expliqué le problème des singletons en général. Je voudrais juste ajouter une note sur le cas spécifique de Logger. Je suis d'accord avec vous que ce n'est généralement pas un problème d'accéder à un enregistreur (ou au root logger, pour être précis) en tant que singleton, via une méthode statique getInstance()ou getRootLogger(). (sauf si vous voulez voir ce qui est consigné par la classe que vous testez - mais d'après mon expérience, je peux difficilement me souvenir de tels cas où cela était nécessaire. Là encore, pour d'autres, cela pourrait être une préoccupation plus urgente).

IMO généralement un enregistreur singleton n'est pas un souci, car il ne contient aucun état pertinent pour la classe que vous testez. Autrement dit, l'état de l'enregistreur (et ses modifications possibles) n'ont aucun effet sur l'état de la classe testée. Cela ne rend donc pas vos tests unitaires plus difficiles.

L'alternative serait d'injecter le logger via le constructeur, dans (presque) toutes les classes de votre application. Pour la cohérence des interfaces, il doit être injecté même si la classe en question ne consigne rien pour le moment - l'alternative serait que lorsque vous découvrez à un moment donné que vous devez maintenant enregistrer quelque chose de cette classe, vous avez besoin d'un enregistreur, donc vous devez ajouter un paramètre de constructeur pour DI, cassant tout le code client. Je n'aime pas ces deux options et je pense que l'utilisation de DI pour la journalisation ne ferait que me compliquer la vie afin de me conformer à une règle théorique, sans aucun avantage concret.

Donc, ma conclusion est la suivante: une classe qui est utilisée (presque) universellement, mais qui ne contient pas d'état pertinent pour votre application, peut être implémentée en toute sécurité en tant que Singleton .

Péter Török
la source
1
Même dans ce cas, un singleton peut être pénible. Si vous réalisez que votre enregistreur a besoin d'un paramètre supplémentaire dans certains cas, vous pouvez soit créer une nouvelle méthode (et laisser l'enregistreur devenir plus laid), soit casser tous les consommateurs de l'enregistreur, même s'ils ne s'en soucient pas.
kyoryu
4
@kyoryu, je parle du cas «habituel», ce qui à mon humble avis implique l'utilisation d'un cadre de journalisation standard (de facto). (Qui sont généralement configurables via des fichiers de propriété / XML btw.) Bien sûr, il y a des exceptions - comme toujours. Si je sais que mon application est exceptionnelle à cet égard, je n'utiliserai pas de Singleton. Mais la suringénierie en disant "cela pourrait être utile parfois" est presque toujours un effort inutile.
Péter Török
Si vous utilisez déjà DI, ce n'est pas beaucoup d'ingénierie supplémentaire. (BTW, je ne suis pas en désaccord avec vous, je suis le vote positif). De nombreux enregistreurs nécessitent des informations de «catégorie» ou quelque chose du genre, et l'ajout de paramètres supplémentaires peut causer des problèmes. Le cacher derrière une interface peut aider à garder le code consommateur propre et peut faciliter le passage à un autre framework de journalisation.
kyoryu
1
@kyoryu, désolé pour la mauvaise hypothèse. J'ai vu un vote négatif et un commentaire, alors j'ai connecté les points - dans le mauvais sens, car il s'avère :-( Je n'ai jamais ressenti le besoin de passer à un autre cadre de journalisation, mais là encore, je comprends que cela peut être légitime préoccupation dans certains projets.
Péter Török
5
Corrigez-moi si je me trompe, mais le singleton serait pour la LogFactory, pas pour l'enregistreur. De plus, la LogFactory sera probablement la façade de journalisation Apache commons ou Slf4j. Donc, changer d'implémentation de journalisation est de toute façon indolore. Le vrai problème avec l'utilisation de DI pour injecter une LogFactory n'est-il pas le fait que vous devez maintenant accéder à applicationContext pour créer chaque instance de votre application?
HDave
9

Il s'agit principalement, mais pas entièrement, de tests. Les singltons étaient populaires car il était facile de les consommer, mais les singletons présentent un certain nombre d'inconvénients.

  • Difficile à tester. Signification comment puis-je m'assurer que l'enregistreur fait la bonne chose.
  • Difficile à tester. Cela signifie que si je teste du code qui utilise l'enregistreur, mais que ce n'est pas l'objet de mon test, je dois toujours m'assurer que mon env de test prend en charge l'enregistreur
  • Parfois, vous ne voulez pas de célibataire, mais plus de flexibilité

DI vous permet de consommer facilement vos classes dépendantes - il suffit de le mettre dans les arguments du constructeur, et le système le fournit pour vous - tout en vous offrant la flexibilité de test et de construction.

Scott Weinstein
la source
Pas vraiment. Il s'agit d'un état mutable partagé et de dépendances statiques qui rendent les choses douloureuses à long terme. Les tests ne sont que l'exemple évident et souvent le plus douloureux.
kyoryu
2

À peu près le seul moment où vous devriez utiliser un Singleton au lieu de Dependency Injection est si le Singleton représente une valeur immuable, telle que List.Empty ou similaire (en supposant des listes immuables).

La vérification intestinale d'un Singleton devrait être "Serais-je d'accord si c'était une variable globale au lieu d'un Singleton?" Sinon, vous utilisez le modèle Singleton pour couvrir une variable globale et vous devriez envisager une approche différente.

kyoryu
la source
1

Je viens de consulter l'article de Monostate - c'est une alternative intéressante à Singleton, mais il a des propriétés étranges:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

N'est-ce pas effrayant - parce que le mappeur dépend vraiment de la connexion à la base de données pour effectuer save () - mais si un autre mappeur a été créé auparavant - il peut ignorer cette étape dans l'acquisition de ses dépendances. Bien que soigné, c'est aussi un peu désordonné, n'est-ce pas?

Sunwukung
la source