Injection de dépendance; bonnes pratiques pour réduire le code passe-partout

25

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 ?

Dinaiz
la source
5
J'ai posté cette question aux programmeurs au lieu de stackoverflow parce que, comme je l'ai compris, les programmeurs sont plus pour les quêtes de conception et stackoverflow est pour les questions "quand vous êtes devant votre compilateur". Désolé d'avance si je me trompe.
Dinaiz
2
la programmation orientée aspect, est également bonne pour ces tâches - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov
La classe A est-elle une usine pour les classes B, C et D? Sinon, pourquoi en crée-t-il des instances? Si seule la classe D a besoin d'objets Time, pourquoi injecteriez-vous une autre classe avec TimeFactory? La classe D a-t-elle vraiment besoin de TimeFactory, ou pourrait-on simplement lui injecter une instance de Time?
simoraman
D doit créer des objets Time, avec des informations que seul D est censé avoir. Je dois penser au reste de votre commentaire. Il pourrait y avoir un problème dans "qui crée qui" dans mon code
Dinaiz
"qui crée qui" est résolu par les conteneurs IoC, voir ma réponse pour plus de détails .. "qui crée qui" est l'une des meilleures questions que vous pouvez poser lorsque vous utilisez l'injection de dépendance.
GameDeveloper

Réponses:

23

Dans mon application, beaucoup de classes doivent créer des objets Time

Il semble que votre Timeclasse 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 comme stringdevait être injectée dans chaque partie du code qui utilise des chaînes, et que vous auriez besoin d'utiliser a stringFactorycomme 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 la Timeclasse elle-même, et quand c'est fait, utilisez-le partout dans votre programme, tout comme la stringclasse, ou une vectorclasse 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.

Doc Brown
la source
Vous avez tout à fait raison. "la lisibilité de votre programme diminuerait d'un ordre de grandeur." : c'est exactement ce qui s'est passé oui. Pourvu que je veuille encore utiliser une usine, ce n'est pas si mal d'en faire un singleton alors?
Dinaiz
3
@Dinaiz: l'idée est d'accepter un couplage étroit entre Timeet les autres parties de votre programme. Ainsi, vous pouvez également accepter un couplage étroit TimeFactory. Ce que j'éviterais cependant, c'est d'avoir un seul TimeFactoryobjet 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.
Doc Brown
En fait, il doit avoir un état, donc quand vous dites "Ne l'utilisez pas comme singleton", vous voulez dire "continuez à utiliser l'injection de dépendance, c'est-à-dire en passant l'instance à chaque objet qui en a besoin", n'est-ce pas?
Dinaiz
1
@Dinaiz: honnêtement, cela dépend - du type d'état, du type et du nombre de parties de votre programme utilisant Timeet TimeFactory, du degré d '"évolutivité" dont vous avez besoin pour les futures extensions liées à TimeFactory, et ainsi de suite.
Doc Brown
OK, je vais essayer de garder l'injection de dépendance, mais j'ai une conception plus intelligente qui ne nécessite pas autant de passe-partout alors! merci beaucoup doc '
Dinaiz
4

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.

C0deAttack
la source
L'ID peut concerner la lisibilité. Si vous avez une variable globale apparaissant "par magie" au milieu d'une méthode, cela n'aide pas à comprendre le code (du point de vue de la maintenance). D'où vient cette variable? À qui appartient-il? Qui l'initialise? Et ainsi de suite ... Pour votre deuxième point, vous avez probablement raison, mais je ne pense pas que cela s'applique dans mon cas. Merci d'avoir pris le temps de répondre
Dinaiz
DI augmente la lisibilité principalement parce que "nouveau / supprimer" sera supprimé par magie de votre code. Si vous n'avez pas à vous soucier du code d'allocation dynamique, c'est plus facile à lire.
GameDeveloper
Oubliez également de dire que cela rend le code un peu plus fort car il ne peut plus faire d'hypothèses sur "comment" une dépendance est créée, elle "en a juste besoin". Et cela rend la classe plus facile à maintenir .. voir ma réponse
GameDeveloper
3

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

molyss
la source
Le printemps est pour java et j'utilise C ++. Mais pourquoi 2 personnes vous ont-elles déçu? Il y a des points dans votre post avec
lesquels
Je vais supposer que le downvote était dû ne pas répondre à la question en soi, peut-être ainsi que la mention de Spring. Cependant, la suggestion d'utiliser un Singleton était valable dans mon esprit, et peut ne pas répondre à la question - mais suggère une alternative valide et soulève un problème de conception intéressant. Pour cette raison, j'ai +1.
Fergus In London
1

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"

Développeur de jeu
la source
Wow, je n'ai pas vu votre réponse avant, désolé! Monsieur très informatif! Vote positif
Dinaiz
0

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.

rmaruszewski
la source
-1

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.

krzychoo
la source
3
Répondre aux anciennes questions sur P.SE est une mauvaise façon d'annoncer votre projet. Considérez qu'il existe des dizaines de milliers de projets qui peuvent avoir une certaine pertinence pour une question. Si tous leurs auteurs en publiaient ici, la qualité de la réponse du site chuterait.