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 (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. La TimeFactory n'a qu'une seule instance et est étendue à l'application, donc elle serait admissible à singleton mais, parce qu'elle 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 ?
Dans mon application, beaucoup de classes doivent créer des objets Time
Il semble que votre classe Time
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. Pensez à ce que cela signifie si une classe comme string
devait être injectée dans chaque partie du code qui utilise des chaînes, et vous auriez besoin d'utiliser un stringFactory
comme seule possibilité de créer de nouvelles chaînes - la lisibilité de votre programme diminuerait d'un 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 la classe Time
elle-même, et quand c'est fait, utilisez-le partout dans votre programme, tout comme la classe string
, ou une classe vector
ou toute autre classe de la lib standard. Utilisez DI pour les composants qui doivent être vraiment découplés les uns des autres.
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. Classe C injectée dans la classe B. Classe D injectée dans la classe C.
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
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):
Infector::Container ioc; //your app's context
ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor
// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();
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.
ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D
//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A
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.
auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time
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.
D peut créer des objets Time avec des informations que seul D peut avoir
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).
//factory method
std::unique_ptr<myType> create(params){
auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
istance->setParams(params); //the customization you needed
return std::move(istance);
}
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"
Vos classes A, B et C doivent-elles également créer des instances Time, ou uniquement la classe D? S'il ne s'agit que de classe D, alors A et B ne devraient rien savoir de 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.