J'ai une question simple, et je ne suis même pas sûr qu'elle ait une réponse, mais essayons. Je code en C ++ et j'utilise l'injection de dépendances pour éviter un état global. Cela fonctionne assez bien et je n'ai pas souvent recours à des comportements inattendus / indéfinis.
Cependant, je me rends compte que, à mesure que mon projet se développe, j'écris beaucoup de code que je considère comme passe-partout. Pire: le fait qu'il y ait plus de code passe-partout que le code réel le rend parfois difficile à comprendre.
Rien ne vaut un bon exemple alors allons-y:
J'ai une classe appelée TimeFactory qui crée des objets Time.
Pour plus de détails (je ne suis pas sûr que ce soit pertinent): les objets Time sont assez complexes car le Time peut avoir différents formats, et la conversion entre eux n'est ni linéaire, ni simple. Chaque "Time" contient un synchroniseur pour gérer les conversions, et pour m'assurer qu'ils ont le même synchroniseur correctement initialisé, j'utilise une TimeFactory. TimeFactory n'a qu'une seule instance et est étendue à toute l'application, donc il serait admissible pour singleton mais, parce qu'il est mutable, je ne veux pas en faire un singleton
Dans mon application, de nombreuses classes doivent créer des objets Time. Parfois, ces classes sont profondément imbriquées.
Disons que j'ai une classe A qui contient des instances de classe B, et ainsi de suite jusqu'à la classe D. La classe D doit créer des objets Time.
Dans mon implémentation naïve, je passe la TimeFactory au constructeur de classe A, qui la passe au constructeur de classe B et ainsi de suite jusqu'à la classe D.
Maintenant, imaginez que j'ai quelques classes comme TimeFactory et quelques hiérarchies de classes comme celle ci-dessus: je perds toute la flexibilité et la lisibilité que je suppose pour obtenir en utilisant l'injection de dépendance.
Je commence à me demander s'il n'y a pas un défaut majeur de conception dans mon application ... Ou est-ce un mal nécessaire d'utiliser l'injection de dépendance?
Qu'est-ce que tu penses ?
la source
Réponses:
Il semble que votre
Time
classe soit un type de données très basique qui devrait appartenir à "l'infrastructure générale" de votre application. DI ne fonctionne pas bien pour de telles classes. Réfléchissez à ce que cela signifie si une classe commestring
devait être injectée dans chaque partie du code qui utilise des chaînes, et que vous auriez besoin d'utiliser astringFactory
comme seule possibilité de créer de nouvelles chaînes - la lisibilité de votre programme diminuerait d'un ordre de ordre de grandeur.Donc, ma suggestion: n'utilisez pas DI pour les types de données généraux comme
Time
. Écrivez des tests unitaires pour laTime
classe elle-même, et quand c'est fait, utilisez-le partout dans votre programme, tout comme lastring
classe, ou unevector
classe ou toute autre classe de la bibliothèque standard. Utilisez DI pour les composants qui doivent être vraiment découplés les uns des autres.la source
Time
et les autres parties de votre programme. Ainsi, vous pouvez également accepter un couplage étroitTimeFactory
. Ce que j'éviterais cependant, c'est d'avoir un seulTimeFactory
objet global avec un état (par exemple, une information locale ou quelque chose comme ça) - qui pourrait causer des effets secondaires désagréables à votre programme et rendre les tests généraux et la réutilisation très difficiles. Soit le rendre apatride, soit ne pas l'utiliser comme singleton.Time
etTimeFactory
, du degré d '"évolutivité" dont vous avez besoin pour les futures extensions liées àTimeFactory
, et ainsi de suite.Que voulez-vous dire par «je perds toute la flexibilité et la lisibilité que je suppose pour utiliser l'injection de dépendance» - DI n'est pas une question de lisibilité. Il s'agit de découpler la dépendance entre les objets.
Il semble que la classe A crée la classe B, la classe B crée la classe C et la classe C crée la classe D.
Ce que vous devriez avoir, c'est la classe B injectée dans la classe A. La classe C injectée dans la classe B. La classe D injectée dans la classe C.
la source
Je ne sais pas pourquoi vous ne voulez pas faire de votre fabrique de temps un singleton. S'il n'y en a qu'une seule instance dans l'ensemble de votre application, il s'agit de facto d'un singleton.
Cela étant dit, il est très dangereux de partager un objet mutable, sauf s'il est correctement protégé par des blocs de synchronisation, auquel cas, il n'y a aucune raison pour qu'il ne soit pas un singleton.
Si vous voulez faire l'injection de dépendances, vous voudrez peut-être regarder Spring ou d'autres frameworks d'injection de dépendances, qui vous permettraient d'affecter automatiquement des paramètres à l'aide d'une annotation
la source
Cela vise à être une réponse complémentaire à Doc Brown, et aussi à répondre aux commentaires sans réponse de Dinaiz qui sont toujours liés à la Question.
Ce dont vous avez probablement besoin, c'est d'un cadre pour faire DI. Avoir des hiérarchies complexes ne signifie pas nécessairement une mauvaise conception, mais si vous devez injecter une méthode ascendante TimeFactory (de A à D) au lieu d'injecter directement vers D, il y a probablement un problème avec la façon dont vous faites l'injection de dépendance.
Un singleton? Non merci. Si vous n'avez besoin que d'une seule istance pour la partager dans votre contexte d'application (l'utilisation d'un conteneur IoC pour DI comme Infector ++ nécessite simplement de lier TimeFactory en tant que istance unique), voici l'exemple (C ++ 11 soit dit en passant, mais C ++ donc. vers C ++ 11 déjà? Vous obtenez gratuitement une application sans fuite):
Maintenant, le bon point d'un conteneur IoC est que vous n'avez pas besoin de passer la fabrique de temps à D. si votre classe "D" a besoin de fabrique de temps, mettez simplement la fabrique de temps comme paramètre constructeur pour la classe D.
comme vous le voyez, vous n'injectez TimeFactory qu'une seule fois. Comment utiliser "A"? Très simple, chaque classe est injectée, construite dans le principal ou à distance avec une usine.
chaque fois que vous créez la classe A, elle sera automatiquement (istiation paresseuse) injectée avec toutes les dépendances jusqu'à D et D sera injectée avec TimeFactory, donc en appelant seulement 1 méthode, vous avez votre hiérarchie complète prête (et même les hiérarchies complexes sont résolues de cette façon enlever BEAUCOUP de code de plaque de chaudière): Vous n'avez pas à appeler "nouveau / supprimer" et c'est très important car vous pouvez séparer la logique d'application du code de colle.
C'est facile, votre TimeFactory a une méthode "create", alors utilisez simplement une signature différente "create (params)" et vous avez terminé. Les paramètres qui ne sont pas des dépendances sont souvent résolus de cette façon. Cela supprime également le devoir d'injecter des choses comme des "chaînes" ou des "entiers" car cela n'ajoute qu'une plaque de chaudière supplémentaire.
Qui crée qui? Le conteneur IoC crée des istances et des usines, les usines créent le reste (les usines peuvent créer différents objets avec des paramètres arbitraires, donc vous n'avez pas vraiment besoin d'un état pour les usines). Vous pouvez toujours utiliser les usines comme wrappers pour le conteneur IoC: en général, Injectin the IoC Container est très mauvais et revient à utiliser un localisateur de services. Certaines personnes ont résolu le problème en enveloppant le conteneur IoC avec une usine (ce n'est pas strictement nécessaire, mais a l'avantage que la hiérarchie est résolue par le conteneur et toutes vos usines deviennent encore plus faciles à entretenir).
N'abusez pas non plus de l'injection de dépendances, les types simples peuvent simplement être des membres de classe ou des variables de portée locale. Cela semble évident mais j'ai vu des gens injecter "std :: vector" juste parce qu'il y avait un framework DI qui permettait cela. Rappelez-vous toujours la loi de Demeter: "Injectez uniquement ce dont vous avez vraiment besoin pour injecter"
la source
Vos classes A, B et C doivent-elles également créer des instances de temps, ou uniquement de classe D? Si c'est seulement la classe D, alors A et B ne devraient rien savoir sur TimeFactory. Créez une instance de TimeFactory dans la classe C et passez-la à la classe D. Notez que par "créer une instance", je ne veux pas nécessairement dire que la classe C doit être responsable de l'instanciation de TimeFactory. Il peut recevoir DClassFactory de la classe B et DClassFactory sait comment créer une instance Time.
Une technique que j'utilise aussi souvent lorsque je n'ai pas de framework DI fournit deux constructeurs, un qui accepte une usine et un autre qui crée une usine par défaut. Le second a généralement un accès protégé / package et est principalement utilisé pour les tests unitaires.
la source
J'ai implémenté un autre cadre d'injection de dépendance C ++, qui a récemment été proposé pour boost - https://github.com/krzysztof-jusiak/di - la bibliothèque est sans macro (gratuite), en-tête uniquement, C ++ 03 / C ++ 11 / Bibliothèque C ++ 14 fournissant un type sûr, un temps de compilation, une injection de dépendance de constructeur sans macro.
la source