Question

Si je comprends bien, le mécanisme typique de Dependency Injection consiste à injecter via le constructeur d'une classe ou une propriété publique (membre) de la classe.

Ceci expose la dépendance injectée et enfreint le principe d'encapsulation en POO.

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

Voir aussi ma réponse à ma propre question ci-dessous.

Était-ce utile?

La solution

Il existe une autre façon d’examiner cette question qui pourrait vous intéresser.

Lorsque nous utilisons l'injection IoC / dependency, nous n'utilisons pas les concepts de programmation orientée objet. Certes, nous utilisons un langage OO en tant qu’hôte, mais les idées qui sous-tendent IoC proviennent du génie logiciel orienté composants, et non de OO.

Le logiciel de composant concerne uniquement la gestion des dépendances. Un exemple couramment utilisé est le mécanisme d'assemblage de .NET. Chaque assemblage publie la liste des assemblys auxquels il fait référence, ce qui facilite grandement la compilation (et la validation) des éléments nécessaires à l'exécution d'une application.

En appliquant des techniques similaires dans nos programmes OO via IoC, nous souhaitons faciliter la configuration et la maintenance des programmes. La publication de 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é composant / service, il n’existe pas de «type d’implémentation» pour la fuite de détails.

Malheureusement, nos langages ne séparent pas actuellement les concepts orientés objet à grain fin des concepts orientés composant à grain plus grossier, c'est donc une distinction que vous ne devez garder à l'esprit que:)

Autres conseils

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

Classiquement, ce dernier cas est traité 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 que la dépendance sera définie via réflexion sans que vous ayez besoin d'exposer la dépendance via l'une des classes méthodes / constructeurs publiques. C’est peut-être le type de solution que vous recherchiez.

Cela dit, je ne pense pas non plus que l’injection de constructeur pose un gros 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 jouer leur rôle (c'est-à-dire être dans un état valide) soit de toute façon fourni par le constructeur. Si vous avez un objet qui nécessite un collaborateur, il me semble bien que le constructeur publie publiquement cette exigence et veille à ce qu'il soit rempli lors de la création d'une nouvelle instance de la classe.

Idéalement, lorsque vous manipulez des objets, vous interagissez de toute façon avec eux via une interface. Plus vous faites cela (et vous avez des dépendances câblées via DI), moins vous avez réellement à traiter vous-même avec les constructeurs. Dans l'idéal, votre code ne traite pas et ne crée même jamais d'instances concrètes de classes. il reçoit donc simplement un IFoo via DI, sans se soucier de ce que le constructeur de FooImpl indique qu'il doit faire son travail, et en fait sans même connaître le code FooImpl . De ce point de vue, l’encapsulation est parfaite.

C’est bien sûr une opinion, mais je pense que D ne viole pas nécessairement l’encapsulation, mais qu’elle peut même l’aider en centralisant toutes les connaissances nécessaires sur les composants internes. 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 des codes que vous écrivez n'a besoin de connaître les dépendances des classes.

  

Ceci expose la dépendance injectée et enfreint le principe d'encapsulation en POO.

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

Alors, qu'est-ce qui ne respecte pas l'encapsulation?

Héritage fait .

  

"Parce que l'héritage expose une sous-classe aux détails de la mise en œuvre de son parent, il est souvent dit que" l'héritage rompt l'encapsulation "". (Gang of Four 1995: 19)

Programmation orientée vers les aspects fait . Par exemple, vous enregistrez un rappel onMethodCall () et cela vous donne une excellente occasion d’injecter du code dans l’évaluation de méthode normale, en ajoutant d’étranges effets secondaires, etc.

Déclaration d'un ami en C ++ fait .

L'extension de classe en ruby ?? existe . Il suffit de redéfinir 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 font .

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

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

Oui, DI enfreint l'encapsulation (également appelé "masquage d'informations").

Mais le vrai problème vient lorsque les développeurs l'utilisent comme excuse pour violer les principes KISS (Keep It Short and Simple) et YAGNI (Vous n'en aurez pas besoin).

Personnellement, je préfère les solutions simples et efficaces. J'utilise principalement le " nouveau " opérateur pour instancier des dépendances avec état à tout moment et en tout lieu. Il est simple, bien encapsulé, facile à comprendre et à tester. Alors pourquoi pas?

Un conteneur / système d’injection de dépendance efficace permettra l’injection par le constructeur. Les objets dépendants seront encapsulés et ne devront pas du tout être exposés publiquement. De plus, en utilisant un système DP, aucun de vos codes, même "ne sait", les détails de la construction de l'objet, y compris éventuellement l'objet en cours de construction. Dans ce cas, l'encapsulation est plus importante 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, le plus probablement dans son constructeur. D'après ce que je comprends de DP, nous souhaitons soustraire cette responsabilité à l'objet et la confier à 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". encapsulation; L'avantage est qu'il tire cette connaissance de l'objet, elle-même. Quelqu'un doit l'avoir. Le reste de votre demande ne le fait pas.

J'y penserais de cette façon: le conteneur / système d'injection de dépendance enfreint l'encapsulation, mais votre code ne le fait pas. En fait, votre code est plus " encapsulé " alors jamais.

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

Cela ressemble à la réponse votée, mais je veux réfléchir à voix haute. Peut-être que d'autres voient les choses de cette façon aussi.

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

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

Exemple théorique:

La classe Foo a 4 méthodes et nécessite un entier pour l'initialisation. Son constructeur ressemble donc à Foo (taille int) . Les utilisateurs de la classe le voient immédiatement. Foo qu'ils doivent fournir une taille à l'instanciation pour que Foo fonctionne.

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

Ce qui m'irrite, c’est que nous avons maintenant un constructeur qui fusionne les données d’initialisation avec des dépendances - une entrée est intéressante pour l’utilisateur de la classe ( taille ), l'autre est une dépendance interne qui ne sert qu'à dérouter l'utilisateur et constitue un détail d'implémentation ( widget ).

Le paramètre size n'est PAS une dépendance. Il s'agit simplement d'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 doit pas être utilisé!

Comment faire un compromis / réconcilier cela?

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

Cela prend en charge la dépendance du widget, mais comment initialiser la " taille " 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 l'obtenez. Bummer, parce que j'aime beaucoup l'idée et la simplicité de l'injection de constructeur.

Comment obtenir une initialisation garantie dans ce monde DI lorsque l’initialisation est supérieure à SEULEMENT les dépendances externes?

Comme Jeff Sternal l'a souligné dans un commentaire sur la question, la réponse dépend entièrement de la définition de encapsulation .

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

  1. Tout ce qui concerne l'objet est une méthode sur un objet. Ainsi, un objet Fichier peut avoir des méthodes pour Enregistrer , Imprimer , Afficher , ModifyText , etc.
  2. Un objet est son propre petit monde et ne dépend pas d'un comportement extérieur.

Ces deux définitions sont en contradiction directe. Si un objet Fichier peut s’imprimer lui-même, cela dépendra beaucoup du comportement de l’imprimante. Par contre, si il connaît simplement quelque chose qui peut l’imprimer (un IFilePrinter ou une interface similaire), alors l’objet Fichier ne devez rien savoir de l’impression, et travailler avec elle apportera moins de dépendances à l’objet.

Ainsi, l’injection de dépendance rompra 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 n’a clairement pas d’échelle (si tel était le cas, MS Word serait une grande classe).

D'autre part, l'injection de dépendance est presque obligatoire si vous utilisez la deuxième définition de l'encapsulation.

L’encapsulation pure est un idéal qui ne peut jamais être atteint. Si toutes les dépendances étaient cachées, vous n’auriez plus besoin de DI. Pensez-y de cette façon, si vous avez réellement des valeurs privées pouvant être internalisées dans l'objet, par exemple la valeur entière de la vitesse d'un objet voiture, vous n'avez alors aucune dépendance externe ni besoin d'inverser ou d'injecter cette dépendance. Ce genre de valeurs d’état interne qui ne sont exploitées que par des fonctions privées correspond à 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, le nouveau GMOverHeadCamEngine () - en interne dans le constructeur de l'objet car, préservant ainsi l'encapsulation, mais en créant un couplage beaucoup plus insidieux avec une classe concrète GMOverHeadCamEngine - ou 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, ce n’est pas l’essentiel - c’est que vous avez une voiture qui peut utiliser de nombreux types de moteurs sans être couplée à aucun d’eux, ce qui rend votre base de code plus flexible et plus souple. moins sujet aux effets secondaires.

DI n’est pas une violation de l’encapsulation, mais un moyen de minimiser le couplage lorsque l’encapsulation est forcément rompue dans la quasi-totalité des projets de POO. L'injection externe d'une dépendance dans une interface minimise les effets secondaires du couplage et permet à vos classes de rester agnostiques face à la mise en œuvre.

Je crois en la simplicité. L’application de l’injection IOC / Dependecy dans les classes de domaine n’apporte aucune amélioration, sauf qu’il est beaucoup plus difficile de coder le code en ayant un fichier xml externe décrivant la relation. De nombreuses technologies telles que EJB 1.0 / 2.0 & amp; struts 1.1 reviennent en arrière en réduisant le contenu mis en XML et essayez de les mettre en code sous forme d'annotation, etc. Ainsi, l'application d'IOC pour toutes les classes que vous développez rendra le code dépourvu de sens.

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 arriver 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 est plus logique. Cela ne rend toutefois pas le code plus simple / maintenable.

Comme toutes les autres technologies, cela a aussi des PROs & amp; Les inconvénients. Ce qui me préoccupe, c’est que nous mettons en œuvre les dernières technologies dans tous les lieux, quel que soit le meilleur usage de leur contexte.

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 connaître d'une manière ou d'une autre. Une chose pertinente est le niveau d'abstraction ciblé par la classe. Voici quelques exemples:

Si vous utilisez une méthode qui utilise la mise en cache sous le capot pour accélérer les appels, l'objet de cache doit être un Singleton ou quelque chose du genre et doit ne pas être injecté. Le fait que le cache soit utilisé est un détail d'implémentation que 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 logique d’injecter le flux de sortie afin que la classe puisse facilement exporter les résultats dans un tableau, un fichier ou à tout autre endroit où une autre personne pourrait vouloir envoyer les données.

Pour une zone grise, supposons que vous ayez une classe qui effectue une simulation de monte carlo. Cela nécessite une source de hasard. D'une part, le fait qu'il ait besoin de cela est un détail d'implémentation dans la mesure où le client ne se soucie vraiment pas de savoir d'où vient le caractère aléatoire. D'autre part, é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 que le client peut vouloir contrôler l'ensemencement pour obtenir un comportement répétable, l'injection peut avoir un sens. Dans ce cas, je suggérerais de proposer un moyen de créer la classe sans spécifier de générateur de nombres aléatoires, et d'utiliser un Singleton avec thread-local par défaut. Si / lorsque le besoin d’un contrôle plus fin apparaît, fournissez un autre constructeur permettant d’injecter une source d’aléatoire.

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

  

Quand je conduisais mon vieux Kombi de 1971,   Je pourrais appuyer sur l'accélérateur et   est allé (légèrement) plus vite. je n'ai pas   besoin de savoir pourquoi, mais les gars qui   construit le Kombi à l'usine savait   exactement pourquoi.

Mais revenons au codage. L'encapsulation permet de "masquer un détail de la mise en œuvre à un élément utilisant cette mise en oeuvre". L’encapsulation est une bonne chose car les détails de l’implémentation peuvent changer sans que l’utilisateur de la classe ne le sache.

Lors de l'utilisation de l'injection de dépendance, l'injection du constructeur est utilisée pour construire des objets de type (par opposition aux objets entité / valeur dont l'état du modèle). Toute variable membre dans l'objet de type de service représente des détails d'implémentation qui ne doivent pas fuir. par exemple. numéro de port du socket, informations d'identification de la base de données, une autre classe à appeler pour effectuer le cryptage, 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) relie ensemble tous les objets de service. Le conteneur DI ne connaît que les détails de la mise en œuvre. Il connaît tous les détails de la mise en œuvre, tout comme les gars de l'usine Kombi connaissent les bougies d'allumage.

Au moment de l'exécution, l'objet de service créé est appelé pour effectuer un travail réel. À ce moment, l'appelant de l'objet ne sait rien des détails de la mise en œuvre.

  

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

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

  

Je peux aussi conduire ma nouvelle voiture à la plage. L’encapsulation n’est pas brisée.

Si les détails de la mise en œuvre changent, le conteneur DI (ou l’usine) doit être changé. Vous n’avez jamais essayé de cacher les détails d’implémentation à l’usine.

Après avoir lutté un peu plus loin avec le problème, je suis maintenant d'avis que Dependency Injection viole (à ce stade) une certaine violation de l'encapsulation. Ne vous méprenez pas cependant - je pense que l'utilisation de l'injection de dépendance en vaut la peine dans la plupart des cas.

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

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

  

"Empêcher les utilisateurs de définir les données internes du composant dans un état incorrect ou incohérent".

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

  

"Les utilisateurs du composant (autres logiciels) ont simplement besoin de savoir ce que fait le composant et ne peuvent pas se rendre dépendant des détails de la procédure" .

.

Les deux citations sont extraites de wikipedia .

Pour donner un exemple spécifique: je dois fournir une DLL côté client qui simplifie et masque la communication vers un service WCF (essentiellement une façade distante). Parce que cela dépend de 3 classes de proxy WCF différentes, si je suis 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 je cherche à cacher.

En général, je suis tout pour DI. Dans cet exemple particulier (extrême), cela me semble dangereux.

J'ai également eu du mal avec cette notion. Au début, la «nécessité» d'utiliser le conteneur DI (comme Spring) pour instancier un objet ressenti comme si on sautait à 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 des objets dont j'ai besoin. Bien sûr, l’encapsulation est «brisée», car une personne «extérieure à la classe» sait ce dont elle 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 parce que DI 'sait' qu'un objet a besoin d'un autre.

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

PS. En fournissant l'injection de dépendance , vous ne ne supprimez pas nécessairement les encapsulations . Exemple:

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

Le code client ne connaît toujours pas les détails de la mise en oeuvre.

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 en tant que paramètre? Cela signifie-t-il que définir un entier en dehors du nouvel objet et l'introduire dans l'objet rompt l'encapsulation? Si le service est uniquement utilisé dans le nouvel objet, je ne vois pas en quoi cela romprait l’encapsulation.

De plus, en utilisant une sorte de fonctionnalité de câblage automatique (Autofac pour C #, par exemple), le code est extrêmement propre. En construisant des méthodes d'extension pour le constructeur Autofac, j'ai pu supprimer beaucoup de code de configuration DI que j'aurais dû conserver au fil du temps, à mesure que la liste des dépendances s'allongerait.

Je pense qu'il va de soi que, à tout le moins, l'ID affaiblit considérablement l'encapsulation. En plus de cela, voici quelques autres inconvénients de DI à 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 dans lequel le client doit en quelque sorte découvrir quelles sont les dépendances de ce composant et 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 requêtes http du client. Cela peut ne pas être simple à reproduire sur un autre client qui n’a pas 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 impossibles avec une dépendance concrète résolue de manière statique.

  3. Cela peut rendre le code moins flexible, en ce sens que vous aurez peut-être moins de choix quant à la façon dont vous voulez qu'il fonctionne. Toutes les classes ne doivent pas nécessairement posséder toutes leurs dépendances pendant toute la durée de vie de l'instance propriétaire, mais avec de nombreuses implémentations DI, vous n'avez aucune autre option.

Dans cet esprit, je pense que la question la plus importante est alors: " Une dépendance particulière doit-elle être spécifiée de manière externe? ". Dans la pratique, j’ai rarement trouvé nécessaire de créer une dépendance fournie de manière externe uniquement pour permettre les tests.

Lorsqu'une dépendance doit réellement être fournie de manière externe, cela suggère normalement que la relation entre les objets est une collaboration plutôt qu'une dépendance interne, auquel cas l'objectif approprié est alors d'encapsuler chaque classe . plutôt que d’encapsuler une classe dans une autre.

Selon mon expérience, le principal problème en ce qui concerne l’utilisation de DI est qu’il s’agisse de commencer par un framework d’application avec DI intégrée ou d’ajouter une prise en charge DI à votre base de code. être le bon moyen d'instancier tout . Ils n’ont même jamais pris la peine de poser la question "cette dépendance doit-elle être spécifiée de manière externe?". Et pire, ils commencent également à forcer tout le monde à utiliser le support ID pour tout également.

Il en résulte qu'inévitablement, votre base de code commence à évoluer dans un état où la création d'une instance de tout élément de votre base de code requiert une quantité considérable de configuration de conteneur DI obtus, et le débogage de tout élément est deux fois plus difficile, car vous avez la charge de travail supplémentaire nécessaire identifier comment et où quelque chose a été instancié.

Donc, ma réponse à la question est la suivante. Utilisez DI pour identifier un problème réel qu'il résout pour vous et que vous ne pouvez résoudre plus simplement autrement.

DI enfreint l'encapsulation pour les objets non partagés - point. 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 celui-ci. Les objets 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 est accompagné de l'objet composé. Prenons le corps humain à titre d'exemple. Ce qui est composé et ce qui est agrégé. Si nous utilisions DI, le constructeur du corps humain aurait une centaine 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 qu'il soit nécessaire de recourir à des influences externes (autres que les protéines). Ainsi, les cellules sanguines sont créées de manière interne par le corps - nouvelle BloodCell ().

Les avocats de DI soutiennent qu’un objet ne doit JAMAIS utiliser le nouvel opérateur. Ce " puriste " Cette approche enfreint non seulement l’encapsulation, mais aussi le principe de substitution de Liskov pour celui qui crée l’objet.

Je conviens que, à l'extrême, DI peut violer l'encapsulation. Habituellement, DI expose des dépendances qui n'ont jamais été réellement encapsulées. Voici un exemple simplifié emprunté à Singletons de Mio # Hevery sont des menteurs pathologiques :

Vous commencez avec un test CreditCard 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 as-tu été accusé? Le test unitaire a affecté une base de données de production. En interne, CreditCard appelle Database.getInstance () . Refactoriser CreditCard pour qu'il prenne un DatabaseInterface dans son constructeur expose le fait qu'il existe une dépendance. Mais je dirais que la dépendance n'a jamais été encapsulée car la classe CreditCard a 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 qu'il soit utile de se demander si exposer la base de données en tant que dépendance réduit l'encapsulation, car sa conception est bonne. Toutes les décisions de DI ne seront pas aussi simples. Cependant, aucune des autres réponses ne montre un contre-exemple.

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

  1. La classe telle quelle : ce que vous êtes en train d’encapsuler est la seule responsabilité de la classe. Ce qu'il sait faire Par exemple, le tri. Si vous injectez un comparateur pour la commande, disons, clients, cela ne fait pas partie de la chose encapsulée: quicksort.

  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 qu’en implémentant des responsabilités uniques dans les classes, vous utilisez l’option 1.

Lorsque vous programmez des applications, vous effectuez quelque chose qui nécessite un travail concret utile, puis vous utilisez de manière répétée 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 la mise en œuvre (vous modifiez la définition du bean clientSorter ), cela n'interrompt 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 pas rien sur sa trieuse.

Il vaut probablement la peine de mentionner que Encapsulation dépend en partie 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 d’une personne travaillant sur la classe A , dans le deuxième exemple, A en sait beaucoup moins sur la nature de this.b

Attendu sans DI

new A()

vs

new A(new B())

La personne qui consulte ce code en sait plus sur la nature de A dans le deuxième exemple.

Avec DI, au moins toute la fuite de connaissances se trouve au même endroit.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top