L'injection de dépendance doit-elle se faire au détriment de l'encapsulation?


128

Si je comprends bien, le mécanisme typique pour l'injection de dépendances consiste à injecter soit via le constructeur d'une classe, soit via une propriété publique (membre) de la classe.

Cela expose la dépendance injectée et viole le principe d'encapsulation de la POO.

Ai-je raison d'identifier ce compromis? Comment gérez-vous ce problème?

Veuillez également voir ma réponse à ma propre question ci-dessous.


7
c'est une question très intelligente à mon humble avis
dfa

5
Répondre à cette question nécessite d'abord un argument sur ce que signifie l' encapsulation . ;)
Jeff Sternal

2
L'encapsulation est maintenue par les interfaces. Ils exposent les caractéristiques essentielles de l'objet et masquent des détails tels que les dépendances. Cela nous permet «d'ouvrir» les classes dans une certaine mesure afin de fournir une configuration plus flexible.
Lawrence Wagerfield

Réponses:


62

Il y a une autre façon d'aborder ce problème qui pourrait vous intéresser.

Lorsque nous utilisons l'injection d'IoC / de dépendances, nous n'utilisons pas de concepts POO. Certes, nous utilisons un langage OO comme «hôte», mais les idées derrière l'IoC viennent de l'ingénierie logicielle orientée composants, et non de l'OO.

Le logiciel de composants consiste à gérer les dépendances - un exemple couramment utilisé est le mécanisme d'assemblage de .NET. Chaque assembly publie la liste des assemblys auxquels il fait référence, ce qui facilite grandement le regroupement (et la validation) des éléments nécessaires à une application en cours d'exécution.

En appliquant des techniques similaires dans nos programmes OO via IoC, nous visons à rendre les programmes plus faciles à configurer et à maintenir. La publication des dépendances (en tant que paramètres de constructeur ou autre) en est un élément clé. L'encapsulation ne s'applique pas vraiment, car dans le monde orienté composants / services, il n'y a pas de «type d'implémentation» pour que les détails fuient.

Malheureusement, nos langages ne séparent actuellement pas les concepts orientés objet à granularité fine de ceux orientés composants plus grossiers, c'est donc une distinction que vous devez garder à l'esprit uniquement :)


18
L'encapsulation n'est pas simplement un élément de terminologie sophistiquée. C'est une chose réelle avec de réels avantages, et peu importe si vous considérez votre programme comme "orienté composants" ou "orienté objet". L'encapsulation est censée protéger l'état de votre objet / composant / service / quoi que ce soit d'être modifié de manière inattendue, et IoC enlève une partie de cette protection, il y a donc certainement un compromis.
Ron Inbar

1
Les arguments fournis via un constructeur tombent toujours dans le domaine des manières attendues selon lesquelles l'objet peut être "changé": ils sont explicitement exposés et les invariants qui les entourent sont appliqués. La dissimulation d'informations est le meilleur terme pour désigner le type de confidentialité auquel vous faites référence, @RonInbar, et ce n'est pas nécessairement toujours bénéfique (cela rend les pâtes plus difficiles à démêler ;-)).
Nicholas Blumhardt

2
L'intérêt de la POO est que l'enchevêtrement des pâtes est séparé en classes séparées et que vous n'avez à le manipuler que si c'est le comportement de cette classe particulière que vous souhaitez modifier (c'est ainsi que la POO atténue la complexité). Une classe (ou un module) encapsule ses éléments internes tout en exposant une interface publique pratique (c'est ainsi que la POO facilite la réutilisation). Une classe qui expose ses dépendances via des interfaces crée de la complexité pour ses clients et est par conséquent moins réutilisable. Il est également intrinsèquement plus fragile.
Neutrino

1
Quelle que soit la façon dont je le regarde, il me semble que DI sape sérieusement certains des avantages les plus précieux de la POO, et je n'ai pas encore rencontré de situation où je l'ai trouvé utilisé d'une manière qui a résolu un réel problème existant.
Neutrino

1
Voici une autre façon de définir le problème: imaginez si les assemblys .NET choisissaient «l'encapsulation» et ne déclaraient pas les autres assemblys sur lesquels ils s'appuyaient. Ce serait une situation folle, lire des documents et espérer juste que quelque chose fonctionne après le chargement. La déclaration de dépendances à ce niveau permet aux outils automatisés de gérer la composition à grande échelle de l'application. Vous devez plisser les yeux pour voir l'analogie, mais des forces similaires s'appliquent au niveau des composants. Il y a des compromis, et YMMV comme toujours :-)
Nicholas Blumhardt

29

C'est une bonne question - mais à un moment donné, l'encapsulation dans sa forme la plus pure doit être violée si l'objet veut que sa dépendance soit remplie. Certains fournisseurs de la dépendance doivent savoir à la fois que l'objet en question nécessite un Foo, et que le fournisseur doit avoir un moyen de fournir le Fooà l'objet.

Classiquement, ce dernier cas est géré comme vous le dites, via des arguments de constructeur ou des méthodes de définition. Cependant, ce n'est pas nécessairement vrai - je sais que les dernières versions du framework Spring DI en Java, par exemple, vous permettent d'annoter des champs privés (par exemple avec @Autowired) et la dépendance sera définie par réflexion sans que vous ayez besoin d'exposer la dépendance via l'une des classes méthodes / constructeurs publics. C'est peut-être le genre de solution que vous recherchiez.

Cela dit, je ne pense pas non plus que l'injection de constructeur soit vraiment un problème. J'ai toujours pensé que les objets devraient être pleinement valides après la construction, de sorte que tout ce dont ils ont besoin pour remplir leur rôle (c'est-à-dire être dans un état valide) devrait de toute façon être fourni par le constructeur. Si vous avez un objet qui nécessite un collaborateur pour fonctionner, il me semble bien que le constructeur annonce publiquement cette exigence et s'assure qu'elle est remplie lorsqu'une nouvelle instance de la classe est créée.

Idéalement, lorsque vous traitez avec des objets, vous interagissez avec eux via une interface de toute façon, et plus vous faites cela (et avez des dépendances câblées via DI), moins vous avez à traiter vous-même avec les constructeurs. Dans la situation idéale, votre code ne traite ni ne crée jamais d'instances concrètes de classes; donc on lui donne juste une IFooDI via, sans se soucier de ce que le constructeur d ' FooImplindique dont il a besoin pour faire son travail, et en fait sans même être conscient de son FooImplexistence. De ce point de vue, l'encapsulation est parfaite.

C'est une opinion bien sûr, mais à mon avis, la DI ne viole pas nécessairement l'encapsulation et peut en fait l'aider en centralisant toutes les connaissances nécessaires sur les internes en un seul endroit. Non seulement c'est une bonne chose en soi, mais encore mieux cet endroit est en dehors de votre propre base de code, donc aucun code que vous écrivez n'a besoin de connaître les dépendances des classes.


2
Bons points. Je déconseille d'utiliser @Autowired sur des champs privés; cela rend la classe difficile à tester; comment injectez-vous ensuite des faux ou des talons?
lumpynose

4
Je ne suis pas d'accord. DI viole l'encapsulation, et cela peut être évité. Par exemple, en utilisant un ServiceLocator, qui n'a évidemment pas besoin de savoir quoi que ce soit sur la classe client; il a seulement besoin de connaître les implémentations de la dépendance Foo. Mais la meilleure chose, dans la plupart des cas, est d'utiliser simplement l'opérateur "new".
Rogério

5
@Rogerio - On peut soutenir que tout framework DI agit exactement comme le ServiceLocator que vous décrivez; le client ne sait rien de spécifique sur les implémentations Foo, et l'outil DI ne sait rien de spécifique sur le client. Et utiliser "new" est bien pire pour violer l'encapsulation, car vous devez connaître non seulement la classe d'implémentation exacte, mais aussi les classes et instances exactes de toutes les dépendances dont elle a besoin.
Andrzej Doyle

4
Utiliser "new" pour instancier une classe d'assistance, qui n'est souvent même pas publique, favorise l'encapsulation. L'alternative DI serait de rendre la classe d'assistance publique et d'ajouter un constructeur ou un setter public dans la classe client; les deux changements interrompraient l'encapsulation fournie par la classe d'assistance d'origine.
Rogério

1
"C'est une bonne question - mais à un moment donné, l'encapsulation dans sa forme la plus pure doit être violée si l'objet veut que sa dépendance soit remplie" Le principe fondateur de votre réponse est tout simplement faux. Comme @ Rogério déclare la création d'une dépendance en interne, et toute autre méthode où un objet satisfait en interne ses propres dépendances ne viole pas l'encapsulation.
Neutrino

17

Cela expose la dépendance injectée et viole le principe d'encapsulation de la POO.

Eh bien, franchement, tout viole l'encapsulation. :) C'est une sorte de principe tendre qui doit être bien traité.

Alors, qu'est-ce qui viole l'encapsulation?

L'héritage fait .

"Parce que l'héritage expose une sous-classe aux détails de l'implémentation de son parent, on dit souvent que 'l'héritage rompt l'encapsulation'". (Gang of Four 1995: 19)

La programmation orientée aspect le fait . Par exemple, vous enregistrez le rappel onMethodCall () et cela vous donne une excellente opportunité d'injecter du code dans l'évaluation de la méthode normale, en ajoutant des effets secondaires étranges, etc.

La déclaration d'ami en C ++ le fait .

L'extension de classe dans Ruby le fait . Redéfinissez simplement une méthode de chaîne quelque part après la définition complète d'une classe de chaîne.

Eh bien, beaucoup de choses le font .

L'encapsulation est un bon et important principe. Mais pas le seul.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
"Ce sont mes principes, et si vous ne les aimez pas ... eh bien, j'en ai d'autres." (Groucho Marx)
Ron Inbar

2
Je pense que c'est en quelque sorte le point. C'est l'injection dépendante vs l'encapsulation. N'utilisez donc l'injection dépendante que là où elle donne des avantages significatifs. C'est la DI partout qui donne une mauvaise réputation à DI
Richard Tingle

Je ne sais pas ce que cette réponse essaie de dire ... Que c'est correct de violer l'encapsulation lors de la DI, que c'est "toujours ok" parce que cela serait violé de toute façon, ou simplement que DI pourrait être une raison de violer l'encapsulation? Par ailleurs, de nos jours, il n'est plus nécessaire de s'appuyer sur des constructeurs ou des propriétés publics pour injecter des dépendances; à la place, nous pouvons injecter dans des champs annotés privés , ce qui est plus simple (moins de code) et préserve l'encapsulation. Ainsi, nous pouvons profiter des deux principes en même temps.
Rogério

L'héritage ne viole pas en principe l'encapsulation, bien qu'il puisse le faire si la classe parente est mal écrite. Les autres points que vous soulevez sont un paradigme de programmation assez marginal et plusieurs fonctionnalités du langage qui ont peu à voir avec l'architecture ou le design.
Neutrino

13

Oui, DI viole l'encapsulation (également connue sous le nom de «masquage d'informations»).

Mais le vrai problème survient lorsque les développeurs l'utilisent comme excuse pour violer les principes de KISS (Keep It Short and Simple) et YAGNI (You Ain't Gonna Need It).

Personnellement, je préfère des solutions simples et efficaces. J'utilise principalement l'opérateur "new" pour instancier des dépendances avec état quand et où elles sont nécessaires. Il est simple, bien encapsulé, facile à comprendre et facile à tester. Alors pourquoi pas?


Penser à l'avenir n'est pas terrible, mais je suis d'accord Keep It Simple, Stupid, surtout si vous n'en aurez pas besoin! J'ai vu des développeurs gaspiller des cycles parce qu'ils sont sur la conception de quelque chose pour être trop à l'épreuve du temps, et en se basant sur des intuitions à cela, pas même des exigences commerciales connues / suspectées.
Jacob McKay

5

Un bon conteneur / système d'injection de dépendance permettra l'injection du constructeur. Les objets dépendants seront encapsulés et n'ont pas du tout besoin d'être exposés publiquement. En outre, en utilisant un système DP, aucun de votre code ne "connaît" même les détails de la façon dont l'objet est construit, y compris peut-être même l'objet en cours de construction. Il y a plus d'encapsulation dans ce cas puisque la quasi-totalité de votre code est non seulement protégée de la connaissance des objets encapsulés, mais ne participe même pas à la construction des objets.

Maintenant, je suppose que vous comparez avec le cas où l'objet créé crée ses propres objets encapsulés, très probablement dans son constructeur. Ma compréhension de DP est que nous voulons retirer cette responsabilité de l'objet et la donner à quelqu'un d'autre. À cette fin, le «quelqu'un d'autre», qui est le conteneur DP dans ce cas, a une connaissance intime qui «viole» l'encapsulation; l'avantage est qu'il tire cette connaissance de l'objet, lui-même. Quelqu'un doit l'avoir. Le reste de votre application ne le fait pas.

Je penserais de cette façon: le conteneur / système d'injection de dépendance viole l'encapsulation, mais pas votre code. En fait, votre code est plus «encapsulé» que jamais.


3
Si vous avez une situation où l'objet client PEUT instancier directement ses dépendances, alors pourquoi ne pas le faire? C'est certainement la chose la plus simple à faire et ne réduit pas nécessairement la testabilité. Outre la simplicité et une meilleure encapsulation, cela facilite également l'utilisation d'objets avec état au lieu de singletons sans état.
Rogério

1
En plus de ce que @ Rogério a dit, c'est aussi potentiellement beaucoup plus efficace. Toutes les classes jamais créées dans l'histoire du monde n'avaient pas besoin d'avoir chacune de ses dépendances instanciées pendant toute la durée de vie de l'objet propriétaire. Un objet utilisant DI perd le contrôle le plus élémentaire de ses propres dépendances, à savoir leur durée de vie.
Neutrino

5

Comme Jeff Sternal l'a souligné dans un commentaire à la question, la réponse dépend entièrement de la façon dont vous définissez l' encapsulation .

Il semble y avoir deux principaux camps de ce que signifie l'encapsulation:

  1. Tout ce qui concerne l'objet est une méthode sur un objet. Ainsi, un Fileobjet peut avoir des méthodes à Save, Print, Display, ModifyText, etc.
  2. Un objet est son propre petit monde et ne dépend pas du comportement extérieur.

Ces deux définitions sont en contradiction directe l'une avec l'autre. Si un Fileobjet peut s'imprimer, cela dépendra fortement du comportement de l'imprimante. D'un autre côté, s'il connaît simplement quelque chose qui peut imprimer pour lui (une IFilePrinterou une interface de ce type), alors l' Fileobjet n'a pas besoin de savoir quoi que ce soit sur l'impression, et donc travailler avec lui apportera moins de dépendances dans l'objet.

Ainsi, l'injection de dépendances interrompra l'encapsulation si vous utilisez la première définition. Mais, franchement, je ne sais pas si j'aime la première définition - elle ne s'adapte clairement pas (si c'était le cas, MS Word serait une grande classe).

D'un autre côté, l'injection de dépendances est presque obligatoire si vous utilisez la deuxième définition de l'encapsulation.


Je suis tout à fait d'accord avec vous sur la première définition. Il viole également le SoC, qui est sans doute l'un des péchés cardinaux de la programmation et probablement l'une des raisons pour lesquelles il ne s'adapte pas.
Marcus Stade

4

Cela ne viole pas l'encapsulation. Vous fournissez un collaborateur, mais la classe décide de son utilisation. Tant que vous suivez Tell, ne demandez pas , tout va bien. Je trouve que l'injection du constructeur est préférable, mais les setters peuvent être bien aussi longtemps qu'ils sont intelligents. C'est-à-dire qu'ils contiennent une logique pour maintenir les invariants que la classe représente.


1
Parce que ... non? Si vous avez un enregistreur et que vous avez une classe qui a besoin de l'enregistreur, le passage de l'enregistreur à cette classe ne viole pas l'encapsulation. Et c'est tout ce qu'est l'injection de dépendances.
jrockway

3
Je pense que vous comprenez mal l'encapsulation. Par exemple, prenez une classe de date naïve. En interne, il peut avoir des variables d'instance jour, mois et année. Si ceux-ci étaient exposés comme de simples setters sans logique, cela interromprait l'encapsulation, puisque je pourrais faire quelque chose comme régler le mois à 2 et le jour à 31. D'un autre côté, si les setters sont intelligents et vérifient les invariants, alors tout va bien . Notez également que dans la dernière version, je pourrais changer le stockage en jours depuis le 1/1/1970, et rien de ce qui utilisait l'interface n'a besoin d'en être conscient à condition que je réécrive correctement les méthodes jour / mois / année.
Jason Watkins

2
DI enfreint définitivement l'encapsulation / le masquage d'informations. Si vous transformez une dépendance interne privée en quelque chose d'exposé dans l'interface publique de la classe, alors par définition, vous avez rompu l'encapsulation de cette dépendance.
Rogério

2
J'ai un exemple concret où je pense que l'encapsulation est compromise par DI. J'ai un FooProvider qui obtient des «données toto» de la base de données et un FooManager qui le met en cache et calcule des éléments au-dessus du fournisseur. J'ai eu des consommateurs de mon code qui sont allés par erreur au FooProvider pour les données, où j'aurais préféré l'avoir encapsulé afin qu'ils ne connaissent que le FooManager. C'est essentiellement le déclencheur de ma question initiale.
urig

1
@Rogerio: Je dirais que le constructeur ne fait pas partie de l'interface publique, car il n'est utilisé que dans la racine de la composition. Ainsi, la dépendance n'est "vue" que par la racine de la composition. La seule responsabilité de la racine de composition est de relier ces dépendances. Ainsi, l'utilisation de l'injection de constructeur ne rompt aucune encapsulation.
Jay Sullivan

4

C'est similaire à la réponse votée mais je veux réfléchir à haute voix - peut-être que d'autres voient les choses de cette façon également.

  • Le OO classique utilise des constructeurs pour définir le contrat public "d'initialisation" pour les consommateurs de la classe (masquant TOUS les détails d'implémentation; aka l'encapsulation). Ce contrat peut garantir qu'après instanciation, vous disposez d'un objet prêt à l'emploi (c'est-à-dire qu'aucune étape d'initialisation supplémentaire ne doit être mémorisée (euh, oubliée) par l'utilisateur).

  • (constructeur) DI rompt indéniablement l'encapsulation en saignant les détails d'implémentation via cette interface de constructeur publique. Tant que nous considérons toujours le constructeur public responsable de la définition du contrat d'initialisation pour les utilisateurs, nous avons créé une horrible violation de l'encapsulation.

Exemple théorique:

La classe Foo a 4 méthodes et a besoin d'un entier pour l'initialisation, donc son constructeur ressemble à Foo (taille int) et il est immédiatement clair pour les utilisateurs de la classe Foo qu'ils doivent fournir une taille à l'instanciation pour que Foo fonctionne.

Disons que cette implémentation particulière de Foo peut également avoir besoin d'un IWidget pour faire son travail. L'injection de constructeur de cette dépendance nous ferait créer un constructeur comme Foo (int size, widget IWidget)

Ce qui me dérange à ce sujet, c'est que nous avons maintenant un constructeur qui mélange les données d'initialisation avec les dépendances - une entrée intéresse l'utilisateur de la classe ( taille ), l'autre est une dépendance interne qui ne sert qu'à confondre l'utilisateur et est une implémentation détail ( widget ).

Le paramètre size n'est PAS une dépendance - c'est simplement une valeur d'initialisation par instance. IoC est dandy pour les dépendances externes (comme le widget) mais pas pour l'initialisation de l'état interne.

Pire encore, que se passe-t-il si le widget n'est nécessaire que pour 2 des 4 méthodes de cette classe; Il se peut que je subisse une surcharge d'instanciation pour Widget même s'il ne peut pas être utilisé!

Comment compromettre / concilier cela?

Une approche consiste à passer exclusivement aux interfaces pour définir le contrat d'exploitation; et abolir l'utilisation des constructeurs par les utilisateurs. Pour être cohérent, tous les objets devraient être accessibles uniquement via des interfaces et instanciés uniquement via une forme de résolveur (comme un conteneur IOC / DI). Seul le conteneur peut instancier les choses.

Cela prend en charge la dépendance Widget, mais comment initialiser "size" sans recourir à une méthode d'initialisation distincte sur l'interface Foo? En utilisant cette solution, nous avons perdu la possibilité de nous assurer qu'une instance de Foo est complètement initialisée au moment où vous obtenez l'instance. Dommage, parce que j'aime vraiment l' idée et la simplicité de l'injection de constructeur.

Comment obtenir une initialisation garantie dans ce monde DI, lorsque l'initialisation est PLUS QUE SEULEMENT des dépendances externes?


Mise à jour: Je viens de remarquer qu'Unity 2.0 prend en charge la fourniture de valeurs pour les paramètres du constructeur (tels que les initialiseurs d'état), tout en utilisant le mécanisme normal pour l'IoC des dépendances lors de la résolution (). Peut-être que d'autres conteneurs prennent également en charge cela? Cela résout la difficulté technique de mélanger l'état init et DI dans un seul constructeur, mais cela viole toujours l'encapsulation!
shawnT

Je t'entends. J'ai posé cette question parce que je sens moi aussi que deux bonnes choses (DI et encapsulation) viennent l'une au détriment de l'autre. BTW, dans votre exemple où seulement 2 sur 4 méthodes ont besoin de l'IWidget, cela indiquerait que les 2 autres appartiennent à un composant différent à mon humble avis.
urig

3

L'encapsulation pure est un idéal qui ne peut jamais être atteint. Si toutes les dépendances étaient masquées, vous n'auriez pas du tout besoin de DI. Pensez-y de cette façon, si vous avez vraiment des valeurs privées qui peuvent être internalisées dans l'objet, par exemple la valeur entière de la vitesse d'un objet voiture, alors vous n'avez pas de dépendance externe et pas besoin d'inverser ou d'injecter cette dépendance. Ces sortes de valeurs d'état internes qui sont exploitées uniquement par des fonctions privées sont ce que vous voulez toujours encapsuler.

Mais si vous construisez une voiture qui veut un certain type d'objet moteur, vous avez une dépendance externe. Vous pouvez soit instancier ce moteur - par exemple new GMOverHeadCamEngine () - en interne dans le constructeur de l'objet voiture, en préservant l'encapsulation mais en créant un couplage beaucoup plus insidieux à une classe concrète GMOverHeadCamEngine, ou vous pouvez l'injecter, permettant à votre objet Car de fonctionner de manière agnostique (et beaucoup plus robuste) sur par exemple une interface IEngine sans la dépendance concrète. Que vous utilisiez un conteneur IOC ou une simple DI pour y parvenir n'est pas le but - le fait est que vous avez une voiture qui peut utiliser de nombreux types de moteurs sans être couplée à aucun d'entre eux, rendant ainsi votre base de code plus flexible et moins sujet aux effets secondaires.

La DI n'est pas une violation de l'encapsulation, c'est un moyen de minimiser le couplage lorsque l'encapsulation est nécessairement interrompue naturellement dans pratiquement tous les projets POO. L'injection d'une dépendance dans une interface minimise les effets secondaires du couplage et permet à vos classes de rester indépendantes de l'implémentation.


3

Cela dépend si la dépendance est vraiment un détail d'implémentation ou quelque chose que le client voudrait / aurait besoin de savoir d'une manière ou d'une autre. Une chose qui est pertinente est le niveau d'abstraction ciblé par la classe. Voici quelques exemples:

Si vous avez une méthode qui utilise la mise en cache sous le capot pour accélérer les appels, alors l'objet de cache doit être un Singleton ou quelque chose et ne doit pas être injecté. Le fait que le cache soit utilisé est un détail d'implémentation dont les clients de votre classe ne devraient pas avoir à se soucier.

Si votre classe a besoin de générer des flux de données, il est probablement judicieux d'injecter le flux de sortie afin que la classe puisse facilement afficher les résultats dans un tableau, un fichier ou partout où quelqu'un d'autre voudrait envoyer les données.

Pour une zone grise, disons que vous avez une classe qui fait une simulation de Monte Carlo. Il a besoin d'une source d'aléatoire. D'une part, le fait qu'il en ait besoin est un détail de mise en œuvre dans la mesure où le client ne se soucie vraiment pas exactement d'où vient le caractère aléatoire. D'un autre côté, étant donné que les générateurs de nombres aléatoires du monde réel font des compromis entre le degré d'aléatoire, la vitesse, etc. que le client peut vouloir contrôler, et le client peut vouloir contrôler l'amorçage pour obtenir un comportement reproductible, l'injection peut avoir un sens. Dans ce cas, je suggérerais d'offrir un moyen de créer la classe sans spécifier de générateur de nombres aléatoires et d'utiliser un Singleton local au thread par défaut. Si / quand le besoin d'un contrôle plus fin se fait sentir, fournissez un autre constructeur qui permet d'injecter une source d'aléatoire.


2

Je crois à la simplicité. L'application de l'injection IOC / Dependecy dans les classes de domaine n'apporte aucune amélioration, sauf en rendant le code beaucoup plus difficile à gérer en ayant un fichier xml externe décrivant la relation. De nombreuses technologies comme EJB 1.0 / 2.0 et struts 1.1 font marche arrière en réduisant le contenu du XML et en essayant de les mettre dans le code sous forme d'annoation, etc. Donc, appliquer IOC pour toutes les classes que vous développez rendra le code insensé.

IOC présente des avantages lorsque l'objet dépendant n'est pas prêt pour la création au moment de la compilation. Cela peut se produire dans la plupart des composants d'architecture de niveau abstrait d'infrasture, en essayant d'établir un cadre de base commun qui peut devoir fonctionner pour différents scénarios. Dans ces endroits, l'utilisation d'IOC a plus de sens. Pourtant, cela ne rend pas le code plus simple / maintenable.

Comme toutes les autres technologies, cela a aussi des avantages et des inconvénients. Mon souci est que nous implémentons les dernières technologies dans tous les endroits, quelle que soit leur meilleure utilisation du contexte.


2

L'encapsulation n'est interrompue que si une classe a à la fois la responsabilité de créer l'objet (ce qui nécessite la connaissance des détails d'implémentation) et utilise ensuite la classe (qui ne nécessite pas la connaissance de ces détails). Je vais vous expliquer pourquoi, mais d'abord une anaologie rapide de la voiture:

Quand je conduisais mon vieux Kombi 1971, je pouvais appuyer sur l'accélérateur et ça allait (légèrement) plus vite. Je n'avais pas besoin de savoir pourquoi, mais les gars qui ont construit le Kombi à l'usine savaient exactement pourquoi.

Mais revenons au codage. L'encapsulation «cache un détail d'implémentation de quelque chose utilisant cette implémentation». L'encapsulation est une bonne chose car les détails d'implémentation peuvent changer sans que l'utilisateur de la classe le sache.

Lors de l'utilisation de l'injection de dépendances, l'injection de constructeur est utilisée pour construire des objets de type service (par opposition aux objets entité / valeur dont l'état du modèle). Toutes les variables de membre dans l'objet de type de service représentent des détails d'implémentation qui ne doivent pas s'échapper. par exemple, le numéro de port du socket, les informations d'identification de la base de données, une autre classe à appeler pour effectuer le chiffrement, un cache, etc.

Le constructeur est pertinent lors de la création initiale de la classe. Cela se produit pendant la phase de construction pendant que votre conteneur DI (ou usine) connecte tous les objets de service. Le conteneur DI ne connaît que les détails d'implémentation. Il sait tout sur les détails de mise en œuvre comme les gars de l'usine Kombi connaissent les bougies d'allumage.

Au moment de l'exécution, l'objet de service qui a été créé est appelé apon pour effectuer un travail réel. À ce stade, l'appelant de l'objet ne sait rien des détails d'implémentation.

C'est moi qui conduit mon Kombi à la plage.

Maintenant, revenons à l'encapsulation. Si les détails de l'implémentation changent, la classe qui utilise cette implémentation au moment de l'exécution n'a pas besoin de changer. L'encapsulation n'est pas interrompue.

Je peux aussi conduire ma nouvelle voiture à la plage. L'encapsulation n'est pas interrompue.

Si les détails d'implémentation changent, le conteneur DI (ou l'usine) doit changer. Vous n'avez jamais essayé de masquer les détails d'implémentation de l'usine en premier lieu.


Comment testeriez-vous votre usine? Et cela signifie que le client doit connaître l'usine pour obtenir une voiture en état de marche, ce qui signifie que vous avez besoin d'une usine pour chaque autre objet de votre système.
Rodrigo Ruiz

2

Ayant lutté un peu plus loin avec le problème, je suis maintenant d'avis que l'injection de dépendance viole (à l'heure actuelle) l'encapsulation dans une certaine mesure. Ne vous méprenez pas cependant - je pense que l'utilisation de l'injection de dépendances vaut bien le compromis dans la plupart des cas.

La raison pour laquelle DI viole l'encapsulation devient claire lorsque le composant sur lequel vous travaillez doit être livré à une partie "externe" (pensez à écrire une bibliothèque pour un client).

Lorsque mon composant nécessite l'injection de sous-composants via le constructeur (ou des propriétés publiques), il n'y a aucune garantie pour

"empêcher les utilisateurs de mettre les données internes du composant dans un état invalide ou incohérent".

En même temps, on ne peut pas dire que

"les utilisateurs du composant (d'autres logiciels) ont seulement besoin de savoir ce que fait le composant et ne peuvent pas se rendre dépendants des détails sur la façon dont il le fait" .

Les deux citations proviennent de wikipedia .

Pour donner un exemple spécifique: je dois fournir une DLL côté client qui simplifie et masque la communication avec un service WCF (essentiellement une façade distante). Parce que cela dépend de 3 classes de proxy WCF différentes, si j'adopte l'approche DI, je suis obligé de les exposer via le constructeur. Avec cela, j'expose les éléments internes de ma couche de communication que j'essaye de cacher.

En général, je suis tout à fait pour DI. Dans cet exemple particulier (extrême), cela me paraît dangereux.


2

DI viole l'encapsulation pour les objets NON partagés - point final. Les objets partagés ont une durée de vie en dehors de l'objet en cours de création et doivent donc être AGRÉGÉS dans l'objet en cours de création. Les objets qui sont privés de l'objet en cours de création doivent être COMPOSÉS dans l'objet créé - lorsque l'objet créé est détruit, il prend l'objet composé avec lui. Prenons l'exemple du corps humain. Ce qui est composé et ce qui est agrégé. Si nous devions utiliser DI, le constructeur du corps humain aurait des centaines d'objets. De nombreux organes, par exemple, sont (potentiellement) remplaçables. Mais, ils sont toujours composés dans le corps. Les cellules sanguines sont créées dans le corps (et détruites) tous les jours, sans avoir besoin d'influences externes (autres que les protéines). Ainsi, les cellules sanguines sont créées en interne par le corps - new BloodCell ().

Les défenseurs de DI soutiennent qu'un objet ne doit JAMAIS utiliser le nouvel opérateur. Cette approche "puriste" viole non seulement l'encapsulation mais aussi le principe de substitution de Liskov pour quiconque crée l'objet.


1

J'ai également eu du mal avec cette notion. Au début, «l'exigence» d'utiliser le conteneur DI (comme Spring) pour instancier un objet ressemblait à un saut à travers des cerceaux. Mais en réalité, ce n'est vraiment pas un cerceau - c'est juste une autre façon «publiée» de créer les objets dont j'ai besoin. Bien sûr, l'encapsulation est «cassée» parce que quelqu'un «en dehors de la classe» sait ce dont il a besoin, mais ce n'est vraiment pas le reste du système qui le sait - c'est le conteneur DI. Rien de magique ne se passe différemment car DI «sait» qu'un objet en a besoin d'un autre.

En fait, cela s'améliore encore - en me concentrant sur les usines et les référentiels, je n'ai même pas besoin de savoir que DI est impliqué du tout! Cela me remet le couvercle sur l'encapsulation. Ouf!


1
Tant que DI est en charge de toute la chaîne d'instanciation, je conviens que l'encapsulation se produit en quelque sorte. En quelque sorte, parce que les dépendances sont toujours publiques et peuvent être abusées. Mais quand quelque part "en haut" dans la chaîne, quelqu'un a besoin d'instancier un objet sans qu'il utilise DI (peut-être qu'ils sont un "tiers") alors ça devient compliqué. Ils sont exposés à vos dépendances et pourraient être tentés d'en abuser. Ou ils pourraient ne pas vouloir en savoir du tout.
urig

1

PS. En fournissant l' injection de dépendances, vous ne rompez pas nécessairement l' encapsulation . Exemple:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Le code client ne connaît toujours pas les détails d'implémentation.


Dans votre exemple, comment quelque chose est-il injecté? (Sauf pour le nom de votre fonction de passeur)
foo

1

C'est peut-être une façon naïve d'y penser, mais quelle est la différence entre un constructeur qui prend un paramètre entier et un constructeur qui prend un service comme paramètre? Cela signifie-t-il que la définition d'un entier en dehors du nouvel objet et son introduction dans l'objet interrompt l'encapsulation? Si le service n'est utilisé que dans le nouvel objet, je ne vois pas comment cela briserait l'encapsulation.

De plus, en utilisant une sorte de fonction de câblage automatique (Autofac pour C #, par exemple), cela rend le code extrêmement propre. En créant des méthodes d'extension pour le constructeur Autofac, j'ai pu supprimer BEAUCOUP de code de configuration DI que j'aurais dû maintenir au fil du temps à mesure que la liste des dépendances s'allongeait.


Ce n'est pas une question de valeur par rapport au service. Il s'agit de l'inversion du contrôle - si le constructeur de la classe configure la classe ou si ce service prend en charge la configuration de la classe. Pour lequel il a besoin de connaître les détails d'implémentation de cette classe, vous avez donc un autre endroit à maintenir et à synchroniser.
foo

1

Je pense qu'il va de soi qu'à tout le moins DI affaiblit considérablement l'encapsulation. En plus de cela, voici quelques autres inconvénients de l'ID à considérer.

  1. Cela rend le code plus difficile à réutiliser. Un module qu'un client peut utiliser sans avoir à fournir explicitement des dépendances est évidemment plus facile à utiliser qu'un module où le client doit d'une manière ou d'une autre découvrir quelles sont les dépendances de ce composant, puis les rendre disponibles. Par exemple, un composant créé à l'origine pour être utilisé dans une application ASP peut s'attendre à ce que ses dépendances soient fournies par un conteneur DI qui fournit aux instances d'objet des durées de vie liées aux demandes http du client. Cela peut ne pas être simple à reproduire dans un autre client qui ne vient pas avec le même conteneur DI intégré que l'application ASP d'origine.

  2. Cela peut rendre le code plus fragile. Les dépendances fournies par la spécification d'interface peuvent être implémentées de manière inattendue, ce qui donne lieu à toute une classe de bogues d'exécution qui ne sont pas possibles avec une dépendance concrète résolue statiquement.

  3. Cela peut rendre le code moins flexible dans le sens où vous risquez de vous retrouver avec moins de choix quant à la façon dont vous voulez qu'il fonctionne. Toutes les classes n'ont pas besoin d'exister toutes ses dépendances pendant toute la durée de vie de l'instance propriétaire, mais avec de nombreuses implémentations DI, vous n'avez pas d'autre option.

Dans cet esprit, je pense que la question la plus importante devient alors " une dépendance particulière doit-elle être spécifiée de l'extérieur? ". En pratique, j'ai rarement trouvé nécessaire de créer une dépendance fournie en externe uniquement pour prendre en charge les tests.

Lorsqu'une dépendance doit véritablement être fournie de l'extérieur, cela suggère normalement que la relation entre les objets est une collaboration plutôt qu'une dépendance interne, auquel cas le but approprié est alors l'encapsulation de chaque classe, plutôt que l'encapsulation d'une classe dans l'autre .

D'après mon expérience, le principal problème concernant l'utilisation de DI est que, que vous commenciez avec un framework d'application avec DI intégré, ou que vous ajoutiez le support DI à votre base de code, pour une raison quelconque, les gens supposent que puisque vous avez le support DI, cela doit être le bon façon de tout instancier . Ils ne se donnent même jamais la peine de se poser la question "cette dépendance doit-elle être spécifiée de l'extérieur?". Et pire, ils commencent également à essayer de forcer tout le monde à utiliser le support DI pour tout aussi.

Le résultat de ceci est que inexorablement votre base de code commence à évoluer dans un état où la création de toute instance de quoi que ce soit dans votre base de code nécessite des rames de configuration de conteneur DI obtuse, et le débogage de quoi que ce soit est deux fois plus difficile car vous avez la charge de travail supplémentaire d'essayer d'identifier comment et où tout a été instancié.

Ma réponse à la question est donc la suivante. Utilisez DI où vous pouvez identifier un problème réel qu'il résout pour vous, que vous ne pouvez pas résoudre plus simplement d'une autre manière.


0

Je suis d'accord que prise à l'extrême, DI peut violer l'encapsulation. Habituellement, DI expose des dépendances qui n'ont jamais été vraiment encapsulées. Voici un exemple simplifié emprunté à Singletons are Pathological Liars de Miško Hevery :

Vous commencez par un test de carte de crédit et écrivez un test unitaire simple.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

Le mois prochain, vous recevrez une facture de 100 $. Pourquoi avez-vous été facturé? Le test unitaire a affecté une base de données de production. En interne, les appels par carte de crédit Database.getInstance(). Refactoriser CreditCard afin qu'il prenne un DatabaseInterfacedans son constructeur expose le fait qu'il existe une dépendance. Mais je dirais que la dépendance n'a jamais été encapsulée au départ car la classe CreditCard provoque des effets secondaires visibles de l'extérieur. Si vous souhaitez tester CreditCard sans refactoring, vous pouvez certainement observer la dépendance.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

Je ne pense pas que cela vaille la peine de se demander si exposer la base de données en tant que dépendance réduit l'encapsulation, car c'est une bonne conception. Toutes les décisions de DI ne seront pas aussi simples. Cependant, aucune des autres réponses ne montre un contre-exemple.


1
Les tests unitaires sont généralement écrits par l'auteur de la classe, il est donc possible d'énoncer les dépendances dans le cas de test, d'un point de vue technique. Lorsque la classe de carte de crédit change ultérieurement pour utiliser une API Web telle que PayPal, l'utilisateur devra tout changer s'il était DIed. Les tests unitaires sont généralement effectués avec une connaissance intime du sujet testé (n'est-ce pas le but?) Donc je pense que les tests sont plus une exception qu'un exemple typique.
kizzx2

1
Le but de DI est d'éviter les changements que vous avez décrits. Si vous passiez d'une API Web à PayPal, vous ne changeriez pas la plupart des tests car ils utiliseraient un MockPaymentService et CreditCard serait construit avec un PaymentService. Vous auriez juste une poignée de tests pour examiner l'interaction réelle entre la carte de crédit et un vrai service de paiement, les changements futurs sont donc très isolés. Les avantages sont encore plus grands pour les graphiques de dépendance plus profonds (comme une classe qui dépend de CreditCard).
Craig P. Motlin

@Craig p. Motlin Comment l' CreditCardobjet peut-il passer d'une WebAPI à PayPal, sans que rien d'extérieur à la classe ne doive changer?
Ian Boyd du

@Ian j'ai mentionné que CreditCard devrait être refactorisé pour prendre une DatabaseInterface dans son constructeur qui la protège des changements dans les implémentations de DatabaseInterfaces. Peut-être que cela doit être encore plus générique et prendre une StorageInterface dont WebAPI pourrait être une autre implémentation. PayPal est au mauvais niveau, car c'est une alternative à la carte de crédit. PayPal et CreditCard pourraient implémenter PaymentInterface pour protéger d'autres parties de l'application de l'extérieur de cet exemple.
Craig P. Motlin

@ kizzx2 En d'autres termes, il est absurde de dire qu'une carte de crédit devrait utiliser PayPal. Ce sont des alternatives dans la vraie vie.
Craig P. Motlin

0

Je pense que c'est une question de portée. Lorsque vous définissez l'encapsulation (sans savoir comment), vous devez définir quelle est la fonctionnalité encapsulée.

  1. Classe telle quelle: ce que vous encapsulez est la seule responsabilité de la classe. Ce qu'il sait faire. Par exemple, le tri. Si vous injectez un comparateur pour commander, disons, des clients, cela ne fait pas partie de la chose encapsulée: le tri rapide.

  2. Fonctionnalité configurée : si vous souhaitez fournir une fonctionnalité prête à l'emploi, vous ne fournissez pas la classe QuickSort, mais une instance de la classe QuickSort configurée avec un comparateur. Dans ce cas, le code responsable de la création et de la configuration doit être masqué du code utilisateur. Et c'est l'encapsulation.

Lorsque vous programmez des classes, c'est-à-dire que vous implémentez des responsabilités uniques dans des classes, vous utilisez l'option 1.

Lorsque vous programmez des applications, c'est-à-dire que vous faites quelque chose qui entreprend un travail concret utile, vous utilisez à plusieurs reprises l'option 2.

Voici l'implémentation de l'instance configurée:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

Voici comment un autre code client l'utilise:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Il est encapsulé car si vous modifiez l'implémentation (vous changez la clientSorterdéfinition du bean), cela ne rompt pas l'utilisation du client. Peut-être que, lorsque vous utilisez des fichiers xml avec tous écrits ensemble, vous voyez tous les détails. Mais croyez-moi, le code client ( ClientService) ne sait rien de son trieur.


0

Cela vaut probablement la peine de mentionner que cela Encapsulationdépend quelque peu de la perspective.

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

Du point de vue de quelqu'un travaillant sur la Aclasse, dans le deuxième exemple en Asait beaucoup moins sur la naturethis.b

Alors que sans DI

new A()

contre

new A(new B())

La personne qui regarde ce code en sait plus sur la nature de Adans le deuxième exemple.

Avec DI, au moins toutes ces connaissances divulguées se trouvent au même endroit.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.